周六有四百张订单,为了求和先把它们全塞进一个列表,收银台就开始卡。生成器一次只交出一张。yield 之间到底保留了什么?
生成器函数一次产生一个值,并记住自己停在哪里。因此它可以在不把全部值放进内存的情况下处理很长甚至无限的序列。关键在 yield:它暂停函数,而不是永久返回。 你还会用 yield from 委托、观察惰性执行,并用 close() 触发清理。
一次交出一个值
函数执行到 yield 时,会把一个值交还给调用方并暂停。下一次请求会从这一行之后继续。循环可以消费整个序列,而不必事先知道会产出多少个值。
def countdown(n):
while n > 0:
yield n
n -= 1
for value in countdown(3):
print(value)会暂停的函数
用 next 主动取值
调用生成器函数不会立刻执行函数体,而是返回一个生成器对象。每次调用 next 才请求一个值。没有更多值时,next 会抛出 StopIteration,for 循环会自动处理它。
def numbers():
yield 1
yield 2
gen = numbers()
print(next(gen))
print(next(gen))按需求值
使用生成器表达式
生成器表达式使用圆括号,而不是列表推导式的方括号。它惰性地产生值。需要查看或重复使用完整结果时,再用 list() 包起来。
squares = (n * n for n in range(4))
print(list(squares))惰性推导式
yield from 委托一个流
yield from 把控制权交给另一个可迭代对象,并转发每个值。它替代嵌套的 for/yield,让组合关系更明确。内层生成器耗尽后,外层生成器继续执行。
def drinks():
yield "latte"
yield "tea"
def menu():
yield "menu"
yield from drinks()
yield "done"
print(list(menu()))从子生成器转发值
生成器流水线保持惰性
创建生成器不会运行函数体。只有消费者请求下一个值时,值才流动,所以流水线可以提前停止。日志等副作用发生在消费时,而不是构造流水线时。
def source():
print("source start")
for n in [1, 2, 3]:
print("yield", n)
yield n
pipeline = (n * 10 for n in source())
print("before consume")
print(next(pipeline))
print("after first")
print(list(pipeline))消费者拉动之前什么都不运行
close() 会执行 finally
调用 close() 会在暂停的 yield 处抛出 GeneratorExit,并执行 finally。消费者提前停止时,生成器因此能释放文件或锁。通常 with 更清楚,但 close() 解释了清理契约。
def resource():
try:
yield "open"
yield "still open"
finally:
print("cleanup")
gen = resource()
print(next(gen))
gen.close()
print("closed")提前停止也能清理
本课要点
- yield 会暂停生成器并产生一个值。
- 调用生成器函数会返回生成器对象。
- next 取出一个值;StopIteration 表示结束。
- 生成器表达式使用圆括号,而且只能消费一次。
- 生成器惰性执行;yield from 组合流;close() 触发 finally 清理。
常见错误
- 生成器耗尽后还试图重复使用它。
- 以为第一次调用 next 之前函数体就会执行。
- 序列太大无法放入内存时,仍然使用列表推导式。
- 创建生成器后就期待它的副作用立刻执行。
写一个依次产出 3、2、1 的生成器,再打印 list(countdown(3))。
动手试试
改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。