文章目录
1. 摘要
提出了一种用于生成器的语法,用于将其部分操作委托给另一个生成器。这允许将包含 yield
的一段代码分解并放入另一个生成器中。此外,子生成器允许返回一个值,并且该值对委托生成器可用。
当一个生成器重新生成另一个生成器生成的值时,新的语法还为优化提供了一些机会。
2. 动力
Python 生成器是协程的一种形式,但是有一个限制,即它只能屈服于它的直接调用者。这意味着不能以与其他代码相同的方式将包含 yield
的代码段分解并放入单独的函数中。执行这样的分解会使被调用的函数本身成为生成器,有必要显式地遍历第二个生成器并重新生成它生成的任何值。
如果只关心值的生成,那么可以使用循环:
for v in g:
yield v
但是,如果子生成器要在 send()
、throw()
和 close()
调用的情况下正确地与调用者交互,事情就会变得相当困难。正如稍后将看到的,必要的代码非常复杂,正确处理所有的极端情况非常棘手。
一种新的语法将用来来解决这个问题。在最简单的用例中,它将等同于上面的 for
循环,但它还会处理生成器的所有行为,并允许以简单而直接的方式重构生成器代码。
3. 建议
在生成器的主体中将允许使用以下新的表达式语法:
yield from <expr>
其中,<expr>
是对可迭代对象求值的表达式,从可迭代对象中提取迭代器。这个迭代器会运行到耗尽为止,在此期间,它直接向生成器的调用者或从生成器的调用者(委托生成器)产出和接收值。
此外,当迭代器是另一个生成器时,允许子生成器使用一个值执行 return
语句(即 return value
),该值将成为 yield from
表达式的值。
yield from
表达式的完整语义可以用生成器协议描述如下:
- 迭代器生成的任何值都直接传递给调用者。
- 使用
send()
方法发送给委托生成器的任何值都将直接传递给迭代器。如果发送的值为 None,则调用迭代器的__next__()
方法。如果发送的值不是 None,则调用迭代器的send()
方法。如果调用引发 StopIteration,则恢复委托生成器。任何其他异常都将传播到委托生成器。 - 抛出到委托生成器中的 GeneratorExit 之外的异常将传递给迭代器的
throw()
方法。如果调用throw()
方法引发 StopIteration,则恢复委托生成器。任何其他异常都将传播到委托生成器。 - 如果将 GeneratorExit 异常抛出到委托生成器中,或者调用委托生成器的
close()
方法,则如果有迭代器的close()
方法,则调用迭代器的close()
方法。如果此调用导致异常,则将其传播到委托生成器。否则, 委托生成器将引发 GeneratorExit 异常。 yield from
的值是迭代器终止时抛出的 StopIteration 异常的第一个参数。- 生成器中的
return expr
将会在退出生成器时引发StopIteration(expr)
异常。
3.1 对于 StopIteration 的增强
为了方便起见,StopIteration 异常将被赋予一个 value
属性,该属性包含它的第一个参数,如果没有参数,则为 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
表达式允许将线程的逻辑扩展到所需的任意多个函数上,任何子函数中都会产生或消耗项,并且这些项会自动地 routed to or from 它们的最终源或目的地。
关于 throw()
和 close()
,可以合理地预期,如果一个异常从外部抛出到线程中,它应该首先在线程被挂起的最内层生成器中引发,然后从那里向外传播;并且,如果线程通过调用 close()
从外部终止,则活动生成器链应该从内部到外部完成。
4.4 语法
所提出的特定语法被选择为暗示其含义,同时不引入任何新的关键字,并且明显不同于普通的 yield
。
4.5 优化
当有很长的生成器链时,使用专门的语法为优化打开了可能。例如,当递归遍历树结构时,可能会出现这样的链。传递__next__()
调用和在链上下生成值的开销可能导致原本应该是 O(n)
的操作在最坏的情况下变成 O(n**2)
。
一种可能的策略是向生成器对象添加一个槽,以保存被委托给的生成器。当生成器上执行了一个 __next__()
或 send()
调用时,首先检查这个槽,如果它不是空的,则恢复它引用的生成器。如果引发 StopIteration,则清除插槽并恢复主生成器。
这将把委托开销减少到不涉及 Python 代码执行的 C 函数调用链。一个可能的改进是在一个循环中遍历整个生成器链,并直接恢复最后的生成器链,尽管 StopIteration 的处理要比这复杂得多。
4.6 使用 StopIteration 返回值
从生成器返回值的方法有很多种。一些替代方法包括将其存储为生成器-迭代器对象的属性,或者将其返回为子生成器的close()
调用的值。然而,提议的机制具有吸引力的原因有二:
- 使用 StopIteration 异常的泛化使得其他类型的迭代器可以很容易地参与到协议中,而不需要增加额外的属性或
close()
方法。 - 它简化了实现,因为子生成器返回值可用的时间点与引发异常的时间点相同。延迟到以后的任何时间都需要将返回值存储在某个地方。