迭代器、生成器和装饰器


一、命名空间和作用域

1.命名空间

官方文档的一句话:
A namespace is a mapping from names to objects.
Most namespaces are currently implemented as Python dictionaries.

  命名空间(Namespace)是从名称到对象的映射,大部分的命名空间都是通过python字典来实现的。

  命名空间提供了在项目中避免名字冲突的一种方法。各个命名空间是独立的,没有任何关系的,所以一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。

  一般有三种命名空间:

  • 内置命名(built-in names),python语言内置的名称,比如函数名abs,char和异常名称BaseException,Exception等等;
  • 全局名称(global names),模块中定义的名称,记录了模块的变量,包括函数、类、其他导入的模块、模块级的变量和常量;
  • 局部名称(local names),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量(类中定义的也是);

  命名空间的查找顺序:使用变量“a”为例:

  命名空间的生命周期:取决于对象的作用域,如果对象执行完成,则该命名空间的生命周期就结束。因此,我们无法从外部命名空间访问内部命名空间的对象。如下图所示,相同的对象名称可以存在于多个命名空间中。

2.作用域

A scope is a textual region of a python program shere a namespace is directly accessible. "Directly accessible" here means that an unqualified reference to a name attempts to find the name in the namespace.

作用域 就是python程序可以直接访问命名空间的正文区域。

  • 在一个python程序中,直接访问一个变量,会从内到外依次访问所有的作用域直到找到,否则会报未定义的错误。
  • python中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量在哪里赋值的。
  • 变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称。

python的作用域一共有4种:

  • L (Local):最内层,包含局部变量,比如一个函数 / 方法的内部;
  • E (Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类)A里面又包含了一个函数B,那么对于B中的名称来说A中的作用域就为nonlocal;
  • G (Global):当前脚本的最外层,比如当前模块的全局变量;
  • B (Built-in):包含了内建的变量 / 关键字等,最后被搜索。

在局部找不到,便会去局部外的局部找(例如闭包),再找不到就回去全局找,再者去内置中找。

规则顺序: L ----> E ----> G ----> B

g_count = 0         # 全局作用域
def outer():
    o_count = 1     # 闭包函数外的函数中
    def inner():
        i_count = 2 # 局部作用域

内置作用域是通过一个名为builtin的标准模块来实现的,但是这个变量自身并没有放入内置作用域中,所以必须导入这个文件才能够使用它。再python3.0中,可以使用以下的代码查看预定义了哪些变量:

import builtins
print(dir(builtins))

