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 的代码段不能像其他代码一样被分解并放入单独的函数中。执行这种分解会导致被调用的函数本身成为一个生成器,并且有必要显式迭代这个第二个生成器并重新让出它产生的任何值。
如果只关心值的让出,这可以通过使用以下循环轻松完成:
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 异常抛入委派生成器,则分解后的代码可能与未分解的代码行为不一致。
鉴于这些用例很少甚至不存在,不认为为了支持它们而增加额外的复杂性是值得的。
终结化
关于是否在委派生成器暂停在 yield from 处时,通过调用其 close() 方法来显式终结委派生成器也应该终结子迭代器,存在一些争论。反对这样做的论点是,如果对子迭代器的引用存在于其他地方,这将导致子迭代器过早终结。
考虑到非引用计数 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__() 调用和 yielded 值的开销可能导致 O(n) 操作在最坏情况下变成 O(n**2)。
一种可能的策略是为生成器对象添加一个槽,以保存正在委托的生成器。当对生成器进行 __next__() 或 send() 调用时,首先检查该槽,如果它不为空,则恢复它引用的生成器。如果它引发 StopIteration,则清除该槽并恢复主生成器。
这将把委托开销减少到一系列不涉及 Python 代码执行的 C 函数调用。一种可能的增强是遍历整个生成器链并直接恢复末尾的那个,尽管那样处理 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 循环写起来并不够麻烦,不足以证明一种新语法的合理性。通过处理完整的生成器协议,本提案提供了更多的益处。
附加材料
提供了一些关于所提议语法用法的示例,以及基于上述第一个优化的原型实现。
针对 Python 3.3 更新的实现版本可从追踪器 issue #11682 获取
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0380.rst
最后修改: 2025-02-01 08:59:27 GMT