文章目录
0. 引言
在阅读 PEP380 的内容之前,我们先来看一下《流畅的 Python》一书对 yield from
的简短介绍。
PEP 380 添加了 yield from
表达式,允许生成器将其部分操作委派给另一个生成器。这允许将包含 yield
的一段代码分解并放入另一个生成器中。此外,子生成器允许返回一个值,并且该值对委派生成器可用。
虽然主要用于委派给子生成器,但是 yield from
表达式实际上允许委派给任意子迭代器。
对于简单的迭代器:
yield from iterables
本质上只是
for item in iterable:
yield item
的缩写形式。
通过下面的例子你应该就能明白了:
>>> def chain1(*iterables):
... for it in iterables:
... for i in it:
... yield i
...
>>> s = 'ABC'
>>> t = range(3)
>>> list(chain1(s, t))
['A', 'B', 'C', 0, 1, 2]
>>> def chain2(*iterables):
... for it in iterables:
... yield from it
...
>>> list(chain2(s, t))
['A', 'B', 'C', 0, 1, 2]
可以看到,yield from it
完全替代了内层的 for
循环。不过,除了代替循环外,yield from
还会创建通道,把内层生成器直接与外层生成器的客户端联系起来。把生成器当成协程使用时,这个通道特别重要。yield from
允许子生成器直接从调用范围接收 发送 和 抛出 的值,并向外部生成器返回一个最终值:
>>> def accumulate():
... tally = 0
... while True:
... next = yield
... if next is None:
... return tally
... tally += next
...
>>> def gather_tallies(tallies):
... while True:
... tally = yield from accumulate()
... tallies.append(tally)
...
>>> tallies = []
>>> acc = gather_tallies(tallies)
>>> next(acc) # 确保累加器准备接收值
>>> for i in range(4):
... acc.send(i)
...
>>> acc.send(None) # 完成第一次记账
>>> for i in range(5):
... acc.send(i)
...
>>> acc.send(None) # 完成第二次记账
>>> tallies
[6, 10]
驱动这一变化的主要原则是允许将设计用于发送和抛出方法的生成器拆分为多个子生成器,就像将单个大函数拆分为多个子函数一样简单。
1. 摘要
提出了一种用于生成器的语法,用于将其部分操作委派给另一个生成器。这允许将包含 yield
的一段代码分解并放入另一个生成器中。此外,子生成器允许返回一个值,并且该值对委派生成器可用。
当一个生成器re-yield另一个生成器生产的值时,新的语法还为优化提供了一些机会。
2. 动力
Python 生成器是协程的一种形式,但是有一个限制,即它只能向它的直接调用者产出值。这意味着包含 yield
的代码段不能像其他代码一样分解并放入单独的函数中。执行这样的分解会导致被调用的函数本身成为生成器,因此有必要在第二个生成器上进行显式迭代,并re-yield其生产的任何值。
如果只考虑 yield 值,那么可以使用如下的形式:
for v in g:
yield v
但是,如果子生成器要在调用 send()
、throw()
和 close()
的情况下与调用者正确的交互,则事情将变得更加困难。正如稍后将看到的,必要的代码非常复杂,并且正确处理所有的极端情况非常棘手。
将提出一种新的语法来解决这个问题。在最简单的用例中,它等同于上面的 for
循环,但它还会处理生成器的所有行为,并允许以简单明了的方式重构生成器代码。
3. 建议
在生成器的主体中将允许使用以下新的表达式语法:
yield from <expr>
其中,<expr>
是从可迭代对象中提取迭代器的求值表达式。提取的迭代器会运行到耗尽为止,在此期间,它直接向包含 yield from
表达式的生成器(即委派生成器)的调用者产出和接收值。
此外,当迭代器是另一个生成器时,允许子生成器执行带有值的 return
语句(即 return value
),并且该值成为 yield from
表达式的值。
yield from
表达式的完整语义可以用生成器协议描述如下:
- 迭代器yield的任何值都直接传递给调用者。
- 使用
send()
方法发送给委派生成器的任何值都将直接传递给迭代器。如果发送的值为 None,则调用迭代器的__next__()
方法。如果发送的值不是 None,则调用迭代器的send()
方法。如果调用的方法引发 StopIteration,则恢复委派生成器。任何其他异常都将传播到委派生成器。 - 抛出到委派生成器中的 GeneratorExit 之外的其他异常将传递给迭代器的
throw()
方法。如果调用引发 StopIteration,则恢复委派生成器。任何其他异常都会传播到委派生成器。 - 如果将 GeneratorExit 异常抛出到委派生成器中,或者调用了委派生成器的
close()
方法(如果存在的话),则迭代器的close()
方法将被调用。如果此调用导致异常,则将其传播到委派生成器。否则,将在委派生成器中引发 GeneratorExit 异常。 yield from
表达式的值是迭代器终止时引发的 StopIteration 异常的第一个参数。- 生成器中的
return expr
导致生成器退出时引发StopIteration(expr)
。
3.1 对于 StopIteration 的增强
为了方便起见,StopIteration 异常将被赋予一个 value
属性,该属性包含创建 StopIteration 异常的第一个参数,如果没有参数,则为 None 。
3.2 正式的语义
3.2.1 “RESULT = yield from EXPR” 语句的语义
下面的语句:
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
3.2.2 生成器中 “return value” 的语义
在生成器中,语句:
return value
在语义上等价于:
raise StopIteration(value)
除了(与当前一样)该异常不能由返回的生成器中的 except
子句捕获。
3.2.3 StopIteration 异常
StopIteration 异常的行为类似于下面代码的逻辑:
class StopIteration(Exception):
def __init__(self, *args):
if len(args) > 0:
self.value = args[0]
else:
self.value = None
Exception.__init__(self, *args)
4. 原理
4.1 重构的原则
上面介绍的大多数语义背后的基本原理都源于能够重构生成器代码的渴望。应该可以将包含一个或多个 yield
表达式的代码段移动到一个单独的函数中(使用通常的技术来处理对周围范围内变量的引用,等等),然后使用 yield from
表达式调用新函数。
在合理可行的范围内,生成的复合生成器的行为应该与原始的未分解生成器在所有情况下的行为相同,包括调用__next__()
、send()
、throw()
和 close()
方法。
选择子迭代器而不是生成器的情况下的语义作为生成器情况的合理泛化。
提出的语义在重构方面存在以下限制:
- 捕获 GeneratorExit 而不随后重新引发它的代码块不能在保留完全相同的行为的同时被分解出来。
- 如果将 StopIteration 异常抛出到委派生成器中,则分解后的代码的行为可能与未分解的代码不同。
由于这些用例很少甚至不存在,因此不值得增加额外的复杂性逻辑以对它们进行支持。
4.2 生成器的终结
对于委派生成器在以 yield from
挂起时通过调用其 close()
方法显式地终结委派生成器时是否也应该终结子迭代器,存在一些争论。反对这样做的一个理由是,如果在其他地方存在对该子迭代器的引用,则会导致该子迭代器的过早终结。
考虑到非引用计数 Python 实现,我们决定应该执行这种显式的终结,以便显式地关闭一个 factored 生成器,其效果与在所有Python 实现中关闭一个 unfactored 生成器的效果相同。
所做的假设是,在大多数用例中,子迭代器不会被共享。共享子迭代器的罕见情况可以通过阻止 throw()
和 close()
调用的包装器或通过使用非 yield from
的方法来调用子迭代器来解决。
4.3 生成器作为线程
生成器能够返回值的动机与使用生成器实现轻量级线程有关。当以这种方式使用生成器时,将轻量级线程执行的计算分散到许多函数上是合理的。人们希望能够像调用普通函数一样调用子生成器,传递它的参数并接收返回值。
使用建议的语法,如:
y = f(x)
其中 f
是一个普通的函数,可以转换成委派调用.
y = yield from g(x)
其中,g
是一个生成器。通过将 g
视为可以使用 yield
语句挂起的普通函数,可以推断出结果代码的行为。
当以这种方式将生成器用作线程时,通常不会对传入 yield
或从 yield
传出的值感兴趣。然而,也有用于此目的的用例,其中线程被视为项的生产者或消费者。yield from
表达式允许将线程的逻辑散布在任意多个函数上,而在任何子函数中都会产生或消耗项,并且这些项会自动路由到其最终来源或目的地,或从其最终来源或目的地路由。
关于 throw()
和 close()
,可以合理地预期,如果一个异常从外部抛出到线程中,它应该首先在线程被挂起的最内层生成器中引发,然后从那里向外传播;并且,如果线程是通过调用 close()
从外部终止,则活动生成器的链应该从内部到外完成。
4.4 语法
所提出的特定语法被选择为暗示其含义,同时不引入任何新的关键字,并且明显不同于普通的 yield
。
4.5 优化
当生成器链很长时,使用专门的语法可进行优化。例如,当递归遍历树结构时,可能会出现这样的链。在链中上下传递 __next__()
调用和yield值的开销可能导致原本应该是 O(n)
的操作在最坏的情况下变成 O(n**2)
。
一种可能的策略是向生成器对象添加一个槽,以容纳要委派到的生成器。在生成器上执行了一个 __next__()
或 send()
调用时,首先检查这个槽,如果该槽为空,则改为恢复其引用的生成器。如果引发 StopIteration,则清除槽并恢复主生成器。
这将把委派开销减少到不涉及 Python 代码执行的 C 函数调用链。可能的改进方法是在循环遍历整个生成器链,并直接恢复最后的生成器链,尽管 StopIteration 的处理要复杂得多。
4.6 使用 StopIteration 返回值
从生成器返回值的方法有很多种。一些替代方法包括将其存储为生成器-迭代器对象的属性,或者将其返回为子生成器的close()
调用的值。然而,提议的机制具有吸引力的原因有两点:
- 使用 StopIteration 异常的泛化使得其他类型的迭代器可以很容易地参与到协议中,而不需要增加额外的属性或
close()
方法。 - 它简化了实现,因为子生成器返回值可用的时间点与引发异常的时间点相同。延迟到以后的任何时间都需要将返回值存储在某个地方。