
迭代器、生成器、装饰器
更新: 2025/2/24 字数: 0 字 时长: 0 分钟
迭代器(Iterator
)
在 Python 中用于遍历可迭代对象的对象,名称叫“迭代器(Iterator
)”。
可迭代对象
在学习迭代器之前,回顾一下前面我们学习的可迭代对象(Iterable
)。在 Python 中,许多内置的数据类型如字符(str
)、列表(list
)、字典(dict
)、集合(set
)、元祖(tuple
)等都是可迭代对象,当时就简单介绍了可迭代对象是指实现了迭代协议的对象。其实,这里的**“迭代协议”指的就是类中实现的 __iter__
魔术方法**。有小伙伴可能不理解,没关系,先看下面的代码:我们创建了一个自定义类,并生成了一个对象,经过类型判断,该对象不是一个可迭代对象。
# 从collections.abc库导入Iterable可迭代对象(若Python版本较低,则引入collections库)
from collections.abc import Iterable
class MyObject:
def __init__(self, *data):
self.data = data
obj = MyObject(1, 2, 3) # 注释:生成一个对象。
print(isinstance(obj, Iterable)) # 输出:False。注释:对象不是可迭代对象。
现在我们在类中定义一个名称为 __iter__
返回为 None
的魔术方法,代码如下:在自定义类中定义了 __iter__
方法后,通过该类生成的对象,就是一个可迭代对象了。既然是可迭代对象,也就说明该对象可以被 for
循环。在 for
循环中,首先输出了 execute iter
,说明 for
循环调用了对象的 __iter__
方法,然后抛出了类型错误,说明 __iter__
方法返回的内容不是 for
循环所期望的。
# 从collections.abc库导入Iterable可迭代对象(若Python版本较低,则引入collections库)
from collections.abc import Iterable
class MyObject:
def __init__(self, *data):
self.data = data
def __iter__(self):
print("execute iter", end=', ')
return # 注释:返回None。
obj = MyObject(1, 2, 3)
print(isinstance(obj, Iterable)) # 输出:True。注释:obj对象是可迭代对象。
for item in obj: # 输出:execute iter。注释:说明这里for循环调用了obj对象的__iter__()方法。
print(item, end=', ') # 报错:返回了NoneType类型的非迭代器。
获取迭代器
那么 for
循环期望 __iter__
方法返回什么内容呢?答案就是可迭代对象的迭代器,因为只有拿到迭代器才能进行遍历。那么如何获取可迭代对象的迭代器呢?很简单,通过内置函数 iter(可迭代对象)
,我们就可以拿到可迭代对象中的迭代器了,看下面的代码:
# 从collections.abc库导入Iterable可迭代对象、Iterator迭代器(若Python版本较低,则引入collections库)
from collections.abc import Iterable, Iterator
obj = ['a', 'b', 'c'] # 注释:定义一个列表属于可迭代对象。
print(isinstance(obj, Iterable)) # 输出:True。注释:对象obj是可迭代对象。
print(isinstance(obj, Iterator)) # 输出:False。注释:对象obj不是迭代器。
tor = iter(obj) # 注释:通过内置函数iter()获取可迭代对象的迭代器。
print(tor) # 输出:<list_iterator object at 0x0000022FA7D07FD0>
print(isinstance(tor, Iterable)) # 输出:True。注释:对象tor是可迭代对象。
print(isinstance(tor, Iterator)) # 输出:True。注释:对象tor是迭代器。
建议
使用 iter(可迭代对象)
和 可迭代对象.__iter__()
都能获取可迭代对象的迭代器,两种写法可以说是等价的。但通常不会直接调用 可迭代对象.__iter__()
方法来获取迭代器,因为从写法上来讲,使用 iter()
函数来获取迭代器会更加的 Pythonic。
重要
这里有一点需要记住,就是迭代器一定是可迭代对象,但可迭代对象不一定就是迭代器。
明白了内置函数 iter()
的用法,回到前面的代码例子:在 __iter__
方法中,通过内置的 iter()
方法返回了可迭代对象中 data
部分的迭代器。for
循环调用 __iter__
方法拿到了该迭代器后,成功遍历了可迭代对象中 data
部分的元素。
# 从collections.abc库导入Iterable可迭代对象、Iterator迭代器(若Python版本较低,则引入collections库)
from collections.abc import Iterable, Iterator
class MyObject:
def __init__(self, *data):
self.data = data
def __iter__(self):
print("execute iter", end=', ')
return iter(self.data) # 注释:返回了可迭代对象中data部分的迭代器。
obj = MyObject(1, 2, 3)
print(isinstance(obj, Iterable)) # 输出:True。注释:obj对象是可迭代对象。
print(isinstance(obj, Iterator)) # 输出:False。注释:obj对象不是迭代器。
for item in obj: # 输出:execute iter
print(item, end=', ') # 输出:1, 2, 3, 。注释:这里for循环成功遍历了可迭代对象中的元素。
建议
有小伙伴可能会问,为什么 __iter__
方法返回的是 iter(self.data)
可迭代对象中 data
部分的迭代器,而不是返回 iter(self)
可迭代对象整体的迭代器?这是因为 iter(self)
等价于 self.__iter__()
,如果在 __iter__
方法返回 iter(self)
就相当于在回调自身,最后会抛出一个超出最大递归深度的 RecursionError
异常。
实现迭代器
通过上面的代码案例,我们可以总结得到,for
循环先是通过可迭代对象的 __iter__
方法拿到迭代器,再通过迭代器来遍历可迭代对象中的元素。那么我们有没有可能实现迭代器逻辑呢?当然可以,看下面代码:我们在自定义类中新定义一个名称为 __next__
的魔术方法,再通过自定义类生成对象,可以看到,该对象就不仅仅是一个可迭代对象了,它还是一个迭代器。
# 从collections.abc库导入Iterable可迭代对象、Iterator迭代器(若Python版本较低,则引入collections库)
from collections.abc import Iterable, Iterator
class MyObject:
def __init__(self, *data):
self.data = data
def __iter__(self):
print("execute iter", end=', ')
return iter(self.data)
def __next__(self):
print("execute next", end=', ')
obj = MyObject(1, 2, 3)
print(isinstance(obj, Iterable)) # 输出:True。注释:obj对象是可迭代对象。
print(isinstance(obj, Iterator)) # 输出:True。注释:obj对象是迭代器。
重要
这里有一点需要记住的就是,当类中定义了 __iter__
方法和 __next__
方法后,这个类所返回的对象就不仅仅是一个可迭代对象了,它还是一个迭代器。
既然生成的对象是迭代器,那么在 __iter__
方法中,我们就可以直接 return self
返回对象本身了,因为上面提到 for
循环期望 __iter__
方法返回迭代器,代码如下:在 __iter__
方法中,我们直接 return self
返回对象本身。使用 for
循环遍历对象的时候,首先把对象当作可迭代对象,调用了对象的 __iter__
方法,输出了 execute iter
,返回了对象本身。然后 for
循环把返回的对象当作迭代器,调用了对象的 __next__
方法,输出了 execute next
,返回了字符串 ABC
,而这个返回值就是输出 item
变量的值。后续就是,一直重复不断的输出 execute next
和 ABC
。
class MyObject:
def __init__(self, *data):
self.data = data
def __iter__(self):
print("execute iter", end=', ')
return self
def __next__(self):
print("execute next", end=', ')
return "ABC"
obj = MyObject(1, 2, 3)
for item in obj: # 输出:execute iter, execute next, execute next, execute next...
print(item) # 输出:ABC, ABC, ABC...
上面的例子中有了两个地方没有达到我们想要的效果,第一个就是没有输出可迭代对象中的每个元素,第二个就是程序没有停止。针对这两点,我们进行如下修改:增加 index
属性用来标记遍历的位置,增加 StopIteration
异常用来退出遍历。
class MyObject:
def __init__(self, *data):
self.data = data
self.index = 0 # 注释:新增index属性用来标记遍历的位置。
def __iter__(self):
print("Creating iterator")
return self
def __next__(self):
if self.index < len(self.data):
result = self.data[self.index]
self.index += 1
return result
else:
raise StopIteration # 注释:新增StopIteration异常用来退出遍历。
obj = MyObject(1, 2, 3)
for i in obj: # 输出:Creating iterator
print(i, end=', ') # 输出:1,2,3
到这里我们就实现了一个完整的迭代器了,从而也完整的看清了 for
循环整个的工作流程:当我们使用 for
循环遍历一个可迭代对象时, for
循环会在遍历开始前调用该对象的 __iter__
方法来拿到可迭代对象的迭代器,然后循环调用这个迭代器的 __next__
方法来逐一遍历并返回可迭代对象中的元素,直到所有元素都被访问过,最后触发 StopIteration
异常终止迭代。不过这个触发是一个“隐式的触发”,也就是说 for
循环不会抛出 StopIteration
异常,而是在 for
循环的内部被捕获并处理的。
建议
虽然我们可能很少直接构建迭代器对象,但在 Python 中,迭代器已经被广泛应用于各种场景中,例如在文件处理、数据流处理、惰性计算等方面。可以这样说,迭代器最大的作用就是统一了所有可迭代数据类型的遍历工作。
遍历不同性
上面讲了迭代器也属于可迭代对象,也就意味着迭代器也可以进行循环遍历,不过在遍历过程中,迭代器和可迭代对象还是有差别的,具体如下:
- 遍历次数:可迭代对象每次被遍历时,都会生成新的迭代器用来遍历,因此可以被不限次数的遍历。迭代器被遍历时,由于每个元素只能被遍历一次,因此一个迭代器也只能遍历一次,除非重新获取一个新的迭代器。
# 可迭代对象
obj = [1, 2, 3] # 注释:定义一个列表属于可迭代对象。
for item in obj: # 注释:遍历可迭代对象。
print(item, end=', ') # 输出:1, 2, 3,
print(obj) # 输出:[1, 2, 3]。注释:可迭代对象遍历一次后,对象不会发生任何改变。
# 迭代器
tor = iter(obj) # 注释:获取可迭代对象的迭代器。
for item in tor: # 注释:遍历迭代器。
print(item, end=', ') # 输出:1, 2, 3,
print(list(tor)) # 输出:[]。注释:迭代器经历一次遍历后,所有的元素都被迭代完了,因此通过list转换得到的是空列表。
- 函数遍历:可迭代对象使用
list()
、dict()
、set()
、tuple()
、sum()
、max()
、min()
等函数时,会生成新的迭代器用来遍历,因此可以不限次数的使用函数。迭代器使用这些函数时,会一次性遍历完迭代器中未被遍历的所有元素,因此只能使用一次函数。
# 可迭代对象
obj = [1, 2, 3] # 注释:定义一个列表属于可迭代对象。
for item in obj: # 注释:遍历可迭代对象。
print(item, end=', ') # 输出:1, 2, 3,
print(list(obj), end=', ') # 输出:[1, 2, 3], [1, 2, 3], [1, 2, 3]。注释:可迭代对象可以不限次数的使用函数,所有能有三次循环。
# 迭代器
tor = iter(obj) # 注释:获取可迭代对象的迭代器。
for item in tor: # 注释:遍历迭代器。
print(item, end=', ') # 输出:1。注释:迭代器中元素1已被消耗。
print(list(tor), end=', ') # 输出:[2, 3]。注释:list()函数将迭代器中未遍历的元素全部遍历了,所以只有一次循环。
- 迭代方法:可迭代对象可以使用
for
循环依次返回对象中的元素,而迭代器可以使用next(迭代器)
或for
循环依次返回迭代器中的元素,但是当迭代器中的元素被消耗完时,再使用next(迭代器)
会抛出StopIteration
异常,因此建议使用for
循环来依次返回迭代器中的元素。
# 可迭代对象(for循环)
obj = [1, 2, 3] # 注释:定义一个列表属于可迭代对象
for item in obj: # 注释:遍历可迭代对象
print(item, end=', ') # 输出:1, 2, 3,
# 迭代器(next方法)
tor = iter(obj) # 注释:获取可迭代对象的迭代器。
print(next(tor)) # 输出:1
print(next(tor)) # 输出:2
print(next(tor)) # 输出:3
print(next(tor)) # 报错:空迭代器无法返回下一个元素,抛出StopIteration异常。
# 迭代器(for循环,建议)
tor = iter(obj) # 注释:获取可迭代对象的迭代器。
for item in tor: # 注释:遍历迭代器
print(item, end=', ') # 输出:1, 2, 3,
建议
使用 next(迭代器)
和 迭代器.__next__()
都能依次返回迭代器中的元素,两种写法可以说是等价的。但通常不会直接调用 迭代器.__next__()
方法来返回迭代器中的元素,因为从写法上来讲,使用 next()
函数来返回迭代器中的元素会更加的 Pythonic。
重要
迭代器是惰性计算机制,也就是说,只要不调用 迭代器.__next__()
方法,它就不会给你返回下一个元素。
内置过滤器
filter
函数是 Python 内置的过滤函数,用于过滤序列中不符合条件的元素,返回由符合条件元素组成的迭代器。使用格式如下:
filter(function or None, iterable) --> filter object
function
函数用来对可迭代对象的每个元素进行判断并返回布尔值,如果返回的布尔值为True
,则保留该元素,否则过滤掉。None
表示不调用任何函数,只对可迭代对象中的每个元素本身进行判断,如果元素的布尔值为True
,则保留该元素,否则过滤掉。iterable
参数表示参与计算的可迭代对象。filter object
返回一个由符合条件元素组成的迭代器,这个迭代器可以通过for
循环或者list()
函数转换成列表来使用。
from collections.abc import Iterator
# 可迭代对象
it = [0, 1, 2, 3, 4, 5]
# is_odd函数
def is_odd(i):
return i % 2 == 0
# 参数为is_odd函数
obj = filter(is_odd, it)
print(isinstance(obj, Iterator)) # 输出:True。注释:返回一个迭代器。
print(list(obj)) # 输出:[0, 2, 4]。注释:过滤器过滤。
# 参数为lambda匿名函数
obj = filter(lambda i: i % 2 == 0 and i > 3, it)
print(isinstance(obj, Iterator)) # 输出:True。注释:返回一个迭代器。
print(list(obj)) # 输出:[4]。注释:将过滤函数转化为lambda匿名函数。
# 参数为None空
obj = filter(None, it)
print(isinstance(obj, Iterator)) # 输出:True。注释:返回一个迭代器。
print(list(obj)) # 输出:[1, 2, 3, 4, 5]。注释:可迭代对象中只有0的布尔值为False,所以对0进行了过滤。
生成器(Generator
)
上面我们介绍了迭代器(Iterator
),这里再介绍一个在 Python 中能边循环边计算的对象,名称叫“生成器(Generator
)”。
关系区别
生成器(Generator
)和迭代器(Iterator
)之间既有关联又有区别,具体如下:
- 包含关系:生成器是一种特殊的迭代器,也就是说迭代器包含生成器,因此定义范围可以归纳为,可迭代对象(
Iterable
) > 迭代器(Iterator
) > 生成器(Generator
)。 - 惰性计算:生成器(
Generator
)和迭代器(Iterator
)在 Python 中都用于惰性计算,也就是说它们是按需生成元素的,而不是一次性将所有元素存储在内存中,所以它们占用的内存空间极小。 - 定义方式:迭代器通常是通过类中定义
__iter__()
和__next__()
魔术方法来实现的,其中__iter__()
方法返回迭代器对象自身,而__next__()
方法用于生成下一个元素。生成器语法则更加简洁,通过使用yield
来实现状态的保存和生成元素。生成器函数在每次调用时会暂停并保存当前状态,然后在下一次调用时从暂停的位置恢复执行。 - 本质区别:迭代器(
Iterator
)是一个遍历对象数据的机器。生成器(Generator
)是一个生成数据的机器。 - 使用场景:迭代器通常用于需要遍历数据的场景,例如文件处理、数据流处理等。生成器通常用于需要生成数据的场景,例如生成器表达式。
总的来说,生成器是一种特殊的迭代器,它提供了一种更简洁、更高效的方式来实现惰性计算。在大多数情况下,生成器是首选的实现方式,因为它们更易于理解、更简洁、更高效。
生成器表达式
在讲生成器表达式之前,我们先回顾一下前面学习的推导式,代码如下:推导式外层使用方括号 []
或花括号 {}
,然后根据给定的规则直接生成列表、字典或集合。
# 列表推导式
list_der = [i for i in range(3)]
print(list_der) # 输出:[0, 1, 2]
# 字典推导式
dict_der = {i: i for i in range(3)}
print(dict_der) # 输出:{0: 0, 1: 1, 2: 2}
# 集合推导式
set_der = {i for i in range(3)}
print(set_der) # 输出:{0, 1, 2}
现在回过头来写生成器表达式就很简单了,只需要将推导式外层使用的方括号 []
或花括号 {}
变成圆括号 ()
就是一个生成器表达式,代码如下:
# 从collections.abc库导入Generator生成器(若Python版本较低,则引入collections库)
from collections.abc import Generator
ex = (i for i in range(3)) # 注释:生成器表达式外层使用圆括号。
print(ex, isinstance(ex, Generator)) # 输出:<generator object...>, True。注释:生成器表达式的本质就是一个生成器。
在上面我们提到生成器本身就是一种特殊的迭代器,所以生成器也具备迭代器相似的特点,具体如下:
- 生成器中每个元素只能生成一次,而且只能单向的、从前往后的顺序进行生成。
ex = (i for i in range(3)) # 注释:定义一个生成器。
for item in ex: # 注释:遍历生成器。
print(item, end=', ') # 输出:0, 1, 2,
print(list(ex)) # 输出:[]。注释:生成器经历一次迭代后,所有的元素都已生成,因此list()函数转换得到的是空列表。
- 生成器使用
list()
、dict()
、set()
、tuple()
、sum()
、max()
、min()
等函数时,会将未生成的元素一次性全部生成。
ex = (i for i in range(3)) # 注释:定义一个生成器。
for item in ex: # 注释:遍历生成器。
print(item, end=', ') # 输出:0。注释:生成0元素。
print(list(ex)) # 输出:[1, 2]。注释:list()函数消耗了生成器中未生成的所有元素,因此只有一次循环。
- 生成器也能通过调用
for
循环、next()
方法、__next__()
方法来生成数据。在没有数据能够生成的情况下,继续调用next()
方法、__next__()
方法,同样也会抛出StopIteration
异常。
ex = (i for i in range(3))
print(ex.__next__()) # 输出:0
print(next(ex)) # 输出:1
print(ex.__next__()) # 输出:2
print(next(ex)) # 报错:没有数据生成时继续调用next方法,会抛出StopIteration异常。
这里我们就可以利用生成器表达式结合内置函数,来达到推导式的效果,代码写法如下:
# 写法一:list列表转化
ex = (i for i in range(3))
list_ex = list(ex)
print(list_ex) # 输出:[0, 1, 2]
# 写法二:省略ex变量
list_ex = list((i for i in range(3)))
print(list_ex) # 输出:[0, 1, 2]
# 写法三:Python为了更好的封装,可以省略最外层括号
list_ex = list(i for i in range(3))
print(list_ex) # 输出:[0, 1, 2]
现在,我们就可以将上面推导式的例子全部用生成器表达式重写,而且还能写出元组的生成器表达式,代码如下:
# list列表函数结合生成器表达式
list_ex = list(i for i in range(3))
print(list_ex) # 输出:[0, 1, 2]
# dict字典函数结合生成器表达式
dict_ex = dict((i, i) for i in range(3))
print(dict_ex) # 输出:{0: 0, 1: 1, 2: 2}
# set集合函数结合生成器表达式
set_ex = set(i for i in range(3))
print(set_ex) # 输出:{0, 1, 2}
# tuple元组函数结合生成器表达式
tuple_ex = tuple(i for i in range(3))
print(tuple_ex) # 输出:(0, 1, 2)
有小伙伴可能会说,推导式重写为生成器表达式后,输出的结果一样,重写的意义在哪呢?我们来看看下面这一个求 0 ~ 100000000
数值之和例子:使用列表推导式,虽然很快得出了答案,但在结果计算期间内,内存占用飙升。使用生成器表达式,虽然耗费的时间更长,但在结果计算期间内,内存占用几乎不变。可以说,生成器最大的优势就是节约内存。
# 方法一:sum(列表推导式)
print(sum([i for i in range(10**7)])) # 输出:4999999950000000。注释:内存占用飙升,计算用时12.47秒。
# 方法二:sum(生成器表达式)
print(sum(i for i in range(10**7))) # 输出:4999999950000000。注释:内存占用几乎不变,计算用时65.53秒。
总的来说,推导式(无论是列表、字典还是集合推导式)和生成器表达式都是 Python 中非常强大的工具,它们允许你以简洁、易读的方式生成复杂的数据结构。然而,它们之间的关键区别在于,推导式会立即生成整个数据结构,而生成器表达式是返回一个按需生成元素的生成器对象,两者所带来的直接影响就是内存占用率的高低。
生成器函数
除了通过生成器表达式来创建一个生成器以外,我们还可以使用生成器函数来创建一个生成器。首先,我们定义一个普通的函数,然后输出它,代码如下:
def func():
return 123
res = func()
print(res) # 输出:123
现在我们将函数中的关键字 return
替换为关键字 yield
进行输出,代码如下:在普通函数执行路径的任何位置声明了关键字 yield
后,普通函数就会变为一个生成器函数,并且执行生成器函数返回的是一个生成器对象。
# 从collections.abc库导入Generator生成器(若Python版本较低,则引入collections库)
from collections.abc import Generator
# func函数中有关键字yield,因此func是生成器函数。
def func():
yield 123
res = func() # 注释:执行func生成器函数将返回对象赋值给res变量。
print(res) # 输出:<generator object func at 0x000001C920ABAD60>
print(isinstance(res, Generator)) # 输出:True。注释:func生成器函数返回的是一个生成器对象。
生成器表达式中的生成器和生成器函数返回的生成器一样,都可以通过调用 for
循环、next()
方法来生成数据,而且在没有数据能够生成的情况下,继续调用 next()
方法,同样会抛出 StopIteration
异常,代码案例如下:关键字 yield
不仅将普通函数变为了生成器函数,还通过 next()
方法返回了其后面的值。
def func():
yield 123
res1 = func() # 注释:创建一个新的生成器。
for r in res1:
print(r) # 输出:123
res2 = func() # 注释:创建一个新的生成器。
print(next(res2)) # 输出:123
print(next(res2)) # 报错:没有数据生成时继续调用next方法,会抛出StopIteration异常。
一个生成器中可以有多个关键字 yield
,代码案例如下:当一个生成器中有多个关键字 yield
时,每次调用 next()
方法就是在分段执行生成器函数,执行到关键字 yield
会暂停生成器运行并返回后面的值,如果关键字 yield
后面没有返回值,则默认返回 None
。这一点和关键字 return
有点像,但不同的是关键字 return
会结束函数的运行,而关键字 yield
只是暂停生成器函数的执行,原因是关键字 yield
内部实现支持了“迭代协议”,同时也是一个状态机,可以维护着挂起和继续的状态。
def func():
print(1, end=', ')
yield 123
print(2, end=', ')
yield 456
print(3, end=', ')
yield
res1 = func() # 注释:创建一个新的生成器。
for r in res1:
print(r, end=', ') # 输出:1, 123, 2, 456, 3, None,
res2 = func() # 注释:创建一个新的生成器。
print(next(res2)) # 输出:1, 123。注释:next()启动生成器,输出了'1',执行到yield暂停生成器,并返回了后面的值'123'。
print(next(res2)) # 输出:2, 456。注释:__next__()再启动生成器,从上次暂停的位置继续运行,输出了'2',执行到yield暂停生成器,返回后面的值'456'。
print(next(res2)) # 输出:3, None。注释:__next__()再启动生成器,从上次暂停的位置继续运行,输出了'3',执行到yield暂停生成器,返回了默认值None。
到这里我们还可以总结一点的就是,生成器通过 next()
方法启动后,后面必须要有关键字 yield
来结束本次启动,否则就会抛出 StopIteration
异常,而通过 for
循环启动的就不需要,因为 for
循环内部会捕获并隐性处理 StopIteration
异常。代码如下:
def func():
print(1, end=', ')
yield 123
print(2, end=', ')
yield 456
print(3, end=', ')
res1 = func() # 注释:创建一个新的生成器。
for r in res1:
print(r, end=', ') # 输出:1, 123, 2, 456, 3,
res2 = func() # 注释:创建一个新的生成器。
print(next(res2)) # 输出:1, 123。注释:next()启动生成器,输出了'1',执行到yield暂停生成器,并返回了后面的值'123'。
print(next(res2)) # 输出:2, 456。注释:__next__()再启动生成器,从上次暂停的位置继续运行,输出了'2',执行到yield暂停生成器,返回后面的值'456'。
print(next(res2)) # 输出:3, StopIteration异常。注释:__next__()再启动生成器,从上次暂停的位置继续运行,输出了'3',接着抛出了StopIteration异常。
yield from
迭代
现在我们知晓了关键字 yield
可以创建生成器并返回后面的值,那假如说生成器中的元素都在一个列表里面,我们要遍历生成器中的元素该如何做呢?代码如下:先调用生成器函数返回生成器对象,再通过 next()
方法获取列表,最后遍历列表获取元素。
def main_generator():
yield ['a', 'b', 'c']
tor = main_generator()
res = next(tor) # 注释:获取yield返回的列表
print(res) # 输出:['a', 'b', 'c']
for t in res:
print(t, end=', ') # 输出:a, b, c,
不过在 Python 中还有更好的做法,使用关键词 yield from
在一个生成器中迭代另一个可迭代对象(如列表、元组、集合、字典、字符串、文件对象、另一个生成器等)的所有元素。代码如下:当在生成器中使用 yield from
时,它会将内部可迭代对象的元素逐个 yield
出来,而不是将整个可迭代对象作为一个整体给 yield
出来。
def main_generator():
# 使用将['a', 'b', 'c']逐个yield出来
yield from ['a', 'b', 'c']
tor = main_generator()
for t in tor:
print(t, end=', ') # 输出:a, b, c,
另外,如果我们需要 yield
的元素在另外的生成器里面,同样也可以使用 yield from
来返回,毕竟生成器就是可迭代对象嘛。代码案例如下:
def sub_generator():
yield 'b'
yield 'c'
def main_generator():
yield 'a'
yield from sub_generator()
tor = main_generator()
for t in tor:
print(t, end=', ') # 输出:a, b, c,
建议
yield from
结构会在内部自动捕获生成器的 StopIteration
异常,这种处理方式与 for
循环处理 StopIteration
异常的方式一样,都是隐性处理的。
send
传递
现在我们知晓 next()
方法可以启动生成器往外输出数据,而 send()
方法不仅可以启动生成器往外输出数据,还可以往生成器里面传递参数。从本质上来说, next()
方法等价于 send(None)
方法。代码案例如下:
def func():
print(1, end=', ')
a = yield 2
print(a, end=', ')
b = yield 3
print(b, end=', ')
yield 4
g = func() # 注释:f()返回一个生成器对象,将其引用赋值给了变量g。
print(next(g)) # 输出:1, 2。注释:next()启动生成器,输出了'1',执行到yield暂停生成器,并返回了后面的值'2'。
print(next(g)) # 输出:None, 3。注释:next()本质是send(None),send()启动生成器,从上次暂停的位置继续运行,None赋值给变量a,执行到yield暂停生成器,返回后面的值'3'。
print(g.send(0)) # 输出:0, 4。注释:send()启动生成器g,从上次暂停的位置继续运行,0赋值给变量b,执行到yield暂停生成器,返回后面的值'4'。
重要
使用 send()
方法有一个重要的细节需要注意,就是 send()
方法通常是在生成器已经启动(至少执行到了第一个 yield
语句)之后,用于向生成器内部发送一个值,并继续执行生成器直到遇到下一个 yield
语句或生成器结束。所以在第一次启动生成器时,不能直接使用 send()
方法,必须先用 next()
或 send(None)
来启动生成器,否则就会抛出 TypeError
异常。
close
关闭
生成器生成输出数据的过程中,如果想关闭生成器,可以使用 生成器.close()
方法来关闭。代码案例如下:
def generator():
yield '1'
yield '2'
yield '3'
g = generator() # 注释:返回一个生成器保存到变量g中。
print(next(g)) # 输出:1。注释:next()启动生成器g,运行到yield时暂停,返回后面的值'1'。
g.close() # 注释:关闭生成器g,将其在内存中销毁。
print(next(g)) # 报错:生成器g已被销毁,next()无法再启动。
装饰器(Decorator
)
前面我们学习了迭代器(Iterator
)、生成器(Generator
),这里再介绍一个可以在执行指定函数的前后执行别的代码并返回修改后的对象,名称叫“装饰器(Decorator
)”。
场景假设
现在我们要给某个已上线程序增添一个新功能(例如日志插入、事物处理等),具体要求有如下三点:
- 不修改程序源代码。
- 不改变函数的调用方式。
- 在满足要求 1、2 的情况下给程序增添功能。
首先,我们写一个函数,实现普通功能。代码如下:
def test():
print("running") # 输出:running
test() # 注释:test指函数对象引用,test()指调用test函数对象。
接着,使用高阶函数把函数对象当作实参传给另外一个函数。代码如下:
def inner(func): # 1.注释:声明函数inner。4.注释:形参func接收实参函数对象引用test。
print("one") # 5.输出:one
func() # 6.注释:func就是函数对象引用test,func()即调用函数test。
print("two") # 8.输出:two
def test(): # 2.注释:声明函数test。
print("running") # 7.输出:running
inner(test) # 3.注释:调用函数inner并将函数对象引用test当作实参传递。
最后,使用闭包(可以记住并访问外部函数变量的嵌套函数)重构上面的代码。代码如下:
def timer(func): # 1.注释:声明函数timer。4.注释:形参func接收实参函数对象引用test。
def inner(): # 5.注释:声明函数inner。
print("one") # 9.输出:one
func() # 10.注释:func就是函数对象引用test,func()即调用函数test。
print("two") # 12.输出:two
return inner # 6.注释:返回函数对象引用inner。
def test(): # 2.注释:声明函数test。
print("running") # 11.输出:running
test = timer(test) # 3.注释:调用函数timer并将函数对象引用test当作实参传递。7.注释:返回函数对象引用inner赋值给变量test。
test() # 8.注释:变量test为函数对象引用inner,test()即调用函数inner。
到这里我们就实现了对原程序功能的添加,并且满足的上面的三点要求,具体解释如下:
- 不修改程序源代码:目前没有修改程序中
test
函数的源代码。 - 不改变函数的调用方式:之前程序中
test
函数的调用方式和目前程序中test
函数的调用方式一样,没有任何区别。 - 在满足要求 1、2 的情况下给程序增添功能:目前程序中调用
test
函数的前后会执行别的功能代码。
装饰语法
虽然现在我们已经在满足要求的情况下实现了对原程序功能的添加,但是程序中有一个不便的地方,就是每次调用 test
函数都必须要先经过 test = timer(test)
手动对 test
函数进行封装,代码如下:
# 第一次调用
test = timer(test)
test()
# 第二次调用
test = timer(test)
test()
# 第三次调用
test = timer(test)
test()
好在 Python 提供了一种语法糖 @装饰器
(特殊语法)去装饰其他的函数,使用格式如下:
def 装饰器(func):
'在被装饰函数前运行的代码'
def 内部函数():
'在被装饰函数前运行的代码'
func() # 注释:执行被装饰函数代码。
'在被装饰函数后运行的代码'
return 内部函数
@装饰器
def 函数():
'函数代码'
# 第一次调用
函数()
# 第二次调用
函数()
# 第三次调用
函数()
可以看到,使用语法糖 @装饰器
能自动装饰函数,避免每次调用函数前都需要对函数进行封装。这里就可以重写代码如下:
def timer(func): # 1.注释:声明函数timer。3.注释:形参func接受函数对象引用test。
def inner(): # 4.注释:声明函数inner。
print("one") # 7.输出:one
func() # 8.注释:调用函数test。
print("two") # 10.输出:two
return inner # 5.注释:返回函数对象引用inner。
@timer # 2.注释:调用函数timer并将被装饰函数对象引用test当作实参传递。
def test():
print("running") # 9.输出:running
test() # 6.注释:调用返回的函数inner。
建议
如果在递归函数上添加装饰器,那么装饰器会因为递归函数的特性启动多次。如果只需要启动一次,只需要再写个新函数来调用递归函数,然后在新函数上添加装饰器即可。
带参装饰
装饰器不仅能在不改变程序源代码的情况下添加功能,结合不定参数 *args
、**kwargs
我们还可以实现带参装饰。代码如下:
def timer(func): # 1.注释:声明函数timer。3.注释:形参func接受函数对象引用test。
def inner(*args, **kwargs): # 4.注释:声明函数inner。7.注释:不定参数*args接受实参123。
print("one") # 8.输出:one
func(*args, **kwargs) # 9.注释:调用函数test,不定参数*args等于(123,)。11.注释:返回值'abc'赋值给变量res。
print("two") # 12.输出:two
print(args, kwargs) # 13.输出:(123,), {}
return inner # 5.注释:返回函数对象引用inner。
@timer # 2.注释:调用函数timer并将被装饰函数对象引用test当作实参传递。
def test(param):
print("running") # 10.输出:running
test(123) # 6.注释:调用返回的函数inner并传递实参123。
如果需要对不同的函数有不同的装饰,那就要带一个参数告诉装饰器哪个是 test1
,哪个是 test2
,例如:@timer(param=test1)
。通过再加一层函数来接受参数,把 param
参数传递到装饰器中,根据嵌套函数的概念,要想执行内函数,就要先执行外函数,才能调用到内函数。代码如下:
def timer(param): # 1.注释:声明函数timer。3.注释:形参param接收函数对象引用task1。10.注释:形参param接收函数对象引用task1。
def outer_wrapper(func): # 4.注释:声明函数outer_wrapper。6.注释:形参func等于函数对象task1。11.注释:声明函数outer_wrapper。13.注释:形参func等于函数对象task2。
def wrapper(): # 7.注释:声明函数wrapper。14.注释:声明函数wrapper。
print("one") # 17.输出:one。22.输出:one。
func(param) # 18.注释:执行函数task1。23.注释:执行函数task2。
print("two") # 20.输出:two。25.输出:two。
return wrapper # 8.注释:返回函数对象wrapper。15.注释:返回函数对象wrapper。
return outer_wrapper # 5.注释:返回函数对象outer_wrapper。12.注释:返回函数对象outer_wrapper。
@timer(param='task1') # 2.注释:调用函数timer。
def task1(param):
print(param) # 19.输出:task1。
@timer(param='task2') # 9.注释:调用函数timer。
def task2(param):
print(param) # 24.输出:task2。
task1() # 16.注释:调用带有装饰器的函数task1。
task2() # 21.注释:调用带有装饰器的函数task2。
多个装饰
一个函数可以有多个相同或不同的装饰器,然后从上往下去执行,在上面的就是外部装饰器,在下面的就是内部装饰器,代码的执行过程就是从外部装饰器到内部装饰器再到函数,然后再从函数到内部装饰器再到外部装饰器。代码的具体执行流程如下:
def timer_1(func):
def inner():
print("one") # 4.输出:one
func() # 5.注释:返回执行内部装饰器timer_2。
print("four") # 10.输出:four
return inner
def timer_2(func):
def inner():
print("two") # 6.输出:two
func() # 7.注释:返回执行函数。
print("three") # 9.输出:three
return inner
@timer_1 # 1.注释:外部装饰器timer_1。
@timer_2 # 2.注释:内部装饰器timer_2。
def test():
print("running") # 8.输出:running
test() # 3.注释:执行test函数。
携带返回值
最后再补充说明一点的就是,如果被装饰的函数有返回值,那么在装饰器中就必须返回该函数的返回值。代码案例如下:
def timer_1(func):
def inner():
print("one") # 3.输出:one
ret = func() # 4.注释:返回执行函数。7.注释:返回值赋值给变量ret。
print("four") # 8.输出:four
return ret # 9.注释:返回变量ret。
return inner
@timer_1 # 1.注释:装饰器timer_1。
def test():
print("running") # 5.输出:running
return "finish" # 6.注释:返回值finish。
test() # 2.注释:执行test函数。