查看原文
其他

[PEP 380] 子生成器的语法

The following article is from Python猫 Author 豌豆花下猫

(给Python开发者加星标,提升Python技能


作者:豌豆花下猫 (本文来自作者投稿)

摘要

为生成器提出了一种新的语法,用于将部分的操作委派给其它的生成器。这使得一部分包含“yield”的代码段,可以被分离并放置到其它生成器中。与此同时,子生成器会返回一个值,交给委派生成器(delegating generator)使用。

当一个生成器再次 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语法。

1、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

2、在生成器中,return value 语句在语义上等同于raise StopIteration(value) ,除了一点,当前返回的生成器中的 except 子句无法捕获该异常。

3、 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() 。

子迭代器(而非生成器)的语义被选择成为生成器案例的合理泛化(generalization)。

所提出的语义在重构方面具有如下限制:

  • 一个捕获了 GenetatorExit 却不重新抛出的代码块,不能在完全保留相同行为的情况下被分离出去。

  • 如果将 StopIteration 异常抛进了委托生成器中,则分离的生成器的行为跟原始代码的行为可能会不同。

由于这些用例几乎不存在,因此不值得为支持它们而考虑额外的复杂性。

结束方式

当在 yield from 处挂起时,并且使用 close() 方法显式地终止委托生成器时,关于是否要一并终止子迭代器,存在一些争议。一个反对的论据是,如果在别处存在对子迭代器的引用,这样做会导致过早结束它。

对非引用计数型的 Python 实现的考虑,导致了应该显式地结束的结论,以便在所有类型的 Python 实现上,显式地结束子迭代器与非重构的迭代器,能具有相同的效果。

这里做的假设是,在大多数用例中,子迭代器不会被共享。在子迭代器被共享的稀有情况下,可通过一个阻塞调用 throw() 和 close() 的装饰器来实现,或者使用除 yield from 以外的方法来调用子迭代器。

作为线程的生成器

使生成器能够 return 值的动机,还考虑到使用生成器来实现轻量级的线程。当以这种方式使用生成器时,将轻量级线程的计算扩散到许多函数上就会是合理的。人们希望能够像调用普通函数一样调用子生成器,传递给它参数并接收返回值。

使用提议的语法,像以下的表达式

y = f(x)

其中 f 是一个普通的函数,就可以被转化成一个委托调用

y = yield from g(x)

其中 g 是生成器。通过把 g 想象成一个普通的能被 yield 语句挂起的函数,人们可以推断出结果代码的行为。

当以这种方式把生成器作为线程使用时,通常人们不会对 yield 所传入或传出的值感兴趣。但是,也有一些例子,线程可以作为 item 的生产者或消费者。yield from 表达式允许线程的逻辑被扩散到所需的尽可能多的函数中,item 的生产与消费发生在任意的子函数中,并且这些 item 会自动路由到/去它们的最终来源/目的地。

对于 throw() 与 close() ,可以合理地预期,如果从外部向线程内抛入了一个异常,那么首先应该在线程挂起处的最内部的生成器中引发,再从那里向外传递;而如果线程是从外部调用 close() 来终结的,那也应该从最内部往外地终止处于活动态的生成器链。

语法

所提出的特定语法被选中,像它的含义所暗示,并没有引入任何新的关键词,且清晰地突出了它与普通 yield 的不同。

优化

当存在一长串生成器时,使用专门的语法就为优化提供了可能性。这种生成器链可能存在,例如,当递归遍历树结构时。在链上传递 __next__() 的调用与 yield 返回值,可能造成 O(n) 开销,最坏情况下会是 O(n**2)。

可能的策略是向生成器对象添加一个槽(slot)来保存委派给它的生成器。当在生成器上调用 __next__() 或 send() 时,首先检查该槽,如果非空,则它引用的生成器将会被激活。如果引发了 StopIteration,该槽会被清空,并且主生成器会被激活。

这将减少一系列 C 函数调用的委托开销,并不涉及 Python 代码的执行。一种可能的增强方法是在循环中遍历整个生成器链,并直接激活最后一个生成器,尽管 StopIteration 的处理会比较复杂。

使用StopIteration来返回值

有多种方法可以将生成器的返回值传回。也有一些替代的方法,例如将其存储为生成器-迭代器对象的属性,或将其作为子生成器的 close() 方法的调用值返回。然而,本 PEP 提议的机制很有吸引力,有如下理由:

  • 使用泛化的 StopIteration 异常,可以使其它类型的迭代器轻松地加入协议,而不必增加额外的属性或 close() 方法。

  • 它简化了实现,因为子生成器的返回值变得可用的点与引发异常的点相同。延迟到任意时间都需要在某处存储返回值。

被拒绝的建议

一些想法被讨论并且拒绝了。

建议:应该有一些方法可以避免对__next__() 的调用,或者用带有指定值的 send() 调用来替换它,目的是支持对生成器作装饰,以便可以自动地执行初始的 __next__() 。

决议:超出本提案的范围。这种生成器不该与 yield from 一起使用。

建议:如果关闭一个子迭代器时,引发了带返回值的 StopIteration 异常,则将该值从 close() 调用中返回给委托生成器。

此功能的动机是为了通过关闭生成器,传信号给传入生成器的最后的值。被关闭的生成器会捕获 GeneratorExit ,完成其计算并返回一个结果,该结果最终成为 close() 调用的返回值。

决议:close() 与 GeneratorExit 的这种用法,将与当前的退出(bail-out)与清理机制的角色不兼容。这要求在关闭子生成器后、关闭一个委托生成器时,该委托生成器可以被恢复,而不是重新引发 GeneratorExit。但这是不可接受的,因为调用 close() 进行清理的意图,无法保证委托生成器能正确地终止。

通过其它方式,可以更好地处理向消费者告知(signal)最后的值的问题,例如发送一个哨兵值(sentinel value)或者抛入一个被生产者与消费者都认可的异常。然后,消费者可以检查该哨兵或异常,通过完成其计算并正常地返回,来作响应。这种方案在存在委托的情况下表现正确。

建议:如果 close() 不返回值,如果出现 StopIteration 中带有非 None 的值,则抛出一个异常。

决议:没有明确的理由如此做。忽略返回值在 Python 中的任何其它地方,都不会被视为错误。

批评

根据本提案,yield from 表达式的值将以跟普通 yield 表达式非常不同的方式得出。这意味着其它不包含 yield 表达式的语法可能会更合适,但到目前为止,还没有提出可接受的替代方案。被拒绝的替代品包括 call、delegate 和 gcall。

有人提议,应该使用子生成器中除 return 以外的某些机制,来处理 yield from 表达式的返回值。但是,这会干扰将子生成器视为可挂起函数的目的,因为它不能像其它函数一样 return 值。

有人批评,说使用异常来传递返回值是“滥用异常”,却没有任何具体的理由来证明它。无论如何,这只是一种实现的建议;其它机制可以在不丢失本提案的任何关键特性的情况下使用。

有人建议,使用与 StopIteration 不同的异常来返回值,例如 GeneratorReturn。但是,还没有令人信服的实际理由被提出,并且向 StopIteration 添加 value 属性减轻了从异常(该异常可能存在也可能不存在)中提取返回值的所有困难。此外,使用不同的异常意味着,与普通函数不同,生成器中不带值的 return,将不等同于 return None 。

可选的提案

之前已经提到了类似的提议,有些语法使用 yield * 而不是 yield from。虽然 yield * 更简洁,但是有争议的是,它看起来与普通的 yield 太相似了,可能在阅读代码时会忽视了其中的差异。

据作者所知,之前的提案只关注于 yield 产生值,因此遭受到了批评,即他们所替代的两行 for 循环并没有足够令人厌烦,不足以让人为新的语法辩护。通过处理完整的生成器协议,本提案提供了更多的好处。

附加材料

本提案的语法的一些用例已经被提供出来,并且基于上面概括的第一个优化的原型也已实现。

Examples and Implementation

可以从跟踪器问题的 issue 11682 中获得针对 Python 3.3 实现的升级版本。

参考资料

https://mail.python.org/pipermail/python-dev/2011-June/112010.html

http://www.cosc.canterbury.ac.nz/greg.ewing/python/yield-from/

http://bugs.python.org/issue11682

版权

本文档已经放置在公共领域。源文档:

https://github.com/python/peps/blob/master/pep-0380.txt


【本文作者】


豌豆花下猫:某985高校毕业生, 兼具极客思维与人文情怀 。个人公众号Python猫, 专注python技术、数据科学和深度学习。



推荐阅读

(点击标题可跳转阅读)

PEP 255 :简单的生成器

学习Python,怎能不懂点PEP呢?

[PEP 342] 增强型生成器:协程



觉得本文对你有帮助?请分享给更多人

关注「Python开发者」加星标,提升Python技能

喜欢就点一下「好看」呗~

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存