python中也只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其他的代码块(如if/elif/else、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句定义的变量,外部也可以访问,如下代码:

if True:
    msg = 1111
    
print(msg)  # 1111

上例中msg定义在if语句块中,但外部还是可以访问的;如果将msg定义在函数中,则它就是局部变量,外部不能访问。msg是局部变量,只能在函数内可以使用。

def fun():
    msg = 2222
    
print(msg)  # NameError: name 'msg' is not defined

全局变量和局部变量

定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。调用函数时,所有在函数内声明的变量名称都将被加入到作用域中。如下实例:

total = 0       # total 这里是一个全局变量
def sum(a,b):
    total = a + b   # total 这里是一个局部变量
    print('函数内是局部变量: ',total)
    return total

sum(1,3)
print('函数外是全局变量:',total)
# 输出结果:
# 函数内是局部变量:  4
# 函数外是全局变量: 0

global和nonlocal关键字

当内部作用域想修改外部作用域的变量时,就要用到global和nonlocal关键字了。

  • 修改全局变量num:
num = 1
def func():
    global num
    print(num)      # 1
    num = 2
    print(num)      # 2

func()
print(num)          # 2
# 全局变量num由1修改成2
  • 如果要修改嵌套作用域(enclosing作用域,外层非全局作用域)中的变量则需要nonlocal关键字:
def outer():
    num = 10
    def inner():
        nonlocal num
        num = 100
        print(num)  # 100
    inner()
    print(num)      # 100 如果没有nonlocal num这一行代码,num值为10

outer()

二、迭代器

1.函数名的运用

  函数名是一个变量,一个特殊的变量,和括号配合可以执行函数的变量。

函数名的内存地址

def func():
    print('函数名的内存地址')
    
print(func) 	# <function func at 0x0000023B61BE90D8>

函数名可以赋值给其他变量

def func():
    print('函数名的内存地址')
    print(func) # 每次执行func(),内存地址都会发生改变 <function func at 0x000001B69FDA90D8> 

a = func        # 把函数当成一个值赋值给另一个变量
print(a)        # 变量a的内存地址始终指向func的内存地址 <function func at 0x000001B69FDA90D8>
a()             # 函数调用 func()

函数名可以当作容器类的元素

def func1():
    print(1111)
def func2():
    print(2222)
def func3():
    print(3333)
    
func_list = [func1,func2,func3]
for func in func_list:
    func()
# 运行结果
# 1111
# 2222
# 3333

函数名可以当作函数的参数

def func1():
    print('I am func1...')

def func2(fn):
    print('I am func2...')
    fn()		# 执行传递过来的fn
    print('I am func2...')

func2(func1)	# 把func1当成参数传递给func2的参数fn
# 运行结果
# I am func2...
# I am func1...
# I am func2...

函数名可以作为函数的返回值

def func1():
    print('I am func1...')
    def func2():
        print('I am func2...')
    print('I am func1...')
    return func2

ret = func1()
print('*********')
ret()

运行结果
I am func1...
I am func1...
*********
I am func2...

2.闭包

  什么是闭包?闭包就是内层函数,对外层函数(非全局)的变量的引用,叫闭包。通俗一点来说,如果在一个函数内部,嵌套了函数,这个内部函数对(非全局作用域)外部作用域的变量进行了引用,那么这个内部函数称为闭包。闭包每次运行是能记住引用的外部作用域的变量的值。

  在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部函数的变量,就有可能产生闭包。运行时,一旦外部的函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。

闭包必须满足三个条件:

(1)必须有一个内嵌函数
(2)内嵌函数必须引用外部嵌套函数中的变量(非全局的外部函数中的变量)
(3)外部函数的返回值必须是内嵌函数名

闭包作用: 可以用来编写惰性求值的代码,可以在函数调用时保持特定状态。

检测闭包: 可以使用__closure__来检测函数是否是闭包。使用函数名.__closure__,返回cell就是闭包返回None就不是闭包

def outer():
    name = 'Lucy'
    def inner():    # 内部函数
        print(name)
    return inner

fn = outer()        # 访问外部函数,获取到内部函数的内存地址
fn()                # 访问内部函数
  • 这里将内部函数的函数名当作参数返回给外部函数;
  • 获取内部函数的内存地址就可执行内部函数;
def func1():
    def func2():
        name = 'Lucy'
        def func3():
            print(name)
        return func3
    return func2

fn2 = func1()   # 获取到的是 func2
fn3 = fn2()     # 获取到的是 func3
fn3()           # 执行结果 Lucy
print(func1.__closure__)    # 执行结果 None
print(fn2.__closure__)      # 执行结果 None
print(fn3.__closure__)      # 执行结果 (<cell at 0x000001897ADF0798: str object at 0x000001897ADBB770>,)
# 只有函数func3是闭包函数
  • 多层函数的嵌套,只需一层一层的往外层返回就行了;

  python正常情况下,如果一个函数执行完毕,则这个函数中的变量以及局部命名空间中的内容都将会被销毁。在闭包中,如果变量被销毁了,那内部函数将不能正常执行。所以,python规定,如果在内部函数中访问了外层函数中的变量,那么这个变量将不会消亡,将会常驻在内存中。也就是说,使用闭包,可以保证外层函数中的变量在内存中常驻。这样做的好处,也是闭包的一个作用就是让一个变量能够常驻内存,供后面的程序使用。

3.迭代器

可迭代协议:对象必须提供一个__next__方法,执行该方法要么返回迭代中的下一项,要么引起一个StopIteration异常,终止迭代(只能往后走不能往前退)。

可迭代对象:实现了迭代器协议的对象(如何实现:对象内部定义一个__iter__方法)。str、list、tuple、dict、set都是Iterable(可迭代对象),但不是iterator(迭代器对象)。可以通过内建函数__iter__方法,把这些可迭代对象都变成迭代器对象。

for item in Iterable:循环的本质就是先通过__iter__方法获取可迭代对象的迭代器,然后对获取到的迭代器不断调用__next__方法来获取下一个值并将其赋值给item,当遇到StopIteration异常后循环结束。

# dir查看对象的方法和函数,如果在打印结果中包含'__iter__'就表示当前的这个类型是可迭代对象
s = '123'
print(dir(s))           # 包含'__iter__'
print(s.__iter__())     # <str_iterator object at 0x00000140D8747D60>

lst = [11,22,33]
print(dir(lst))         # 包含'__iter__'
print(lst.__iter__())   # <list_iterator object at 0x000001A209E47BB0>

tpl = (11,22,33)
print(dir(tpl))         # 包含'__iter__'
print(tpl.__iter__())   # <tuple_iterator object at 0x00000287FE1A7BB0>

dic = {11:'111',22:'222',33:'333'}
print(dir(dic))         # 包含'__iter__'
print(dic.__iter__())   # <dict_keyiterator object at 0x000002087F49CB30>

se = {11,22,33}
print(dir(se))          # 包含'__iter__'
print(se.__iter__())    # <set_iterator object at 0x00000274A9B619C0>

print(dir(range))           # 包含'__iter__'
print(range(3).__iter__())  # range expected 1 argument;<range_iterator object at 0x00000202098DAB90>
lst = [11,22,33]
print(dir(lst))                      # 包含'__iter__'
list_iterator_obj = lst.__iter__()   # 可迭代对象
print(list_iterator_obj)             # <list_iterator object at 0x000001A209E47BB0>
print(dir(list_iterator_obj))        # 包含'__iter__','__next__'
print(list_iterator_obj.__next__())  # 11 使用迭代器进行迭代,获取第一个元素
print(list_iterator_obj.__next__())  # 22
print(list_iterator_obj.__next__())  # 33
print(list_iterator_obj.__next__())  # StopIteration

实际执行情况如下图:

  • 调用可迭代对象的__iter__方法返回一个迭代器对象
  • 不断调用迭代器的__next__方法返回元素
  • 直到迭代完成后,处理StopIteration异常

使用while和迭代器模拟for循环:

lst = [11,22,33]
list_iterator_obj = lst.__iter__()
while True:
    try:
        i = list_iterator_obj.__next__()
        print(i)
    except StopIteration:
        break

总结

  • iterable:可迭代对象,内部包含__iter__()
  • iterator:迭代器对象,内部包含__Iter__(),同时包含__next__()
  • 迭代器一定是可迭代对象,但可迭代对象不一定是迭代器
  • 迭代器:迭代取值的工具
  • 迭代器的作用是提供一个不依赖索引取值的方式

迭代器的特点:

  • 节省内存
  • 惰性机制
  • 不能反复,只能往后依次取,不能从后往前取

迭代器的优点:

  • 不依赖索引取值
  • 内存中永远只占一份空间,不会导致内存溢出

迭代器的缺点:

  • 不能获取指定的元素
  • 迭代器中的元素取完一个元素后再取另一个,会报StopIteration异常

三、生成器

生成器的本质是迭代器。在python中有以下两种方式获取生成器:

(1)通过生成器函数;常规函数定义,使用def定义函数,但使用的是yield语句而不是return语句返回结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行。

(2)通过各种推导式来实现生成器;类似于列表推导,只不过是把一对中括号[]变换成一对小括号()。但是,生成器表达式是按需产生一个生成器结果对象,要想拿到每一个元素,就需要循环遍历。

1.定义生成器

只需要修改一个地方就可以把函数变成生成器,就是将函数中的return换成yield就是生成器。

def func():
    print(111)
    yield 222
generator_obj = func()
print(generator_obj)  # <generator object func at 0x000002A1BF5B3CF0>

函数名加括号获取到的是什么?函数名加括号func()为什么没有调用函数,函数未执行?

(1)正常情况下,函数名加括号确实是调用函数并执行,上述代码只是因为函数体中出现了yield;可以理解为,生成器是基于函数的形式变成的;
(2)func() 这一步是在 创建一个生成器,然后赋值到变量generator_obj中,再进行使用;

def func():
    print(111)
    yield 222
    print(333)
    yield 444

generator_obj = func()
print(generator_obj)            # <generator object func at 0x000002A1BF5B3CF0>
print(dir(generator_obj))       # 包含 '__iter__','__next__'
func_generator_obj = generator_obj.__iter__()
print(func_generator_obj)       # <generator object func at 0x000002A1BF5B3CF0>
print(generator_obj.__next__())
print(generator_obj.__next__())
print(generator_obj.__next__())

输出结果:
111
222
333
444
StopIteration

获取到的生成器,如何使用呢?生成器的本质就是迭代器,是不是可以直接使用迭代器的但是直接使用生成器?
(1)generator_obj = func() 这一步函数不会执行,而是获取到生成器;
(2)generator_obj.__next__() 这一步函数才会执行,yield的作用和return一样,也是返回数据;
(3)当程序运行完成最后一个yield,那么后面继续运行__next__(),程序会报错;

yield和return的区别:

(1)return: 在程序函数中返回某个值,返回之后函数不再继续执行,彻底结束;是直接停止这个函数,return可以出现多次,

(2)yield: 带有yield的函数是一个迭代器,函数返回某个值时,会停留在某个位置,返回函数值后,会在前面停留的位置继续执行,直到程序结束;是分段来执行一个函数,yield可以出现多次;

生成器的作用

一个例子,获取1-100之间的数字

  • 方式一:
def sort_number(n):
    lst = []
    for i in range(1,n):
        lst.append(i)
    return lst

ret = sort_number(100)
print(ret)              # [1,2,...,98,99]
  • 方式二:
def sort_number(n):
    for i in range(1,n):
        yield i

gener = sort_number(100)
print(gener.__next__())     # 1
print(gener.__next__())     # 2
print(gener.__next__())     # 3
print(gener.__next__())     # 4
...

两种方式的区别:

  • 方式一是直接把1-100之间的数字一次全部都拿来,很占内存
  • 方式二使用生成器,一次拿一个数字,一直向下进行,不能向上; __next__()到哪儿指针就指到哪里,下一次继续就获取指针指向的值;
  • 对比显示:生成器的好处就是节省内存

send()

send() 和 __next__()一样都可以让生成器执行到下一个yield;send()方法可以往生成器里面的变量传值。

def eat():
    for i in range(1,10):
        a = yield 'apple' + str(i)
        print('a is', a)
        b = yield 'banana'
        print('b is', b)

e = eat()
print(e.__next__())	# apple1
print(e.__next__())

输出结果
apple1
a is None
banana

代码执行顺序:

  • 程序开始执行之后,因为函数eat()中由yield关键字,所以函数eat()并不会真的执行,而是先得到一个生成器对象e;
  • 直到调用next方法,eat()函数正式开始执行,进入for循环;
  • 程序遇到yield关键字,返回apple1之后,程序停止,此时并未执行赋值给变量a操作;
  • 再次执行next()方法,这个时候是从刚才那个next程序停止的地方开始的,也就是要执行a的赋值操作,此时应注意,这个赋值操作的右边是没有值的(因为刚才那个apple1是返回出去了,并没有给赋值操作的左边传参数,所以此时a赋值是None),输出结果是a is None;
  • 程序会继续在for循环里面继续向下执行,又一次碰到yield,这个时候同样返回banana,然后程序停止。
def eat():
    for i in range(1,10):
        a = yield 'apple' + str(i)
        print('a is', a)
        b = yield 'banana'
        print('b is', b)

e = eat()
print(e.__next__())	
print(e.send('grape'))
print(e.send('orange'))

输出结果
apple1
a is grape
banana
b is orange
apple2

代码执行顺序:

  • 先获取生成器对象e,执行next()方法,函数eat()开始执行,进入for循环,遇到yield返回apple1,未执行赋值操作,函数停止;
  • 执行第一个send()方法,将grape赋值给a,输出结果a is grape,让生成器向下走一次,即走到第二个yield,返回值banana;
  • 执行第二个send()方法,将orange赋值给b,输出结果b is orange,让生成器向下走一次,即走到第一个yield(for循环),返回值apple2;

send()方法可以给上一个yield的位置传递值,并可以让生成器向下走一次(走到下一个yield位置);在第一次执行生成器的时候不能直接使用send(),但是可以使用send(None)。

yield from

在python3中提供了一种可以直接把可迭代对象中的每一个数据作为生成器的结果进行返回

def func():
    lst = [11,22,33]
    yield from lst

gener = func()
for i in gener:
    print(i)

输出结果
11
22
33

yield form 小坑

yield from 是将列表中的每一个元素返回,所以如果写两个yield form并不会产生交替的效果

def func():
    lst = [11,22]
    lst2 = [111,222]
    yield from lst
    yield from lst2

gener = func()
for i in gener:
    print(i)
输出结果
11
22
111
222

2.推导式

列表推导式

常用写法:[ 结果 for 变量 in 可迭代对象 ]

列表推导式是通过一行来构建想要的列表,列表推导式看起来代码很简单,但出现错误之后很难排查。

lst = [i for i in range(10)]
print(lst)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

lst2 = ['python %s' % i for i in range(1,10)]
print(lst2) # ['python 1', 'python 2', 'python 3', 'python 4', 'python 5', 'python 6', 'python 7', 'python 8', 'python 9']

筛选模式

常用写法:[ 结果 for 变量 in 可迭代对象 if 条件 ]

lst = [i for i in range(10) if i % 2 ==0]
print(lst)  # [0, 2, 4, 6, 8]

生成器推导式

常用写法:( 结果 for 变量 in 可迭代对象 )

  • 生成器表达式和列表推导式的语法基本上是一样的,只是把 中括号[] 换成 小括号()
  • gener打印的结果就是一个生成器,可以使用for循环来循环这个生成器;
  • 生成器表达式也可以进行筛选
gener = ('第%s次' % i for i in range(1,10) if i % 3 == 0)
print(gener)    # <generator object <genexpr> at 0x00000156F4853CF0>
for j in gener:
    print(j)

输出结果
第3次
第6次
第9

生成器表达式和列表推导式的区别:

(1)列表推导式比较耗内存,一次性加载;生成器表达式几乎不占内存,使用的时候才进行分配和使用内存;
(2)得到的值不一样,列表推导式得到的是一个列表,生成器表达式得到的是一个生成器;

生成器的惰性机制: 生成器只有在访问的时候才取值。说白了,就是你找它要才给你值,不找它要,它是不会执行的。

字典推导式

根据名字应该可猜到,推导出来的是字典。

lst1 = [1,2,3]
lst2 = ["11","22","33"]
dic = {lst1[i]:lst2[i] for i in range(len(lst2))}
print(dic)  # {1: '11', 2: '22', 3: '33'}

集合推导式

集合推导式可以直接生成一个集合,集合的特点:无序,不重复,所以集合推导式自带去重功能。

lst = [1,2,3,-1,-3,-7,9]
s = {abs(i) for i in lst}
print(s)    # {1, 2, 3, 7, 9}

总结

(1)推导式:列表推导式字典推导式集合推导式,没有元组推导式;
(2)生成器表达式:( 结果 for 变量 in 可迭代对象 if 条件筛选 )
(3)生成器表达式可以直接获取到生成器对象,生成器对象可直接进行for循环,生成器具有惰性机制
(4)区分字典推导式和集合推导式小技巧:字典推导式前面的结果有个冒号,而集合的前面结果就是单纯的结果。

四、装饰器

python装饰器本质上就是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象(函数的指针)。 装饰器函数的外部函数传入要装饰的函数名字,返回经过装饰后函数的名字;内层函数(闭包)负责修饰被装饰函数。

实质:是一个函数
参数:是你要装饰的函数名(并非函数调用)
返回:是装饰完的函数名(也非函数调用)
作用:为已经存在的对象添加额外的功能
特点:不需要对对象做任何代码上的变动

装饰器经典应用场景:比如插入日志、性能测试、事务处理、权限校验等。装饰器最大的作用就是对于我们已经写好的程序,可以抽离出一些雷同的代码组建多个特定功能的装饰器,这样我们就可以针对不同的需求去使用特定的装饰器。

1.函数装饰器

函数的函数装饰器
在这里插入图片描述
上述代码中,fn是要装饰的函数,想用装饰器decorator显示函数fn运行的时间。@decorator 等价于fn = decorator(fn),为函数装饰并返回。装饰器函数decorator,该函数的参数是func(被装饰函数),返回参数是内层函数;这里的内层函数wrapper相当于闭包,它起到装饰给定函数的作用,wrapper参数为*args,**kwargs,*args表示的参数以列表的形式传入,**kwargs表示的参数以字典的形式传入。

为了不破坏原函数的逻辑,我们要保证内层函数wrapper和被装饰函数func的传入参数和返回值类型必须保持一致。

类方法的函数装饰器
在这里插入图片描述
对于类方法来说,都会有一个默认的参数self,它实际表示的是类的一个实例,所以在装饰器的内部函数wrapper也要传入一个参数instance就表示将类的实例instance_1传给wrapper,其他的用法和函数装饰器相同。

2.类装饰器

在python中,一切皆对象,因此类也可以发挥装饰器的作用。

class Decorator(object):
    def __init__(self,f):
        self.f = f
    def __call__(self):
        print("decorator start")
        self.f()
        print("decorator end")

@Decorator			# fn = Decorator(fn)
def fn():
    print("fn.....")

fn()
# decorator start
# fn.....
# decorator end

注意:__call__()是一个特殊方法,它可以将一个类实例变成一个可调用对象。

p = Decorator(fn)  # p是类Decorator的一个实例
p() # 实现了__call__()方法后,p可以被调用

在未加@wraps(func)时,fn.__name__执行的结果不是fn,而是inner,这表示被装饰函数自身的信息丢失了,避免这种问题的发生,可以借助functools.wraps()函数。

from functools import wraps

def decorator(func):
    @wraps(func)
    def inner(*args,**kwargs):
        pass
    return inner

@decorator
def fn():
    time.sleep(3)

fn()
print(fn.__name__)  # inner 不加  @wraps(func)
print(fn.__name__)  # fn   加上 @wraps(func)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值