PEP: 255
题目: 简单的GENERATOR
版本: $修订本: 1.18 $
作者: Neil Schemenauer <nas at python.ca>, Tim Peters <tim.one at home.com>,
Magnus Lie Hetland <magnus at hetland.org>
讨论区: python-iterators@lists.sourceforge.net
状态: 定型本
类型: 标准轨迹
要求: 234
完成时间: 18-May-2001
Python版本号: 2.2
Post-History: 14-Jun-2001, 23-Jun-2001
摘要
这篇PET介绍了Python语言的GENERATOR的概念,以及在他们使用过程中的一些相关语句,"yield"语句。
目的
当一个过程函数要处理相当困难的任务时,它要求对过程值进行维护,大多数的编程语言只是在过程函数的参数列表中增加一个回调函数而没有更为有效的更合适的解决方法来调用每一个过程值。
例如:在标准库之内的tokenize.py采用以下方法:调用者必须传递一个"tokeneater"到tokenize(),也就是说:无论什么时候tokenize()被调用都能找到下一个令牌。这就使得tokenize()函数可以用一种自然的方式编码。但是,调用tokenize的程序非常的麻烦,因为它需要记住回调时哪一个令牌最后被看见。在tabnanny.py 中的tokeneater函数就是一个很好的例子,它在全局变量中维护一个状态设备,让它来记住回调时哪些已经看见过,哪些是希望下一个看见的。这要使它正确的运行是困难的,并且也难于被人们理解。不幸的是,这正是这个方法的典型之处。
对于tokenize来说,另一种选择就是在一个大的列表立即生成Python程序完全解释。于是,tokenize客户端就会以一种自然的方式编写,使用局部变量和局部控制流程(例如循环和嵌套的if语句)来追踪它们的状态。但这也是不现实的:程序会很大,实现整个解释的过程所需的内存将会是无法限制,但tokenize客户端只想看看特殊的某些事是否会发生,这些事件可能只在程序的前部份(如:future语句,空闲调用,第一个嵌套语句), 所以,首先追踪整个程序严重的浪费时间。
还有一种方式就是把tokenize当成一个迭代器[1],无论它的.next()方法什么时候调用,都传递该令牌。这对于调用者来说是一个高兴的事情,对于即将出现的结果的大列表同样如此,而且它没有了内存和对于 "哪个是我想早点取出来的?"问题的缺点。甚至,他还减少了tokenize记住.next()函数调用的*its*的状态的负担。并且读者只要提到tokenize.tokenize_loop()函数就可意识到它是件可怕的繁琐的事。或者描述一个递归算法来生成一个普通的树结构的编码:将它放进一个迭代器框架需要手工卸载递归式和手动维持这个往返移动的状态。
第四种方式就是在单独的线程上运行生产者和消费者。这就可以都以自然的方式保持它们的状态,因此这对于两者都是高兴的。实际上,在Python资源分布中的Demo/threads/Generator.py提供了一个可以使用的同步-通讯类,这样可以用一个通用的方式保持状态。这个方法在不支持线程的平台上是无法工作的,即使能工作也是很慢的(与不使用线程的方法相比)。
最后一种方法就是使用无堆栈[2][3]的、不同的Python实现,该过程支持轻量的协同程序。这与线程的方式有相同的功效,而且要有效的多。但是,无堆栈方法是Python核心的一个争议性的设想,并且对于Jython来说,要实现同一个语义也许是不可能的。但PEP并不是争论这个问题的地方,所以在这里可以充分地说:该GENERATOR在某种方式上提供了一个有用的无堆栈功能性的子集。它容易适合当前的CPython实现过程,相信对于其他Python的使用来说也相当易懂。
上面列举了所有的备选的方法。一些其他高级别的语言在Sather[4]中提供了让人高兴的解决方案和杰出的迭代器。Sather是从CLU的迭代器中得到灵感的;他们也提供了在Icon[5]中的GENERATOR,一种各处都表述为"is a generator"的新语言。这些在表现上各有不同,但是基础观念都是一样的:提供一种能够返回一个中间结果("下一个值")到它的调用者的函数;但是保持函数的局部状态,所以可以在它离开的地方重新开始。下面就是一个简单的例子:
def fib():
a, b = 0, 1
while 1:
yield b
a, b = b, a+b
当fib()函数第一次调用时,他设定a=0,b=1,于是yield b返回到调用者,调用者看到的是1,当fib继续时,从它的观点来看,yield语句与打印语句实际上是一样的:yield 执行后,所有的局部变量都没有改变,fib继续。a和b就会变成1和1,并且fib循环回到yield调用,将1赋给它的调用程序。以此类推。从fib的观点来看,它仅仅是传递了结果的顺序,就好像借助于回调程序一样。但是从调用者的观点来看,fib 调用是一个任意重新开始的可迭代的对象。 在线程方式中,这就允许两边都以自然的方式编码;但是与线程方式不同的是,它可以更有效并且可工作于所有的平台。实际上,重新开始一个GENERATOR应该和一个函数调用占用差不多的资源。
同样一种方式应用在多生产者/消费者函数上。例如:tokenize.py可以赋值给下一个令牌,而不是将一个回调函数作为参数调用,并且tokenize客户端函数能够用一种自然的方式迭代令牌;一个Python GENERATOR是一个Python迭代器[1],而且相当功能强大。
说明: Yield
一个新的语句是这样引入的:
yield_stmt: "yield" expression_list
"yield"语句是一个新的关键词,所有future语句[8]都需要经历下列阶段:在初始发布时,需要使用GENERATOR的模块必须包括下划线:
from __future__ import generators
并放在靠近开头的部分(具体细节请看 PEP 236[8]))。不用一个future语句就使用标志符"yield"的模块会引起警告的。在接下来的发布中,"yield"会成为一种语言的关键词,而且future语句不需要了。
yield语句只能用于操作函数内部。包括一个yield语句的操作函数称为GENERATOR操作函数。GENERATOR操作函数在各个方面都是一个普通的操作函数,但是代码对象的co_flags成员中拥有新的CO_GENERATOR标记设置。
当一个GENERATOR函数被调用时,实参以一个通用的方式与局部操作函数的形参对应,但是在该操作函数的体中没有执行代码。它不再返回一个GENERATOR-迭代器对象,而是遵守迭代器协议[6],所以在特殊情况下可以一种自然的方式应用在for循环语句中。记住:当上下文含义清楚时,绝对的名字"generator"要么指代一个GENERATOR操作函数,要么指代GENERATOR-迭代器。
每一次调用一个GENERATOR-迭代器的.next()函数时,GENERATOR操作函数的体中的编码会一直执行直到碰到一个yield语句或者返回语句(参看上文)为止,再就是到达体的结尾。
如果碰到yield语句的话,操作函数的状态被冻结,并且返回参数列表的值到.next()的调用处。冻结的意思是所有的状态都被保持,包括局部变量的当前捆绑、指令指针、和内部计算堆栈:保存了足够的信息以便下一次调用.next()时,该操作函数可以准确地执行,就好像yield语句只是外部的一个调用而已。
限制:yield语句不允许出现在一个try/finally结构的一个try子句中。问题是无法保证GENERATOR什么时候可重新开始,因此也无法保证finally块什么时候执行;那是对finally用途违背了太多了。
限制:在GENERATOR正在运行时不得重新开始:
>>> 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
说明:返回
一个GENERATOR操作函数也可以包括下列形式的返回语句:
"return"
记住:参数列表不允许用在GENERATOR体中的返回语句上(尽管,他们可以嵌套在GENERATOR的非GENERATOR操作函数的体上)。
当遇到一个返回语句时,控制进程像其他函数一样的返回,执行正确的finally子句(如果有的话)。当产生一个StopIteration异常,表示该迭代器已经耗尽,如果控制流程脱离GENERATOR末端而没有一个清楚地返回的话也会产生StopIteration异常。
记住:返回的意思是"我执行完了,并且没有带回任何你所关心的事情",这对于GENERATOR操作函数与非GENERATOR操作函数是一样的。
注意:返回并不总是等同于产生StopIteration:他们的差别在于如何处理封装try/except结构。
例如:
>>> def f1():
... try:
... return
... except:
... yield 1
>>> print list(f1())
[]
因为,和其他操作函数一样,返回仅仅是退出,但是:
>>> def f2():
... try:
... raise StopIteration
... except:
... yield 42
>>> print list(f2())
[42]
因为StopIteration是由一个空的"except"捕捉的,这和其它的异常相同。
说明:GENERATOR和异常传播
如果一个未处理的异常--包括,但是并不只限于StopIteration--由一个GENERATOR操作函数产生,或者通过它传递。这样异常会以一种通常的方式传递到调用者处。试图并发重新开始GENERATOR操作函数会引起StopIteration。换句话说,一个未处理的异常会终止了一个GENERATOR的有效生命周期。
例如(不合乎语言习惯但是可以解释这一观点):
>>> 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子句中。结果是GENERATOR应该非常小心地分配关键性的资源。对于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,
# 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,
两者的输出块显示为:
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参考实现过程也要大力的开发它,为了判断哪种操作函数是GENERATOR操作函数(尽管用一个新的关键字代替"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的机制相像:实现过程需要在开头很好地定义*something*。并且Python将这些机制对高级用户开放。但是,那不是为了强迫每个人都在这个等级上工作而设定的一个参数。"return"在任何一个操作汉书中都意味着"我已经完成了",并且那是易于理解和使用的。记住:在try/except结构中,"return"并不总是等同于"raise StopIteration"(参看"说明:返回"部分)。
问题:那么为什么不允许"return"语句加上表达式呢?
解答:也许有一天我们会允许的。在Icon语言中"return expr"既意味着"我已经完成了",也表示"但是我有一个最终有用的值返回,就是这个值"的意思。在开始阶段,缺乏"return expr"的强制使用,对于值的传送来说,只使用"yield"要简单清楚的多。
BDFL 声明
提议方:介绍另一种新的关键字("gen" 或者"generator")取代"def",或者其他的备选语法,从而区分GENERATOR操作函数与非GENERATOR操作函数。
反对方:实际上(不管你怎么认为),GENERATOR属于操作函数,但是他们稍做了变动,他们是可重启的。他们如何启动的机制是相对较小的技术难题,并且引入一个新的关键字不会帮助全面强调GENERATOR如何启动的机制(GENERATOR的生命中的一个关键却又微小的部分)。
专家:实际上(不管你怎么认为),GENERATOR操作函数是一个真正的库函数。它魔术似的产生GENERATOR-迭代器。在这个方面,他们与非GENERATOR操作函数有着本质的区别,他们更像一个结构而不仅仅是一个操作函数,所以重复使用"def"是最好的选择。藏于体中的"yield"并不足以警告:该部分是如此的与众不同。
BDFL:"def"它保留下来,任何一方都没有完全令人信服的论据,所以我咨询了我的语言设计者。它告诉我们:在PEP中假设的语法是相当正确的--既不太热,也不太冷。但是,像在希腊神话中特尔斐的预言一样,它不会告诉我们为什么,所以我也不会为PEP的争论辩驳。我所能提出的(远离辩驳...已经做过)最好就是"FuD"。一旦某天成为语言的一部分,我非常怀疑它是否抄袭了Andrew Kuchling的 《Python 瑕疵》一文。
参考实现过程
当前的实现,处在初始阶段(没有文档,但是测试良好又可靠)。它是Python CVS开发树[9]的一部分。它要求你从资源中建立Python语言。
本文源于Neil Schemenauer[7]的一个早期的补丁程序。
脚本和参考
[1] PEP 234, Iterators, Yee, Van Rossum
http://www.python.org/peps/pep-0234.html
[3] PEP 219, Stackless Python, McMillan
http://www.python.org/peps/pep-0219.html
[4] "Iteration Abstraction in Sather"
Murer , Omohundro, Stoutamire and Szyperski
http://www.icsi.berkeley.edu/~sather/Publications/toplas.html
[5] http://www.cs.arizona.edu/icon/
[6] The concept of iterators is described in PEP 234. See [1] above.
[7] http://python.ca/nas/python/generator.diff
[8] PEP 236, Back to the __future__, Peters
http://www.python.org/peps/pep-0236.html
[9] To experiment with this implementation, check out Python from CVS
according to the instructions at
http://sf.net/cvs/?group_id=5470
Note that the std test Lib/test/test_generators.py contains many
examples, including all those in this PEP.