yield from的意义
制定 PEP 380 时,有人质疑作者 Greg Ewing 提议的语义过于复杂了。他 的回应之一是:“对人类来说,几乎所有最重要的信息都在靠近顶部的 某个段落里。”他还引述了 PEP 380 草稿中的一段话,当时那段话是这 样的: “把迭代器当作生成器使用,相当于把子生成器的定义体内联在 yield from 表达式中。此外,子生成器可以执行 return 语句, 返回一个值,而返回的值会成为 yield from 表达式的值。
PEP 380 中已经没有这段宽慰人心的话,因为没有涵盖所有极端情况。 不过,一开始可以这样粗略地说。
批准后的 PEP 380 在“Proposal”一节 (https://www.python.org/dev/peps/pep-0380/#proposal)分六点说明了 yield from 的行为。这里,我几乎原封不动地引述,不过把有歧义 的“迭代器”一词都换成了“子生成器”,还做了进一步说明。示例 16-17 阐明了下述四点。
-
子生成器产出的值都直接传给委派生成器的调用方(即客户端代 码)。
-
使用 send() 方法发给委派生成器的值都直接传给子生成器。如果 发送的值是 None,那么会调用子生成器的
__next__()方法。如 果发送的值不是 None,那么会调用子生成器的 send() 方法。如 果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运 行。任何其他异常都会向上冒泡,传给委派生成器。 -
生成器退出时,生成器(或子生成器)中的 return expr 表达式 会触发 StopIteration(expr) 异常抛出。
-
yield from 表达式的值是子生成器终止时传给 StopIteration 异常的第一个参数。
yield from 结构的另外两个特性与异常和终止有关。
-
传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成 器的 throw() 方法。如果调用 throw() 方法时抛出 StopIteration 异常,委派生成器恢复运行。StopIteration 之 外的异常会向上冒泡,传给委派生成器。
-
如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器 上调用 close() 方法,那么在子生成器上调用 close() 方法,如 果它有的话。如果调用 close() 方法导致异常抛出,那么异常会 向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常。
yield from 的具体语义很难理解,尤其是处理异常的那两点。Greg Ewing 做得很好,在 PEP 380 中使用英语阐述了 yield from 的语义。
Ewing 还使用伪代码(使用 Python 句法)演示了 yield from 的行为。 我个人认为值得花时间研究 PEP 380 中的伪代码。不过,那段伪代码长 达 40 行,看一遍很难理解。
若想研究那段伪代码,最好将其简化,只涵盖 yield from 最基本且最 常见的用法。
假设 yield from 出现在委派生成器中。客户端代码驱动着委派生成 器,而委派生成器驱动着子生成器。那么,为了简化涉及到的逻辑,我 们假设客户端没有在委派生成器上调用 .throw(...) 或 .close() 方 法。此外,我们还假设子生成器不会抛出异常,而是一直运行到终止, 让解释器抛出 StopIteration 异常。
示例 16-17 中的脚本就做了这些简化逻辑的假设。其实,在真实的代码 中,委派生成器应该运行到结束。下面来看一下在这个简化的美满世界 中,yield from 是如何运作的。
请看示例 16-18,那里列出的代码是委派生成器的定义体中下面这一行 代码的扩充:
RESULT = yieldfrom EXPR
自己试着理解示例 16-18 中的逻辑。 示例 16-18 简化的伪代码,等效于委派生成器中的 RESULT = yield from EXPR 语句(这里针对的是最简单的情况:不支持 .throw(...) 和 .close() 方法,而且只处理 StopIteration 异 常)
_i = iter(EXPR) ➊
try:
_y = next(_i) ➋
except StopIteration as _e:
_r = _e.value ➌
else:
while1: ➍
_s = yield _y ➎
try:
_y = _i.send(_s) ➏
except StopIteration as _e: ➐
_r = _e.value
break
RESULT = _r ➑
❶ EXPR 可以是任何可迭代的对象,因为获取迭代器 _i(这是子生成 器)使用的是 iter() 函数。
❷ 预激子生成器;结果保存在 _y 中,作为产出的第一个值。
❸ 如果抛出 StopIteration 异常,获取异常对象的 value 属性,赋值 给 _r——这是最简单情况下的返回值(RESULT)。
❹ 运行这个循环时,委派生成器会阻塞,只作为调用方和子生成器之 间的通道。
❺ 产出子生成器当前产出的元素;等待调用方发送 _s 中保存的值。注 意,这个代码清单中只有这一个 yield 表达式。
❻ 尝试让子生成器向前执行,转发调用方发送的 _s。
❼ 如果子生成器抛出 StopIteration 异常,获取 value 属性的值,赋 值给 _r,然后退出循环,让委派生成器恢复运行。
❽ 返回的结果(RESULT)是 _r,即整个 yield from 表达式的值。
在这段简化的伪代码中,我保留了 PEP 380 中那段伪代码使用的变量名 称。这些变量是:
_i(迭代器)
子生成器
_y(产出的值)
子生成器产出的值
_r(结果)
最终的结果(即子生成器运行结束后 yield from 表达式的值)
_s(发送的值)
调用方发给委派生成器的值,这个值会转发给子生成器
_e(异常)
异常对象(在这段简化的伪代码中始终是 StopIteration 实例)
除了没有处理 .throw(...) 和 .close() 方法之外,这段简化的伪代 码还在子生成器上调用 .send(...) 方法,以此达到客户调用 next() 函数或 .send(...) 方法的目的。首次阅读时不要担心这些细微的差 别。前面说过,即使 yield from 结构只做示例 16-18 中展示的事情, 示例 16-17 也依旧能正常运行。
但是,现实情况要复杂一些,因为要处理客户对 .throw(...) 和 .close() 方法的调用,而这两个方法执行的操作必须传入子生成器。
此外,子生成器可能只是纯粹的迭代器,不支持 .throw(...) 和 .close() 方法,因此 yield from 结构的逻辑必须处理这种情况。如 果子生成器实现了这两个方法,而在子生成器内部,这两个方法都会触 发异常抛出,这种情况也必须由 yield from 机制处理。调用方可能会 无缘无故地让子生成器自己抛出异常,实现 yield from 结构时也必须 处理这种情况。最后,为了优化,如果调用方调用 next(...) 函数或 .send(None) 方法,都要转交职责,在子生成器上调用 next(...) 函 数;仅当调用方发送的值不是 None 时,才使用子生成器的 .send(...) 方法。
为了方便对比,下面列出 PEP 380 中扩充 yield from 表达式的完整伪 代码,而且加上了带标号的注解。示例 16-19 中的代码是一字不差复制 过来的,只有标注是我自己加的。
再次说明,示例 16-19 中的代码是委派生成器的定义体中下面这一个语 句的扩充:
RESULT = yieldfrom EXPR
示例 16-19 伪代码,等效于委派生成器中的 RESULT = yield from EXPR 语句
_i = iter(EXPR) ➊
try:
_y = next(_i) ➋
except StopIteration as _e:
_r = _e.value ➌
else:
while 1: ➍
try:
_s = yield _y ➎
except GeneratorExit as _e: ➏
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e
except BaseException as _e: ➐
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else: ➑
try:
_y = _m(*_x)
except StopIteration as _e:
_r = _e.value
break
else: ➒
try: ➓
if _s is None: ⓫
_y = next(_i)
else:
_y = _i.send(_s)
except StopIteration as _e: ⓬
_r = _e.value
break
RESULT = _r ⓭
❶ EXPR 可以是任何可迭代的对象,因为获取迭代器 _i(这是子生成 器)使用的是 iter() 函数。
❷ 预激子生成器;结果保存在 _y 中,作为产出的第一个值。
❸ 如果抛出 StopIteration 异常,获取异常对象的 value 属性,赋值 给 _r——这是最简单情况下的返回值(RESULT)。
❹ 运行这个循环时,委派生成器会阻塞,只作为调用方和子生成器之 间的通道。
❺ 产出子生成器当前产出的元素;等待调用方发送 _s 中保存的值。这 个代码清单中只有这一个 yield 表达式。
❻ 这一部分用于关闭委派生成器和子生成器。因为子生成器可以是任 何可迭代的对象,所以可能没有 close 方法。
❼ 这一部分处理调用方通过 .throw(...) 方法传入的异常。同样,子❶ EXPR 可以是任何可迭代的对象,因为获取迭代器 _i(这是子生成 器)使用的是 iter() 函数。
❷ 预激子生成器;结果保存在 _y 中,作为产出的第一个值。
❸ 如果抛出 StopIteration 异常,获取异常对象的 value 属性,赋值 给 _r——这是最简单情况下的返回值(RESULT)。
❹ 运行这个循环时,委派生成器会阻塞,只作为调用方和子生成器之 间的通道。
❺ 产出子生成器当前产出的元素;等待调用方发送 _s 中保存的值。这 个代码清单中只有这一个 yield 表达式。
❻ 这一部分用于关闭委派生成器和子生成器。因为子生成器可以是任 何可迭代的对象,所以可能没有 close 方法。
❼ 这一部分处理调用方通过 .throw(...) 方法传入的异常。同样,子生成器可以是迭代器,从而没有 throw 方法可调用——这种情况会导 致委派生成器抛出异常。 ❽ 如果子生成器有 throw 方法,调用它并传入调用方发来的异常。子 生成器可能会处理传入的异常(然后继续循环);可能抛出 StopIteration 异常(从中获取结果,赋值给 _r,循环结束);还可 能不处理,而是抛出相同的或不同的异常,向上冒泡,传给委派生成 器。
❾ 如果产出值时没有异常……
❿ 尝试让子生成器向前执行……
⓫ 如果调用方最后发送的值是 None,在子生成器上调用 next 函数, 否则调用 send 方法。
⓬ 如果子生成器抛出 StopIteration 异常,获取 value 属性的值,赋 值给 _r,然后退出循环,让委派生成器恢复运行。
⓭ 返回的结果(RESULT)是 _r,即整个 yield from 表达式的值。
这段 yield from 伪代码的大多数逻辑通过六个 try/except 块实现, 而且嵌套了四层,因此有点难以阅读。此外,用到的其他流程控制关键 字有一个 while、一个 if 和一个 yield。找到 while 循环、yield 表 达式以及 next(...) 函数和 .send(...) 方法调用,这些代码有助于 对 yield from 结构的运作方式有个整体的了解。
就在示例 16-19 所列伪代码的顶部,有行代码(标号❷)揭示了一个重 要的细节:要预激子生成器。 这表明,用于自动预激的装饰器(如 16.4 节定义的那个)与 yield from 结构不兼容。
仔细研究扩充的伪代码可能没什么用——这与你的学习方式有关。显 然,分析真正使用 yield from 结构的代码要比深入研究实现这一结构 的伪代码更有好处。不过,我见过的 yield from 示例几乎都使用 asyncio 模块做异步编程,因此要有有效的事件循环才能运行。第 18 章会多次用到 yield from 结构。16.11 节中有几个链接,指向使用 yield from 结构的一些有趣代码,而且无需事件循环。
下面分析一个使用协程的经典案例:仿真编程。这个案例没有展示 yield from 结构的用法,但是揭示了如何使用协程在单个线程中管理 并发活动。

