Python 3.12 Key Words -yield、yield from
生成器是 Python 中一种特殊的迭代器,它允许你延迟生成数据,而不是一次性将所有结果存储在内存中。yield关键字用于定义生成器函数,而yield from则用于将迭代任务委托给子生成器。生成器在流式处理大型数据集、实现协程(Python 3.5 之前)、以及构建无限序列时具有不可替代的地位。本文将深入解析yield和yield from的语法、语义、使用场景、底层实现,并通过大量示例逐行讲解,帮助你彻底掌握生成器的奥秘。
第一部分:生成器基础 ——yield
1.1 什么是生成器?
生成器是一种特殊的迭代器,它通过yield语句返回值,并记住当前执行状态,待下次调用时从上次暂停的地方继续执行。
普通函数 vs 生成器函数
- 普通函数使用
return返回值,调用后函数结束,局部变量被销毁。 - 生成器函数使用
yield返回值,调用后返回一个生成器对象,函数体暂停,局部变量保留。
defsimple_generator():print("Start")yield1print("Resume")yield2print("End")1.2 基本语法
defgenerator_func():yieldvalue# 可以多个 yield调用生成器函数返回一个生成器对象,支持迭代协议(__iter__和__next__)。
1.3 示例与逐行解析
defcount_up_to(n):i=0whilei<n:yieldi i+=1# 创建生成器对象gen=count_up_to(3)print(next(gen))# 0print(next(gen))# 1print(next(gen))# 2print(next(gen))# StopIteration逐行解析:
def count_up_to(n):定义生成器函数。yield i:暂停函数,将i返回给调用者,并记住当前位置(包括局部变量i的值和循环状态)。gen = count_up_to(3):调用生成器函数,返回生成器对象(不会立即执行函数体)。next(gen):执行生成器代码直到遇到第一个yield,返回0,函数暂停。- 再次
next(gen),从上一次yield之后继续执行,循环体增量,再次yield,返回1,暂停。 - 当循环结束,函数自然退出,再调用
next(gen)会抛出StopIteration。
为什么这样写?
生成器能够在需要时逐个产生值,避免一次性构建整个列表,节省内存。特别适合处理大文件、无限序列或流式数据。
1.4 生成器对象的方法
生成器对象提供了几个方法:
__next__():等价于next(gen),执行到下一个yield。send(value):向生成器内部发送一个值,该值成为当前yield表达式的返回值,然后继续执行到下一个yield。throw(type, value=None, traceback=None):在生成器当前挂起的位置抛出异常。close():关闭生成器,在内部引发GeneratorExit异常。
这些方法使生成器可以作为协程使用(现代 Python 用async/await替代,但仍有应用)。
使用send
defecho():whileTrue:received=yieldprint(f"Received:{received}")g=echo()next(g)# 启动生成器,执行到第一个 yield(此时 yield 返回 None)g.send("Hello")# 发送 "Hello",yield 接收该值并赋值给 received,然后打印,循环继续g.send("World")使用throw
defcatcher():try:yield1yield2exceptValueError:yield3g=catcher()print(next(g))# 1print(g.throw(ValueError))# 3 (异常被捕获,yield 3)使用close
defgen():try:yield1finally:print("Cleanup")g=gen()print(next(g))g.close()# 触发 finally,打印 "Cleanup"1.5 生成器表达式
生成器表达式类似于列表推导式,但使用圆括号而不是方括号,返回一个生成器对象,惰性求值。
squares=(x*xforxinrange(10))print(next(squares))# 0print(next(squares))# 1生成器表达式适合在需要逐个处理数据且只需迭代一次的场景。
第二部分:yield from—— 委托生成器
2.1 为什么需要yield from?
在 Python 3.3 之前,如果要让一个生成器产生另一个可迭代对象的所有值,需要手动循环:
defflatten(nested):forsubinnested:foriteminsub:yielditemyield from可以简化这种嵌套循环,并支持向子生成器发送值、传递异常等高级功能。
2.2 基本语法
defgenerator():yieldfromiterable等价于:
defgenerator():foriteminiterable:yielditem但yield from更加高效,且能处理子生成器的返回值。
2.3 示例与逐行解析
defsubgen():yield1yield2return"Done"defdelegator():result=yieldfromsubgen()print(f"Result from subgen:{result}")g=delegator()print(next(g))# 1print(next(g))# 2try:next(g)# 子生成器结束,返回 "Done",赋值给 result,打印,然后 delegator 结束,抛出 StopIterationexceptStopIteration:pass逐行解析:
subgen是一个简单的生成器,产生1和2,最后return "Done"。delegator中result = yield from subgen():- 首先,
subgen()被调用,返回一个生成器对象。 yield from会迭代该生成器,并将subgen产生的值逐个转发给delegator的调用者。- 当
subgen结束时,它的return值("Done")被捕获,并赋值给result,然后delegator继续执行后面的print。 - 最后
delegator正常结束,抛出StopIteration。
- 首先,
- 外部代码调用
next(g)三次:前两次获取1和2,第三次触发delegator完成,打印并抛出StopIteration。
为什么这样写?yield from不仅简化了嵌套循环,还能传递send、throw和close调用,实现生成器间的双向通信,是编写复杂数据处理管道的基础。
2.4 递归遍历树结构
defflatten_tree(tree):ifisinstance(tree,(list,tuple)):forsubtreeintree:yieldfromflatten_tree(subtree)else:yieldtree nested=[1,[2,[3,4],5],6]foriteminflatten_tree(nested):print(item)# 1 2 3 4 5 6yield from递归地将子列表中的元素逐一产生,简洁高效。
2.5 作为协程的双向通道
yield from可以与子生成器进行双向通信:父生成器通过send发送的值会传递给子生成器,子生成器通过yield返回的值会传递给父生成器的调用者。
defsubgen():whileTrue:received=yieldprint(f"Subgen received:{received}")defdelegator():yieldfromsubgen()g=delegator()next(g)# 启动,执行到子生成器的第一个 yieldg.send("Hello")# 父生成器将值传递给子生成器g.send("World")2.6 异常传播
如果子生成器抛出异常,该异常会传播到父生成器。如果父生成器捕获了异常,可以继续处理。
defsubgen():yield1raiseValueError("Oops")yield2defdelegator():try:yieldfromsubgen()exceptValueErrorase:print(f"Caught:{e}")g=delegator()print(next(g))# 1next(g)# 子生成器抛出异常,被 delegator 捕获,打印,然后 delegator 结束第三部分:生成器的底层实现
3.1 生成器对象的结构(CPython)
在 CPython 中,生成器对象由PyGenObject结构表示:
typedefstruct{PyObject_HEAD PyFrameObject*gi_frame;// 帧对象,保存执行状态PyObject*gi_code;// 代码对象char*gi_running;// 是否正在运行PyObject*gi_weakreflist;// 弱引用列表PyObject*gi_name;PyObject*gi_qualname;}PyGenObject;gi_frame:保存当前执行帧(包括局部变量、字节码指针等)。gi_code:生成器函数的编译后代码对象。gi_running:防止生成器递归调用。
3.2 生成器的执行流程
- 调用生成器函数时,Python 创建
PyGenObject实例,但不立即执行函数体。 - 第一次调用
next(gen)时,解释器调用gen_send_ex函数,开始执行生成器函数的字节码。 - 当遇到
yield指令时,解释器保存当前栈帧的位置,将返回值压栈,并挂起生成器。 - 下一次调用
next(gen)恢复帧,从上次yield之后继续执行。 - 当函数正常返回或抛出异常时,生成器结束,并抛出
StopIteration。
3.3 字节码分析
importdisdefsimple_gen():yield1yield2dis.dis(simple_gen)输出(简化):
2 0 LOAD_CONST 1 (1) 2 YIELD_VALUE 4 POP_TOP 3 6 LOAD_CONST 2 (2) 8 YIELD_VALUE 10 POP_TOP 12 LOAD_CONST 0 (None) 14 RETURN_VALUEYIELD_VALUE:将栈顶的值产出,并挂起生成器。- 当生成器恢复时,从
YIELD_VALUE之后的指令继续。
3.4yield from的字节码
yield from对应GET_YIELD_FROM_ITER和YIELD_FROM指令。它比手动循环更高效,因为它在 C 层面实现了子迭代器的迭代逻辑,并处理send、throw等。
第四部分:综合应用示例
4.1 无限序列:斐波那契数列
deffibonacci():a,b=0,1whileTrue:yielda a,b=b,a+b fib=fibonacci()for_inrange(10):print(next(fib),end=' ')# 0 1 1 2 3 5 8 13 21 344.2 分块读取大文件
defprocess(data):passdefread_in_chunks(file_path,chunk_size=1024):withopen(file_path,'rb')asf:whileTrue:chunk=f.read(chunk_size)ifnotchunk:breakyieldchunkforchunkinread_in_chunks('large_file.bin'):process(chunk)# 每次处理一块,内存友好4.3 管道处理(生成器链)
defread_file(file):withopen(file)asf:forlineinf:yieldline.strip()deffilter_lines(lines,keyword):forlineinlines:ifkeywordinline:yieldlinedefcount_lines(lines):cnt=0forlineinlines:cnt+=1yieldcnt pipeline=count_lines(filter_lines(read_file('data.txt'),'error'))total=next(pipeline)print(total)4.4 使用yield from展平嵌套列表
defflatten(lst):foriteminlst:ifisinstance(item,list):yieldfromflatten(item)else:yielditem lst=[1,2,[3,4],[[5,6],[7,8]],9]foriteminflatten(lst):print(item,end=" ")4.5 实现简单的itertools.chain
defchain(*iterables):foritiniterables:yieldfromit lists=[[1,2],[3,4],[5,6]]# 使用 * 将列表中的每个子列表作为单独参数传入result=list(chain(*lists))print(result)# [1, 2, 3, 4, 5, 6]4.6 生成器与协程:计算移动平均值
defaverager():total=0count=0average=NonewhileTrue:term=yieldaverage total+=term count+=1average=total/count avg=averager()next(avg)# 启动print(avg.send(10))# 10.0print(avg.send(20))# 15.0print(avg.send(30))# 20.04.7 使用yield from结合send
defsub_averager():total=0count=0average=NonewhileTrue:term=yieldaverage total+=term count+=1average=total/countdefdelegator():whileTrue:result=yieldfromsub_averager()print(f"Subgenerator ended with average:{result}")# 正常不会执行,因为 sub_averager 无限循环第五部分:生成器与异步编程的关系
在 Python 3.5 之前,协程是通过生成器实现的:@asyncio.coroutine装饰器配合yield from。现代 Python 使用async/await,但底层机制仍然依赖生成器。
对比:
| 特性 | 生成器协程(旧) | async/await(新) |
|---|---|---|
| 装饰器 | @asyncio.coroutine | 无需装饰器 |
| 暂停 | yield from | await |
| 类型 | 生成器对象 | 协程对象(CO_COROUTINE) |
| 目的 | 实现异步 I/O | 相同,但语法更清晰 |
尽管现在推荐使用async/await,但理解yield和yield from对于阅读旧代码和底层库仍然重要。
第六部分:常见陷阱与最佳实践
6.1 生成器只能遍历一次
gen=(xforxinrange(3))print(list(gen))# [0,1,2]print(list(gen))# []6.2 忘记关闭生成器导致资源泄露
如果生成器使用了外部资源(如文件),应使用with或显式close。
6.3yield与return的混合
在生成器中,return会触发StopIteration,并可以携带返回值(Python 3.3+)。
defgen():yield1return42# 最终 StopIteration 的 value 是 42g=gen()print(next(g))# 1try:next(g)exceptStopIterationase:print(e.value)# 426.4 谨慎使用send前必须启动生成器
必须先用next(g)或g.send(None)将执行推进到第一个yield。
6.5yield from不能与return一起使用?
实际上可以,子生成器的return值会变成yield from表达式的值,如之前的例子。
第七部分:总结
| 关键字 | 作用 | 主要用途 |
|---|---|---|
yield | 定义生成器,返回值并暂停 | 惰性求值、无限序列、流式处理 |
yield from | 将迭代委托给子生成器 | 展平嵌套结构、构建管道、双向通信 |
- 生成器是 Python 中实现惰性求值、内存友好型迭代的基础工具。
yield和yield from不仅适用于简单的序列生成,还能构建复杂的数据处理管道和协程(旧式)。- 理解生成器的底层实现(帧对象、字节码)有助于编写高性能代码。
- 生成器是 Python 迭代器的终极形态,掌握它能让你的代码更加 Pythonic。
在掌握了生成器之后,我们将进入 Python 3.10 以来的重磅特性:结构模式匹配(match/case)以及 Python 3.12 引入的类型别名语法(type)。
如果在学习过程中遇到问题,欢迎在评论区留言讨论!