PEP 255 – 简单生成器
- 作者:
- Neil Schemenauer <nas at arctrix.com>,Tim Peters <tim.peters at gmail.com>,Magnus Lie Hetland <magnus at hetland.org>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 需要:
- 234
- 创建:
- 2001年5月18日
- Python 版本:
- 2.2
- 发布历史:
- 2001年6月14日,2001年6月23日
摘要
本 PEP 引入了 Python 中生成器的概念,以及与之结合使用的新语句,即yield
语句。
动机
当生产者函数的工作足够复杂,需要在生成的值之间维护状态时,大多数编程语言除了在生产者的参数列表中添加回调函数(在生成每个值时调用)之外,没有提供令人愉悦且高效的解决方案。
例如,标准库中的tokenize.py
采用了这种方法:调用者必须将一个tokeneater函数传递给tokenize()
,并在tokenize()
找到下一个标记时调用它。这使得 tokenize 可以以自然的方式编写,但调用 tokenize 的程序通常会因需要记住回调之间看到哪个标记而变得复杂。 tabnanny.py
中的tokeneater函数就是一个很好的例子,它在全局变量中维护一个状态机,以记住跨回调它已经看到的内容以及它希望接下来看到的内容。这很难使其正常工作,并且人们仍然难以理解。不幸的是,这种方法通常是这样的。
另一种方法是让 tokenize 同时生成 Python 程序的完整解析结果,并将其存储在一个大型列表中。然后,tokenize 客户端可以以自然的方式编写,使用局部变量和局部控制流(例如循环和嵌套的 if 语句)来跟踪其状态。但这不实用:程序可能非常大,因此无法对具体化整个解析所需的内存设置先验界限;并且一些 tokenize 客户端只需要查看程序开头是否出现某些特定内容(例如,future 语句,或者,如 IDLE 中所做的那样,仅查看第一个缩进语句),然后先解析整个程序是一种严重的浪费时间。
另一种方法是将 tokenize 变成一个迭代器,在其.next()
方法被调用时返回下一个标记。这与大型结果列表一样,对调用者来说很方便,但没有内存和“如果我想提前退出怎么办?”的缺点。但是,这将负担转移到 tokenize 上,要求其在.next()
调用之间记住其状态,读者只需要浏览tokenize.tokenize_loop()
就可以意识到这是一件多么可怕的任务。或者想象一个递归算法来生成一般树结构的节点:要将其转换为迭代器框架,需要手动删除递归并手动维护遍历的状态。
第四种选择是在单独的线程中运行生产者和消费者。这允许两者都以自然的方式维护其状态,因此对两者来说都很方便。事实上,Python 源代码分发版中的 Demo/threads/Generator.py 提供了一个可用的同步通信类,以一般的方式执行此操作。但是,这在没有线程的平台上不起作用,并且在有线程的平台上速度非常慢(与无需线程所能实现的速度相比)。
最后一种选择是使用 Stackless [1](PEP 219)Python 的变体实现,它支持轻量级协程。这与线程选项具有相同的程序化优势,但效率更高。但是,Stackless 对 Python 核心进行了有争议的重新思考,Jython 可能无法实现相同的语义。本 PEP 不是讨论此问题的地方,因此在这里仅需说明,生成器以一种易于融入当前 CPython 实现的方式提供了 Stackless 功能的一个有用子集,并且据信对于其他 Python 实现来说也相对简单。
这涵盖了当前的替代方案。一些其他高级语言提供了令人愉悦的解决方案,特别是 Sather [2] 中的迭代器,其灵感来自 CLU 中的迭代器;以及 Icon [3] 中的生成器,这是一种新颖的语言,其中每个表达式都是一个生成器。它们之间存在差异,但基本思想相同:提供一种函数,可以将其中间结果(“下一个值”)返回给调用者,但同时维护函数的局部状态,以便函数可以从中断的地方继续执行。一个非常简单的例子
def fib():
a, b = 0, 1
while 1:
yield b
a, b = b, a+b
当第一次调用fib()
时,它将a设置为 0,将b设置为 1,然后将b生成回其调用者。调用者看到 1。当fib
恢复时,从其角度来看,yield
语句实际上与print
语句相同:fib
在 yield 之后继续执行,所有局部状态都保持不变。然后a和b变为 1 和 1,fib
循环回yield
,将其生成回调用者。依此类推。从fib
的角度来看,它只是传递一系列结果,就像通过回调一样。但从其调用者的角度来看,fib
调用是一个可迭代对象,可以随意恢复。与线程方法一样,这允许双方以最自然的方式进行编码;但与线程方法不同的是,这可以高效地在所有平台上完成。实际上,恢复生成器应该不比函数调用更昂贵。
相同的方法适用于许多生产者/消费者函数。例如,tokenize.py
可以生成下一个标记,而不是使用它作为参数来调用回调函数,并且 tokenize 客户端可以以自然的方式迭代标记:Python 生成器是一种 Python 迭代器,但它是一种特别强大的迭代器。
规范:Yield
引入了一个新语句
yield_stmt: "yield" expression_list
yield
是一个新关键字,因此需要使用future
语句(PEP 236)来逐步引入它:在初始版本中,希望使用生成器的模块必须包含以下代码行
from __future__ import generators
在顶部附近(有关详细信息,请参阅PEP 236)。在没有future
语句的情况下使用标识符yield
的模块将触发警告。在下一个版本中,yield
将成为语言关键字,并且不再需要future
语句。
yield
语句只能在函数内部使用。包含yield
语句的函数称为生成器函数。生成器函数在所有方面都是普通的函数对象,但在代码对象的 co_flags 成员中设置了新的CO_GENERATOR
标志。
当调用生成器函数时,实际参数以通常的方式绑定到函数局部形式参数名称,但不会执行函数体中的任何代码。相反,将返回一个生成器迭代器对象;这符合迭代器协议,因此尤其可以以自然的方式在 for 循环中使用。请注意,当上下文清楚地表明意图时,未限定名称“生成器”可以用来指代生成器函数或生成器迭代器。
每次调用生成器迭代器的.next()
方法时,都会执行生成器函数体中的代码,直到遇到yield
或return
语句(见下文),或者直到到达主体末尾。
如果遇到yield
语句,则函数的状态将被冻结,并且expression_list的值将返回给.next()
的调用者。所谓“冻结”,是指所有局部状态都将保留,包括局部变量的当前绑定、指令指针和内部评估堆栈:保存了足够的信息,以便下次调用.next()
时,函数可以像yield
语句只是另一个外部调用一样继续执行。
限制:不允许在 try/finally
结构的 try
子句中使用 yield
语句。困难在于无法保证生成器会被恢复,因此也无法保证 finally
代码块会被执行;这严重违反了 finally
的目的,无法容忍。
限制:生成器在正在运行时不能被恢复。
>>> def g():
... i = me.next()
... yield i
>>> me = g()
>>> me.next()
Traceback (most recent call last):
...
File "<string>", line 2, in g
ValueError: generator already executing
规范:Return
生成器函数也可以包含以下形式的 return
语句:
return
请注意,在生成器主体中的 return
语句不允许使用 expression_list(尽管当然它们可能出现在嵌套在生成器中的非生成器函数的主体中)。
当遇到 return
语句时,控制流程就像在任何函数返回时一样,执行相应的 finally
子句(如果存在)。然后,引发 StopIteration
异常,表示迭代器已耗尽。如果控制流在生成器末尾结束且没有显式 return
,也会引发 StopIteration
异常。
请注意,对于生成器函数和非生成器函数,return
的含义都是“我已完成,并且没有有趣的值要返回”。
请注意,return
并不总是等同于引发 StopIteration
:区别在于如何处理包含的 try/except
结构。例如,
>>> def f1():
... try:
... return
... except:
... yield 1
>>> print list(f1())
[]
因为,就像在任何函数中一样,return
只是退出,但是
>>> def f2():
... try:
... raise StopIteration
... except:
... yield 42
>>> print list(f2())
[42]
因为 StopIteration
被裸 except
捕获,就像任何异常一样。
规范:生成器和异常传播
如果生成器函数引发或传递了未处理的异常(包括但不限于 StopIteration
),则该异常将以通常的方式传递给调用方,并且随后尝试恢复生成器函数将引发 StopIteration
。换句话说,未处理的异常会终止生成器的有效生命周期。
示例(不是惯用的,但为了说明这一点)
>>> def f():
... return 1/0
>>> def g():
... yield f() # the zero division exception propagates
... yield 42 # and we'll never get here
>>> k = g()
>>> k.next()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 2, in g
File "<stdin>", line 2, in f
ZeroDivisionError: integer division or modulo by zero
>>> k.next() # and the generator cannot be resumed
Traceback (most recent call last):
File "<stdin>", line 1, in ?
StopIteration
>>>
规范:Try/Except/Finally
如前所述,不允许在 try/finally
结构的 try
子句中使用 yield
。结果是,生成器应该非常小心地分配关键资源。对于 yield
出现在 finally
子句、except
子句或 try/except
结构的 try
子句中,没有限制。
>>> def f():
... try:
... yield 1
... try:
... yield 2
... 1/0
... yield 3 # never get here
... except ZeroDivisionError:
... yield 4
... yield 5
... raise
... except:
... yield 6
... yield 7 # the "raise" above stops this
... except:
... yield 8
... yield 9
... try:
... x = 12
... finally:
... yield 10
... yield 11
>>> print list(f())
[1, 2, 4, 5, 8, 9, 10, 11]
>>>
示例
# A binary tree class.
class Tree:
def __init__(self, label, left=None, right=None):
self.label = label
self.left = left
self.right = right
def __repr__(self, level=0, indent=" "):
s = level*indent + `self.label`
if self.left:
s = s + "\n" + self.left.__repr__(level+1, indent)
if self.right:
s = s + "\n" + self.right.__repr__(level+1, indent)
return s
def __iter__(self):
return inorder(self)
# Create a Tree from a list.
def tree(list):
n = len(list)
if n == 0:
return []
i = n / 2
return Tree(list[i], tree(list[:i]), tree(list[i+1:]))
# A recursive generator that generates Tree labels in in-order.
def inorder(t):
if t:
for x in inorder(t.left):
yield x
yield t.label
for x in inorder(t.right):
yield x
# Show it off: create a tree.
t = tree("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
# Print the nodes of the tree in in-order.
for x in t:
print x,
print
# A non-recursive generator.
def inorder(node):
stack = []
while node:
while node.left:
stack.append(node)
node = node.left
yield node.label
while not node.right:
try:
node = stack.pop()
except IndexError:
return
yield node.label
node = node.right
# Exercise the non-recursive generator.
for x in t:
print x,
print
两个输出块都显示
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
问答
为什么不使用新关键字,而是重用def
?
请参阅下面的 BDFL 声明部分。
为什么为yield
使用新关键字?为什么不使用内置函数代替?
控制流在 Python 中可以通过关键字更好地表达,而 yield
是一种控制结构。人们还认为,Jython 中的有效实现需要编译器能够在编译时确定潜在的挂起点,而新的关键字使这变得很容易。CPython 参考实现也大量利用它来检测哪些函数是生成器函数(尽管用一个新的关键字代替 def
可以解决 CPython 的问题——但那些询问“为什么需要一个新关键字?”的人不想有任何新的关键字)。
那么为什么不用其他特殊语法,而要使用新关键字呢?
例如,可以使用以下其中一个代替 yield 3
return 3 and continue
return and continue 3
return generating 3
continue return 3
return >> , 3
from generator return 3
return >> 3
return << 3
>> 3
<< 3
* 3
我是否错过了其中一个 <wink>?在数百条消息中,我统计了三条建议使用这种替代方案的消息,并从中提取了上述内容。最好是不需要新的关键字,但更重要的是使 yield
非常清晰——我不想不得不通过理解之前毫无意义的关键字或运算符序列来推断正在发生 yield
。尽管如此,如果这能吸引足够的兴趣,支持者应该达成一个共识建议,然后 Guido 将对此进行宣布。
为什么允许return
?为什么不强制使用raise StopIteration
来表示终止?
StopIteration
的机制是底层细节,很像 Python 2.1 中 IndexError
的机制:实现需要在幕后执行一些定义明确的操作,Python 将这些机制公开给高级用户。但这并不是强迫每个人都以这种级别工作的理由。return
在任何类型的函数中都表示“我已完成”,这很容易解释和使用。请注意,在 try/except
结构中,return
并不总是等同于 raise StopIteration
(请参阅“规范:返回”部分)。
那么为什么不允许在return
中使用表达式呢?
也许我们总有一天会这样做。在 Icon 中,return expr
既表示“我已完成”,也表示“但我还有一个最终的有用值要返回,这就是它”。在开始时,并且在没有 return expr
的令人信服的用法的状态下,仅使用 yield
来传递值更简洁。
BDFL 声明
问题
在 def
的位置引入另一个新关键字(例如,gen
或 generator
),或以其他方式更改语法,以区分生成器函数和非生成器函数。
反对意见
在实践中(你如何思考它们),生成器是函数,但有一个变化,它们是可以恢复的。它们设置方式的机制是一个相对较小的技术问题,引入一个新的关键字会无助地过分强调生成器如何启动的机制(生成器生命周期中至关重要但很小的一部分)。
支持意见
在现实中(你如何思考它们),生成器函数实际上是工厂函数,它们就像变魔术一样生成生成器迭代器。在这方面,它们与非生成器函数有根本的不同,更像是一个构造函数而不是一个函数,因此重用 def
最好说是令人困惑的。隐藏在主体中的 yield
语句不足以警告语义如此不同。
BDFL
def
保持不变。任何一方的论点都没有完全令人信服,因此我咨询了我的语言设计者的直觉。它告诉我,PEP 中提出的语法是恰到好处的——不温不火。但是,就像希腊神话中的德尔斐神谕一样,它没有告诉我为什么,所以我没有对反对 PEP 语法的论点进行反驳。我能想到的最好的(除了同意……已经提出的反驳之外)是“恐惧、不确定和怀疑”。如果这是从第一天起语言的一部分,我非常怀疑它是否会出现在 Andrew Kuchling 的“Python 缺点”页面上。
参考实现
当前的实现处于初步状态(没有文档,但经过充分测试且稳定),是 Python 的 CVS 开发树的一部分 [5]。使用它需要您从源代码构建 Python。
这是从 Neil Schemenauer 的早期补丁派生出来的 [4]。
脚注和参考文献
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0255.rst
上次修改时间:2023-09-09 17:39:29 GMT