说明
《Python 教程》 持续更新中,提供建议、纠错、催更等加作者微信: gairuo123(备注:pandas教程)和关注公众号「盖若」ID: gairuo。跟作者学习,请进入 Python学习课程。欢迎关注作者出版的书籍:《深入浅出Pandas》 和 《Python之光》。
在 Python 中构建迭代器有很多工作要做,这样太麻烦,因此有了生成器类型就变得格外简单。生成器是特殊的迭代器,它也是一种更简单的创建迭代器的方法。它的功能本质是创建了一个规则,但只有在迭代使用时才产生值(当然迭代器的思想也是这样),而不是一次构建完整的列表。生成器类型是这个思想的最佳践行。
Python 中,提供了两种 生成器(Generator) ,一种是生成器函数,另一种是生成器表达式。
生成器函数的语法本质上也是一个函数,和普通函数的 return 返回不同,生成器函数使用 yield。因此我们说 yield 关键词的是生成器。
yield 语句一次返回一个结果,在每个结果中间,会暂停并保存当前所有的运行信息,以便下一次执行 next() 方法时从当前位置继续运行。
def gen():
yield 1
yield 2
yield 3
g = gen()
for i in g:
print(i)
'''
1
2
3
'''
# 已经消费结束
next(g)
# StopIteration
我们定义一个无限偶数生成器:
def gen():
i = 2
while True:
yield i
i+=2
type(gen)
# function
g = gen()
type(g)
# generator
next(g) # 2
next(g) # 4
next(g) # 6
# ...
典型的斐波那契数列可以使用生成器,因为它无限长。斐波那契数列由0和1开始,之后的费波那契系数就是由之前的两数相加而得出,它是一个无穷数列。
# 生成器函数
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fib()
type(fib) # generator
next(fib) # 0
next(fib) # 1
next(fib) # 1
next(fib) # 2
next(fib) # 3
next(fib) # 5
next(fib) # 8
# ...
下面是生成器函数与普通函数的区别。
__iter__()
和 __next__()
这样的方法是自动实现的,因此,我们可以使用 next() 遍历这些项和普通的迭代器不同,生成器有一些方法,在提案 PEP 342(通过增强型生成器实现协程)中,对生成器增加了一些方法:
dir(g)
'''
[...
'close',
'gi_code',
'gi_frame',
'gi_running',
'gi_yieldfrom',
'send',
'throw']
'''
属性有:
g.gi_running # False 是否运行
g.gi_code # 代码对象
g.gi_frame
g.gi_yieldfrom
这些方法的功能是:
generator.__next__()
:开始执行生成器函数,或在最后执行的表达式处继续执行,此方法通常是隐式地调用,例如通过 for 循环或是内置的 next() 函数generator.send(value)
:恢复执行并向生成器函数「发送」一个值。 value 参数将成为当前 yield 表达式的结果。 send() 方法会返回生成器所产生的下一个值,或者如果生成器没有产生下一个值就退出则会引发 StopIteration。 当调用 send() 来启动生成器时,它必须以 None 作为调用参数,因为这时没有可以接收值的 yield 表达式generator.throw(value)
/generator.throw(type[, value[, traceback]])
:在生成器暂停时引发异常,并返回生成器函数生成的下一个值。如果生成器退出而不产生另一个值,则会引发 StopIteration 异常。如果生成器函数没有捕获传入的异常,或引发其他异常,则该异常会传播到调用方。在典型的使用中,这是通过一个异常实例调用的,类似于raise关键字的使用方式。然而,为了向后兼容,支持的多参数,遵循了 Python 旧版本的约定。类型参数应该是异常类,值应该是异常实例。如果未提供该值,则调用类型构造函数以获取实例。如果提供了回溯,则将其设置为异常,否则可能会清除存储在value中的任何现有 __traceback__
属性generator.close()
:在生成器函数暂停的位置引发 GeneratorExit。 如果之后生成器函数正常退出、关闭或引发 GeneratorExit (由于未捕获该异常) 则关闭并返回其调用者。 如果生成器产生了一个值,关闭会引发 RuntimeError。 如果生成器引发任何其他异常,它会被传播给调用者。 如果生成器已经由于异常或正常退出则 close() 不会做任何事。其中最值得关注的是 send() 方法。与.__next__()
方法一样,send() 方法使生成器前进到下一个 yield 语句。不过,send() 方法还允许调用方把数据发送给生成器,即不管传给 send() 方法什么参数,那个参数都会成为生成器函数定义体中对应的 yield 表达式的值。也就是说,send() 方法允许在调用方和生成器之间双向交换数据,而 __next__()
方法只允许调用方从生成器中获取数据。
我们看看怎么影响生成器生成下个值。我们实现了以下一个生成器,内容从 0 开始,每次迭代值加 2,当我们不想加个时可以 send() 我们想要的值,就会按我们指定的值进行增加:
def gen():
i = 0
while True:
n = yield i
i = i + (n or 2)
g = gen()
next(g) # 0
next(g) # 2
next(g) # 4
g.send(3) # 7
next(g) # 9
next(g) # 11
g.send(4) # 15
next(g) # 17
要注意的是,第一次 send() 是必须是传入 None,否则会报如下错。也就是说,在一个生成器函数未启动之前,是不能传递数值进去。必须先传递一个 None g.send(None)
进去或者调用一次 next() (相当于 g.send(None)
,yield 处接收的是 None 值,上例中 n or 2
的 n 为 None 时取 2)方法,才能进行传值操作。
TypeError: can't send non-None value to a just-started generator
利用生成器的这些特点可以实现协程。协程可以理解为一个轻量级的线程,它相对于线程处理高并发场景有很多优势。
用协程实现的生产者-消费者模型:
def producer(c):
n = 0
while n < 5:
n += 1
print('producer {}'.format(n))
r = c.send(n)
print('consumer return {}'.format(r))
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('consumer {} '.format(n))
r = 'ok'
if __name__ == '__main__':
c = consumer()
next(c) # 启动consumer
producer(c)
'''
producer 1
consumer 1
consumer return ok
producer 2
consumer 2
consumer return ok
producer 3
consumer 3
consumer return ok
producer 4
consumer 4
consumer return ok
producer 5
consumer 5
consumer return ok
'''
协程实现了CPU在两个函数之间进行切换从而实现并发的效果。
任何通过 send() 传入的值以及任何通过 throw() 传入的异常如果有适当的方法则会被传给下层迭代器。 如果不是这种情况,那么 send() 将引发 AttributeError 或 TypeError,而 throw() 将立即引发所转入的异常。
当下层迭代器完成时,被引发的 StopIteration 实例的 value 属性会成为 yield 表达式的值。 它可以在引发 StopIteration 时被显式地设置,也可以在子迭代器是一个生成器时自动地设置(通过从子生成器返回一个值)。
当yield表达式是赋值语句右侧的唯一表达式时,括号可以省略。
PEP 380 增加了 yield from
表达式,允许一个生成器将其部分操作委托给另一个生成器。这允许将包含 yield 的代码段分解出来,并放入另一个生成器中。此外,允许子生成器返回一个值,并且该值可供委托生成器使用。虽然主要用于委托给子生成器,但表达式 yield from 实际上允许委托给任意子迭代器。
对于简单的迭代器,iterable 的 yield 本质上只是 iterable 中 for item 的缩写形式:yield item。
>>> def g(x):
... yield from range(x, 0, -1)
... yield from range(x)
...
>>> list(g(5))
[5, 4, 3, 2, 1, 0, 1, 2, 3, 4]
当使用 yield from <expr>
时,所提供的表达式必须是一个可迭代对象。 迭代该可迭代对象所产生的值会被直接传递给当前生成器方法的调用者。
# 字符串
astr='ABC'
# 列表
alist=[1,2,3]
# 字典
adict={"name":"wangbm","age":18}
# 生成器
agen=(i for i in range(4,8))
def gen(*args):
for item in args:
yield from item
new_list=gen(astr, alist, adict, agen)
print(list(new_list))
# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]
由上面两种方式对比,可以看出,yield from后面加上可迭代对象,他可以把可迭代对象里的每个元素一个一个的yield出来,对比yield来说代码更加简洁,结构更加清晰。
在 3.3 版更改: 添加 yield from <expr>
以委托控制流给一个子迭代器。
todo: 流式实时计算平均值案例。
如同列表推导式,对于简单的生成器,可以使用生成器表达式来创建简易生成器。要注意的是,它使用小括号 () 包裹,而不是中括号(所以没有元组推导式的说法,因为元组是不可变的)。
我们生成器表达式定义一个偶数生成器:
ge = (i for i in range(2, 1000000) if i%2==0)
ge
# <generator object <genexpr> at 0x7f8bde6e0660>
type(ge)
# generator
next(ge) # 2
next(ge) # 4
next(ge) # 6
# ...
如果列表推导是制造列表的工厂,那么生成器表达式就是制造生成器的工厂。生成器表达式是创建生成器的简洁语法,这样无需先定义函数再调用。不过,生成器函数灵活得多,可以使用多个语句实现复杂的逻辑,也可以作为协程使用(本教程有介绍)。
除了上文中总结的迭代器的特点外,生成器还有以下特点:
send()
等方法,在迭代过程中可以发送数据,影响下个值,利用这个特性实现协程(本教程协程部分有介绍)如何选择可迭代对象的编写方法:
关于异步生成器可参考本教程关于异步相关的介绍。
Python 内置库 itertools 中的大多数函数是返回各种迭代器对象,如果自己去实现同样的功能,代码量会非常大,而在运行效率上反而更低,因此,我们很有必要了解一下这个标准库。
更新时间:April 5, 2022, 4:59 p.m. 标签:python 迭代 生成器