在生成器语义中,yield 解决的是单个执行帧如何被暂停与恢复的问题;而 yield from 解决的则是一个更高层次的问题:当生成器的执行需要被“结构化地让渡”给另一个生成器或可迭代对象时,执行控制权应当如何转移、托管并最终收回。
yield from 并不是对 yield 的语法补丁,也不是“少写几行代码”的便利写法,而是一种独立的表达式构造。它在语言层面引入了执行控制权委托(delegation)这一机制,使多个生成器的执行过程可以被组合为一条连续、可嵌套、可回收的执行链。
理解 yield from,关键不在于记住它的用法,而在于理解 Python 如何在不暴露执行细节的前提下,规范生成器之间的协作方式。
一、yield from 是表达式,而不是语法糖
在 Python 3 中,yield from 与 yield 一样,是表达式(expression),而不是语句(statement)。
其基本语法形式为:
与普通 yield 一样,yield from 也可以出现在赋值语境中:
result = yield from <iterable>
这意味着两点:
(1)yield from 本身是一次表达式求值过程;
(2)该表达式在完成时是有值的,而不是“什么也不返回”。
需要注意的是,<iterable> 可以是生成器对象,也可以是任意实现了迭代协议的可迭代对象。
但只有当 <iterable> 是生成器对象时,yield from 的完整执行语义(尤其是返回值与异常转交)才会被完全体现。
二、三个角色:调用方、委托生成器与被委托对象
在 yield from 的执行模型中,始终涉及三个明确的角色:调用方、委托生成器和被委托对象。
它们之间的关系可以表示为:
调用方 ⇄ 委托生成器 ⇄ 被委托对象
示例:三种角色在代码中的对应关系
def sub(): yield 1 yield 2def delegator(): yield from sub()g = delegator()next(g)
在这段代码中,三个角色可以被清晰地对应到具体位置。
1、调用方
调用方(caller)是指通过 next()、send()、throw() 等方式,直接推进最外层生成器对象执行的那一方。
调用方的语义角色非常单一:负责驱动生成器继续执行,但并不参与生成器内部的控制逻辑。
2、委托生成器
def delegator(): yield from sub()
委托生成器(delegating generator)是指函数体中直接包含 yield from 表达式,并在执行过程中主动让出执行控制权的生成器。
在本例中,delegator 是一个生成器函数;delegator() 返回的生成器对象,在执行到 yield from sub() 时担任委托生成器这一角色。
需要强调的是,委托生成器并不是“被调用得更多”的那个,而是在自身执行过程中,选择将后续执行暂时交由他者完成的那个。
3、被委托对象
被委托对象(delegated iterable)是指被 yield from 指定,并在一段时间内实际接管执行推进权的对象。当该对象本身是生成器时,亦常被称为“子生成器”(subgenerator)。
在本例中,sub() 是生成器函数 sub 的一次调用结果,返回的生成器对象,正是 yield from sub() 中被委托的对象。
需要注意的是,被委托对象既可以是生成器对象,也可以是其他可迭代对象(如 list)。当它是生成器对象时,yield from 的执行语义才能被完整展开。
三、执行过程解析:值的透明传递与执行控制权的让渡
yield from 的核心语义正是:在一段时间内,委托生成器不再直接处理来自调用方的推进请求,而是将这些请求完整转发给被委托对象。
从调用方视角看,推进的仍是同一个生成器对象;但在解释器内部,当前活动执行帧已从委托生成器切换为被委托对象。
示例:
def sub(): yield 1 yield 2def delegator(): yield from sub() yield 3
执行:
g = delegator()next(g) # 1next(g) # 2next(g) # 3
1、从“产出结果”观察执行行为
从调用方的角度看,执行结果是连续且线性的:
→ 第一次 next(g) 得到 1
→ 第二次 next(g) 得到 2
→ 第三次 next(g) 得到 3
在 yield from sub() 生效期间,所有被产出的值均来自 sub()(本例中为 1、2);对调用方而言,看不到任何“中间转接层”的存在。
当 sub() 迭代结束之后,yield from 表达式结束,控制权回到 delegator(),随后才执行 yield 3。
这说明 yield from 在“产出值”这一层面对调用方是透明的。
2、从执行过程看“透明性”的来源
当 delegator() 的执行推进到:
这一表达式时,当前活动执行帧将发生切换。
在 yield from 生效期间:
• 委托生成器(delegator())的执行帧在该表达式处被暂停
• 被委托对象(sub() 返回的生成器对象)成为当前实际被推进的执行目标
• 调用方的每一次 next(g),都会直接作用于被委托对象,而不是委托生成器本身。
也就是说,在这段时间内,调用方不再与委托生成器发生直接交互,而是被“重定向”到了被委托对象之上。
正因为这一重定向对调用方是不可见的,才表现为前一节中观察到的“值的透明传递”。
3、执行控制权的完整让渡
这种“重定向”并不仅限于产出值,而体现在执行控制权的整体让渡上。
在 yield from 生效期间,以下操作都会被原样转交给被委托对象,比如:
• 通过 __next__() 推进执行
• 通过 send(value) 向暂停点注入值
• 通过 throw(exception) 向生成器内部抛入异常
• 通过 close() 请求生成器终止
在这一阶段,委托生成器并不参与这些操作的处理。它处于一种完全让权、等待回收执行控制权的状态;只有当被委托对象执行完毕,控制权才会回到委托生成器。
因此可以准确地说,yield from 并不是“多了一层调用”,而是在不中断执行流程的前提下,将“如何继续执行”的决定权临时让渡给被委托对象,从而在保持调用方执行连续性感知的同时,实现执行逻辑的拆分与组合。
说明:当被委托对象是普通可迭代对象时,yield from 仍会建立值转发结构,但不会发生执行帧级别的控制权协作。
四、yield from 表达式的返回值来源
与普通 yield 不同,yield from 表达式在完成时,其表达式值并不来自调用方的 send,而是来自被委托对象的完成态返回值。
示例:
def sub(): yield 1 return "done"def delegator(): result = yieldfromsub() yield result
执行:
g = delegator()next(g) # 1next(g) # "done"
其执行语义可以分解为:
1、sub() 在正常结束时执行 return "done"。
2、解释器将该返回值封装进 StopIteration 异常中。
3、yield from sub() 表达式捕获该值。
4、表达式的求值结果即为 "done",并绑定给 result。
因此可以得出一个关键结论:
yield from 表达式的值,来自被委托对象在完成时提供的返回值(当其为生成器对象时),而不是其中任意一次 yield。
五、为什么 for + yield 不能等价替代 yield from
表面上看:
似乎等价于:
for x in iterable: yield x
但在执行模型层面,这两者有本质差异。
for + yield 仅仅完成了“值的逐个转发”,而它无法:
• 接收并转交 send(value) 注入的值
• 向被委托对象注入异常(throw)
• 感知被委托对象的返回值
• 正确处理被委托对象的关闭过程。
相比之下,yield from:
• 转交的是执行推进权本身
• 能够形成执行帧级别的协作关系
• 支持生成器之间的组合与嵌套
因此,yield from 并不是循环展开意义上的语法糖,而是一种执行模型级别的语言构造。
六、执行帧视角下的协作结构
从执行模型角度看:
yield:在同一个执行帧中建立暂停点;
yield from:在多个执行帧之间建立控制权转移关系。
在 yield from 生效期间:
• 委托生成器的执行帧处于挂起状态
• 被委托对象的执行帧成为当前活动执行帧
• 当被委托对象完成后,控制权再回到委托生成器
这使得生成器执行过程可以被拆分、组合、嵌套、分层管理,而无需调用方感知这些内部结构。
在此基础上,Python 后续引入的协程语义(async / await),本质上正是对这一执行模型的进一步规范化与语法化。
📘 小结
yield from 是一种用于委托执行控制权的表达式。它在一段时间内暂停当前生成器的执行,并将执行推进、值注入、异常处理与终止请求统一转交给被委托对象;在被委托对象完成后,再恢复自身执行并接收其完成态返回值。
理解 yield from 的语义,有助于从执行模型与执行帧协作的角度,把握 Python 生成器组合机制及其向协程模型演进的内在逻辑。