很多人把 yield from 记成「省一层 for 循环的语法糖」。写 yield from it 和写 for x in it: yield x,结果看着一样,于是就当它只是少敲几个字。
这个理解会在三个地方翻车:调用方 .send() 的值送不进去、子生成器 return 的值拿不到、异常和 close() 传不下去。yield from 真正做的事是把调用方和子生成器直接接起来,外层生成器退成一个透明通道。
本文所有输出来自本机 CPython 3.13.14 实测。
一句话结论:yield from 不是循环,是委托——next、send、throw、close 四个方向的流量全部直通子生成器,并且子生成器 return 的值会成为整个表达式的结果。
一、先看最容易被忽略的:return 值
def sub_ret():
yield 1
yield 2
return 'DONE'
def outer_ret():
r = yield from sub_ret()
yield ('RETURNED', r)
实测:
list(outer_ret()) -> [1, 2, ('RETURNED', 'DONE')]
换成手写循环:
def manual():
for v in sub_ret():
yield v
list(manual()) -> [1, 2]
'DONE' 没了。for 循环通过 StopIteration 结束迭代,而 StopIteration.value 里刚好装着子生成器的返回值,只是没人去接。
yield from 接了。这是 PEP 380 设计它的主要动机之一:让生成器能像普通函数一样「返回结果」,同时保留中途 yield 的能力。asyncio 早期基于生成器的协程,正是靠这条通道把协程返回值往上传。
二、send 和 throw 是双向的
def sub_echo():
x = yield 'READY'
yield 'GOT:%s' % x
def outer_echo():
yield from sub_echo()
g = outer_echo()
g.send(None) # -> 'READY'
g.send(42) # -> 'GOT:42'
42 穿过了 outer_echo,直接落到 sub_echo 里那个 yield 表达式的左边。外层没有写任何转发代码。
异常同理:
def sub_catch():
try:
yield 'A'
except ValueError as e:
yield 'CAUGHT:%s' % e
g2 = outer_catch()
g2.send(None) # -> 'A',状态 GEN_SUSPENDED
g2.throw(ValueError('boom')) # -> 'CAUGHT:boom'
异常也是从调用方直接抛进子生成器的 yield 位置,被子生成器自己 try/except 接住。
close() 一样:外层 .close() 会让子生成器收到 GeneratorExit,子生成器的 finally 照常执行,实测子生成器打印了清理日志,随后状态变 GEN_CLOSED。
如果手写 for 循环,这四件事都得自己写一遍,还得处理 StopIteration、异常注入、GeneratorExit 的传播——这正是 PEP 380 说「样板代码既啰嗦又容易写错」的原因。
三、3.13 里,它编译成什么
反编译 outer_ret:
['RETURN_GENERATOR', 'POP_TOP', 'RESUME', 'LOAD_GLOBAL', 'CALL',
'GET_YIELD_FROM_ITER', 'LOAD_CONST', 'SEND', 'YIELD_VALUE', 'RESUME',
'JUMP_BACKWARD_NO_INTERRUPT', 'END_SEND', 'STORE_FAST', ...]
两个要点:
- 没有
YIELD_FROM指令了。CPython 3.11 起,这条专用指令被拆成SEND+YIELD_VALUE+END_SEND的组合,配合JUMP_BACKWARD_NO_INTERRUPT组成委托循环。网上不少老教程还在讲YIELD_FROM字节码,在 3.13 上已经对不上。 GET_YIELD_FROM_ITER会先对对象取迭代器,所以yield from 'abc'合法(实测['a','b','c']),但对普通生成器它还要求是可迭代对象,传一个整数会直接TypeError。
END_SEND + STORE_FAST 那一对,就是上面说的「把 StopIteration.value 存进 r」。
顺手测了性能,避免误以为语法糖有加速:
yield from range(1000) / for 循环 yield 2000 轮:0.1315s / 0.1307s,比值 0.99
几乎一样。选 yield from 的理由是语义正确,不是更快。
四、一个必须记住的坑:PEP 479
def bad():
yield 1
raise StopIteration('x')
list(bad()) # RuntimeError: generator raised StopIteration
从 Python 3.7 起(PEP 479),生成器里意外冒出的 StopIteration 会被改成 RuntimeError。原因是它会伪装成「迭代正常结束」,让 yield from 提前收工、静默丢数据。
这条规则和 yield from 是一套设计:委托循环靠 StopIteration 判断子生成器结束,所以这个信号不能再被业务代码随便抛。要提前结束生成器,用 return。
五、对照表
| 行为 | yield from sub() |
for v in sub(): yield v |
|---|---|---|
| 产出值 | 透传 | 透传 |
子生成器 return 值 |
作为表达式结果可接住 | 丢失 |
.send(v) |
直达子生成器 yield 表达式 |
外层收到,需手写转发 |
.throw(e) |
抛进子生成器 yield 位置 |
抛在外层 |
.close() / finally |
传播到子生成器 | 不自动传播 |
| 非迭代对象 | GET_YIELD_FROM_ITER 报 TypeError |
for 报 TypeError |
| 3.13 字节码 | GET_YIELD_FROM_ITER+SEND+YIELD_VALUE+END_SEND |
GET_ITER+FOR_ITER |
| 性能 | 实测比值 0.99,基本持平 | — |
六、七条排错清单
- 子生成器
return的值没拿到:检查是不是写成了yield from sub()却没赋值给变量;要写r = yield from sub()。 .send()的值像石沉大海:外层多半用了手写for循环,改成yield from。RuntimeError: generator raised StopIteration:业务代码里手动raise StopIteration了,改成return(PEP 479)。TypeError: cannot 'yield from' a non-iterable:yield from后面必须是可迭代对象;想推数据用yield,不是yield from。- 反汇编看不到
YIELD_FROM:3.11+ 已改为SEND/YIELD_VALUE/END_SEND,别照着旧资料对字节码。 close()后子生成器资源没释放:确认外层确实走了yield from;手写循环不会替你传播GeneratorExit。- 协程里混用
yield from和await:async def里委托协程要用await,yield from只能用于生成器/可迭代对象。
小结
yield from 把四件事一次做全:值向外、值向内、异常向内、关闭信号向内,再把子生成器的 return 值交回外层。少写一行 for 只是副作用。
判断要不要用它,问一句就够了:调用方需不需要和里层那个生成器直接对话? 需要,就用 yield from;只是想把序列摊平,普通的 for 循环也没问题。
本文代码与字节码输出均在本机 CPython 3.13.14 实测,脚本
d60-probe.py;规范依据 PEP 380、PEP 479。