yield from的意义制定 PEP 380 时,有人质疑作者 Greg Ewing 提议的语义过于复杂了。他的回应之一是:“对人类来说,几乎所有最重要的信息都在靠近顶部的某个段落里。”他还引述了 PEP 380 草稿中的一段话,当时那段话是这样的:
“把迭代器当作生成器使用,相当于把子生成器的定义体内联在
yield from表达式中。此外,子生成器可以执行return语句,返回一个值,而返回的值会成为yield from表达式的值。”8
8摘自 Python-Dev 邮件列表中的一个消息:“PEP 380 (yield from a subgenerator) comments”(发布于 2009 年 3 月 21 日,https://mail.python.org/pipermail/python-dev/2009-March/087385.html)。
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 = yield from 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:
while 1: ➍
_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 = yield from 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(...) 方法传入的异常。同样,子生成器可以是迭代器,从而没有 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 所列伪代码的顶部,有行代码(标号❷)揭示了一个重要的细节:要预激子生成器。9 这表明,用于自动预激的装饰器(如 16.4 节定义的那个)与 yield from 结构不兼容。
9Nick Coghlan 于 2009 年 4 月 5 日在 Python-ideas 邮件列表中发布的一个消息(https://mail.python.org/pipermail/python-ideas/2009-April/003954.html)中质疑,yield from 结构隐式预激是不是好主意。
在本节开头引用的那个消息中(https://mail.python.org/pipermail/python-dev/2009-March/087385.html),关于扩充 yield from 结构的伪代码,Greg Ewing 说:
我不是让你通过扩充的伪代码学习这个结构,那段伪代码是为了让语言专家弄明白细节。
仔细研究扩充的伪代码可能没什么用——这与你的学习方式有关。显然,分析真正使用 yield from 结构的代码要比深入研究实现这一结构的伪代码更有好处。不过,我见过的 yield from 示例几乎都使用 asyncio 模块做异步编程,因此要有有效的事件循环才能运行。第 18 章会多次用到 yield from 结构。16.11 节中有几个链接,指向使用 yield from 结构的一些有趣代码,而且无需事件循环。
下面分析一个使用协程的经典案例:仿真编程。这个案例没有展示 yield from 结构的用法,但是揭示了如何使用协程在单个线程中管理并发活动。