PEP 380 – 向子生成器委派的语法
- 作者:
- Gregory Ewing <greg.ewing at canterbury.ac.nz>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2009 年 2 月 13 日
- Python 版本:
- 3.3
- 历史记录:
- 决议:
- Python-Dev 邮件
摘要
本提案为生成器委派其部分操作给另一个生成器提供了一种语法。这允许将包含“yield”的代码段提取出来,并放置到另一个生成器中。此外,子生成器可以返回值,并且该值可以提供给委派的生成器。
新的语法也为一个生成器重新生成另一个生成器生成的的值时进行优化提供了一些机会。
PEP 接受
Guido 于 2011 年 6 月 26 日正式 接受了 PEP。
动机
Python 生成器是一种协程形式,但它有一个局限性,即它只能向其直接调用者 yield。这意味着包含 yield
的代码片段不能像其他代码一样被提取出来并放入单独的函数中。执行这样的提取会导致被调用函数本身成为生成器,并且有必要显式地迭代这个第二个生成器,并重新生成它生成的任何值。
如果只关心值的生成,可以使用循环来执行,例如
for v in g:
yield v
但是,如果子生成器要与调用者在调用 send()
、throw()
和 close()
的情况下正确交互,那么事情就会变得更加复杂。正如稍后将看到的那样,必要的代码非常复杂,并且很难正确处理所有边缘情况。
将提出一个新的语法来解决这个问题。在最简单的用例中,它将等效于上述 for 循环,但它还将处理生成器的全部行为范围,并允许以简单直观的方式对生成器代码进行重构。
提议
以下新的表达式语法将在生成器主体中允许
yield from <expr>
其中 <expr> 是一个表达式,它计算为一个可迭代对象,从中提取一个迭代器。该迭代器被运行到耗尽,在此期间,它直接向包含 yield from
表达式的生成器(“委派生成器”)的调用者生成和接收值。
此外,当迭代器是另一个生成器时,子生成器被允许执行带有值的 return
语句,并且该值成为 yield from
表达式的值。
可以根据生成器协议描述 yield from
表达式的完整语义,如下所示
- 迭代器生成的任何值都直接传递给调用者。
- 使用
send()
发送给委派生成器的任何值都直接传递给迭代器。如果发送的值为 None,则调用迭代器的__next__()
方法。如果发送的值不为 None,则调用迭代器的send()
方法。如果调用引发 StopIteration,则恢复委派生成器。任何其他异常都将传播到委派生成器。 - 除了 GeneratorExit 之外的任何其他异常都将被抛入委派生成器,并将传递给迭代器的
throw()
方法。如果调用引发 StopIteration,则恢复委派生成器。任何其他异常都将传播到委派生成器。 - 如果 GeneratorExit 异常被抛入委派生成器,或者调用委派生成器的
close()
方法,那么如果迭代器具有close()
方法,则调用该方法。如果此调用导致异常,则将异常传播到委派生成器。否则,将在委派生成器中引发 GeneratorExit。 yield from
表达式的值是迭代器终止时引发的StopIteration
异常的第一个参数。return expr
在生成器中会导致StopIteration(expr)
在退出生成器时被引发。
对 StopIteration 的增强
为了方便起见,StopIteration
异常将被赋予一个 value
属性,该属性保存其第一个参数,或者如果没有参数,则为 None。
正式语义
本节使用 Python 3 语法。
- 语句
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
- 在生成器中,语句
return value
在语义上等效于
raise StopIteration(value)
除了像现在一样,异常不能被返回生成器内的
except
子句捕获。 - StopIteration 异常的行为类似于以下定义
class StopIteration(Exception): def __init__(self, *args): if len(args) > 0: self.value = args[0] else: self.value = None Exception.__init__(self, *args)
基本原理
重构原则
上述大多数语义背后的基本原理源于能够重构生成器代码的愿望。应该能够将包含一个或多个 yield
表达式的代码段移动到一个单独的函数中(使用通常的技术来处理对周围范围中变量的引用等),并使用 yield from
表达式调用新的函数。
生成的复合生成器的行为应该在所有情况下(包括对 __next__()
、send()
、throw()
和 close()
的调用)与原始未分解生成器尽可能相同。
子迭代器不为生成器时的语义被选为生成器情况的合理概括。
提出的语义在重构方面具有以下限制
- 捕获 GeneratorExit 但随后不重新引发的代码块不能在保留完全相同行为的情况下被提取出来。
- 如果 StopIteration 异常被抛入委派生成器,则提取的代码可能不会像未提取的代码一样表现。
由于这些用例非常少见,甚至不存在,因此被认为不值得为此付出支持它们所需的额外复杂性。
终结
关于是否应通过调用其 close()
方法显式地终结委派生成器,而它在 yield from
处暂停,也应终结子迭代器,存在一些争论。反对这样做的理由是,如果子迭代器的引用存在于其他地方,这会导致子迭代器过早终结。
对非引用计数 Python 实现的考虑导致决定应该执行这种显式终结,以便在所有 Python 实现中,显式地关闭分解生成器与对未分解生成器执行相同的操作具有相同的效果。
所做的假设是,在大多数用例中,子迭代器不会被共享。共享子迭代器的罕见情况可以通过使用阻止 throw()
和 close()
调用的包装器来解决,或者通过使用除 yield from
之外的其他方式来调用子迭代器。
生成器作为线程
生成器能够返回值的动机涉及使用生成器来实现轻量级线程。当以这种方式使用生成器时,希望能够像调用普通函数一样调用子生成器,传递参数并接收返回值。
使用提出的语法,语句
y = f(x)
其中 f 是一个普通函数,可以转换为委托调用
y = yield from g(x)
其中 g 是一个生成器。可以通过将 g 视为可以使用 yield
语句暂停的普通函数来推断所得代码的行为。
当以这种方式使用生成器作为线程时,通常不关心通过 yield 传入或传出的值。但是,也有一些用例,其中线程被视为项目生产者或消费者。 yield from
表达式允许线程逻辑分布在尽可能多的函数中,项目生成或消费发生在任何子函数中,并且项目会自动路由到或来自其最终源或目的地。
关于 throw()
和 close()
,合理预期的是,如果从外部抛出异常到线程中,则应首先在线程暂停的最内层生成器中引发该异常,并从那里向外传播;并且如果线程通过调用 close()
从外部终止,则应从最内层向外终结活动生成器的链。
语法
提出的特定语法被选为暗示其含义,同时没有引入任何新的关键字,并且明显区别于普通的 yield
。
优化
使用专门的语法可以为长链生成器打开优化可能性。例如,当递归遍历树结构时,可能会出现此类链。传递 __next__()
调用和生成的值在链中上下传递的开销会导致本应为 O(n) 的操作在最坏情况下变为 O(n**2)。
一种可能的策略是在生成器对象中添加一个插槽来保存委托给它的生成器。当对生成器进行 __next__()
或 send()
调用时,首先检查此插槽,如果它非空,则继续引用该插槽的生成器。如果它引发 StopIteration,则清除插槽并继续主生成器。
这将把委托开销减少到一系列 C 函数调用,其中不涉及任何 Python 代码执行。一种可能的增强是遍历整个生成器链,并在循环中直接继续最后的生成器,尽管处理 StopIteration 会更加复杂。
使用 StopIteration 返回值
从生成器返回的值可以以多种方式传递回来。一些替代方案包括将其存储为生成器-迭代器对象的属性,或将其作为对子生成器进行 close()
调用的返回值。但是,所提出的机制具有以下几个原因吸引人
- 使用 StopIteration 异常的泛化使得其他类型的迭代器可以轻松地参与协议,而无需增长额外的属性或 close() 方法。
- 它简化了实现,因为子生成器返回的值可用的点与引发异常的点相同。延迟到任何更晚的时间将需要将返回值存储在某个地方。
被拒绝的想法
讨论了一些想法,但被拒绝了。
建议:应该有一种方法来防止对 __next__() 的初始调用,或用带有指定值的 send() 调用来代替它,目的是支持使用包装的生成器,以便自动执行初始 __next__()。
决议:不在本提案的范围内。此类生成器不应与 yield from
结合使用。
建议:如果关闭子迭代器引发带有值的 StopIteration,则从对委托生成器的 close()
调用中返回该值。
此功能的动机是,可以通过关闭生成器来向生成器发送的一系列值的结束信号。生成器将捕获 GeneratorExit,完成其计算并返回结果,该结果将成为 close() 调用的返回值。
决议:这种对 close() 和 GeneratorExit 的使用将与其当前作为退出和清理机制的作用不兼容。它将要求在关闭委托生成器后,在关闭子生成器后,继续委托生成器,而不是重新引发 GeneratorExit。但这不可接受,因为它不能确保在出于清理目的调用 close() 时委托生成器能够正确完成。
向消费者发送值结束的信号最好通过其他方法来解决,例如发送哨兵值或抛出生产者和消费者商定的异常。然后,消费者可以检测哨兵或异常,并通过完成其计算并正常返回来做出响应。这种方案在存在委托的情况下能正确运行。
建议:如果 close()
不返回值,则如果出现带有非 None 值的 StopIteration,则引发异常。
决议:没有明确的理由这样做。在 Python 中的任何其他地方,忽略返回值都不被认为是错误。
批评
根据此提案,yield from
表达式的值将以与普通 yield
表达式完全不同的方式派生。这表明一些不包含单词 yield
的其他语法可能更合适,但目前还没有提出任何可接受的替代方案。被拒绝的替代方案包括 call
、delegate
和 gcall
。
有人建议应该使用除子生成器中的 return
之外的某种机制来确定 yield from
表达式返回的值。但是,这将干扰将子生成器视为可挂起的函数的目标,因为它将无法像其他函数一样返回值。
使用异常传递返回值已被批评为“滥用异常”,但没有任何具体的理由来证明这种说法。无论如何,这只是一个建议的实现;可以使用其他机制,而不会丢失提案的任何基本功能。
有人建议应该使用不同的异常(例如 GeneratorReturn)而不是 StopIteration 来返回值。但是,还没有提出令人信服的实际理由,而且在 StopIteration 中添加 value
属性可以缓解从可能或不可能有值的 StopIteration 异常中提取返回值的任何困难。此外,使用不同的异常将意味着,与普通函数不同,“在生成器中不带值的 return”将不等于“return None”。
备选提案
以前也有人提出过类似的提案,有些使用 yield *
语法而不是 yield from
。虽然 yield *
更简洁,但可以说它看起来太像普通的 yield
,在阅读代码时可能会忽略差异。
据作者所知,以前的提案只关注生成值,因此受到批评,即它们替换的两行 for 循环写起来并不足够繁琐,不足以证明使用新的语法。通过处理完整的生成器协议,此提案提供了更多益处。
附加材料
提供了一些使用所提语法示例,以及基于上面概述的第一个优化的原型实现。
可以在跟踪器 问题 #11682 中找到针对 Python 3.3 更新的实现版本。
版权
本文档已进入公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0380.rst