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 客户端只希望查看程序早期是否出现特定内容(例如,一个未来语句,或者像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] 中的生成器,Icon是一种新颖的语言,其中每个表达式 都是一个生成器。这些之间存在差异,但基本思想是相同的:提供一种可以向其调用者返回中间结果(“下一个值”)的函数,但同时保持该函数的局部状态,以便该函数可以从上次中断的地方继续执行。一个非常简单的例子
def fib():
a, b = 0, 1
while 1:
yield b
a, b = b, a+b
当 fib() 首次被调用时,它将 a 设置为 0,将 b 设置为 1,然后将 b yield 回其调用者。调用者看到 1。当 fib 恢复时,从它的角度看,yield 语句实际上与,比如说,一个 print 语句相同:fib 在 yield 之后继续执行,所有局部状态都完好无损。a 和 b 接着变为 1 和 1,然后 fib 循环回到 yield,向其调用者 yield 1。依此类推。从 fib 的角度看,它只是像通过回调一样提供一系列结果。但从其调用者的角度看,fib 调用是一个可迭代对象,可以随意恢复。与线程方法一样,这允许双方以最自然的方式编写代码;但与线程方法不同,这可以高效地在所有平台上完成。事实上,恢复生成器的开销不应超过函数调用。
相同的方法适用于许多生产者/消费者函数。例如,tokenize.py 可以 yield 下一个标记,而不是将其作为参数调用回调函数,并且 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 语句只是另一个外部调用一样继续执行。
限制:yield 语句不允许出现在 try/finally 构造的 try 子句中。困难在于无法保证生成器会被恢复,因此无法保证 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
如前所述,yield 不允许出现在 try/finally 构造的 try 子句中。这意味着生成器应该非常小心地分配关键资源。在其他情况下,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
我漏掉了一个吗 <眨眼>?在数百条消息中,我统计到三条建议这种替代方案,并从中提取了上述内容。不使用新关键字会很好,但让 yield 非常清晰会更好——我不想通过理解以前毫无意义的关键字或运算符序列来 推断 正在发生 yield。不过,如果这引起了足够的兴趣,支持者应该达成一个单一的共识建议,Guido 将对此发表意见。
为什么允许使用 return?为什么不强制使用 raise StopIteration 来终止?
StopIteration 的机制是低级细节,很像 Python 2.1 中 IndexError 的机制:底层实现需要做 一些 定义明确的事情,而 Python 向高级用户公开了这些机制。但这并不是强制每个人都在那个级别工作的论据。return 在任何类型的函数中都表示“我完成了”,这很容易解释和使用。请注意,在 try/except 构造中,return 也不总是等同于 raise StopIteration(参见“规范:Return”部分)。
那么为什么也不允许 return 后面跟着表达式呢?
也许总有一天会。在 Icon 中,return expr 既表示“我完成了”,也表示“但我还有一个最终有用的值要返回,就是这个”。在开始时,并且在没有令人信服地使用 return expr 的情况下,专门使用 yield 来传递值会更清晰。
BDFL 公告
问题
引入另一个新关键字(例如,gen 或 generator)来代替 def,或者以其他方式改变语法,以区分生成器函数和非生成器函数。
反对
在实践中(你如何看待它们),生成器 就是 函数,但它们的特殊之处在于它们是可恢复的。它们如何设置的机制是一个相对次要的技术问题,引入一个新关键字会不必要地过分强调生成器启动的机制(生成器生命中一个重要但很小的部分)。
赞成
实际上(你如何看待它们),生成器函数实际上是工厂函数,它们像魔法一样生成生成器迭代器。在这方面,它们与非生成器函数截然不同,更像是构造函数而不是函数,因此重用 def 充其量是令人困惑的。函数体中隐藏的 yield 语句不足以警告语义差异如此之大。
BDFL
def 保持不变。双方的论点都不能完全令人信服,所以我请教了我的语言设计师直觉。它告诉我,PEP中提出的语法是完全正确的——既不过热也不过冷。但是,就像希腊神话中的德尔斐神谕一样,它没有告诉我原因,所以我无法反驳针对PEP语法的论点。我能想到的最好的(除了同意……已经提出的反驳之外)是“FUD”。如果这从第一天起就是语言的一部分,我非常怀疑它会出现在 Andrew Kuchling 的“Python 糟点”页面上。
参考实现
目前的实现,处于初步状态(无文档,但经过充分测试且稳定),是 Python CVS 开发树的一部分 [5]。使用它需要您从源代码构建 Python。
这源自 Neil Schemenauer 早期的一个补丁 [4]。
脚注和参考文献
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0255.rst