级别: 初级 David Mertz,博士 (mertz@gnosis.cx), 自学者, Gnosis Software,Inc.
2001 年 9 月 01 日 Python 2.2 引进了一种带有新关键字的新型构造。这种构造是生成器;关键字是yield。生成器使几个新型、强大和富有表现力的编程习惯用法成为可能,但初看,要理解生成器,还是有一点困难。本文中,David 由浅入深地介绍了生成器,同时,还介绍了迭代器的相关问题。
欢迎来到奇妙的流控制世界。Python 2.2(现在是 alpha 发行版第三版
― 参见本文后面的
参考资料)将给程序员提供一些新的选项,这些在较早的
Python 版本是没有的 ― 或者至少不是很方便。
虽然 Python 2.2 所给予我们的不能象 Stackless Python
中的完全连续性和微线程那样容易理解,但还是可以说,生成器和迭代器的行为与传统的函数和类会有点不同。
由于迭代器比较容易理解,让我们先来看它。基本上,
迭代器是含有
.next()
方法的对象。唔,这样定义不十分正确,但非常接近。事实上,当迭代器应用新的
iter()
内置函数时,大多数迭代器的上下文希望得到一个可以生成迭代器的对象。为使用户定义的类(该类含有必不可少的
.next() 方法)返回迭代器,需要使
__iter__() 方法返回
self 。本文中的示例会清楚地说明这一点。如果迭代有一个逻辑终止,则迭代器的
.next() 方法可能决定抛出
StopIteration
异常。
生成器要稍微复杂和一般化一点。但生成器最典型的用途是用来定义迭代器;所以不值得总是为一些细微之处而担心。
生成器是这样一个函数,它记住上一次返回时在函数体中的位置。对生成器函数的第二次(或第
n
次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。
在某些方面,生成器就象本专栏前面文章讨论的函数型编程中的“终止”(参见
参考资料)。象“终止”一样,生成器“记住”了它数据状态。但生成器比“终止”要更进一步:生成器还“记住”了它在流控制构造(在命令式编程中,这种构造不只是数据值)中的位置。由于连续性使您在执行框架间任意跳转,而不总是返回到直接调用者的上下文(如同生成器那样),因此它仍是比较一般的。
幸运的是,使用生成器比理解程序流和状态的所有概念性问题容易得多。实际上,稍加实践之后,就可以象普通函数那样容易地使用生成器。
随机遍历
让我们考虑一个相当简单的问题,可以用多种方法来解决它 ―
新方法和旧方法都可以。
假设我们想要一串正的随机数字流,它比服从向后参考约束的数字流要小。明确的讲,我们希望每个后续数字比前一个数字至少大或小
0.4。而且,数字流本身不是无限的,在几个随机步骤后结束。这个示例中,当数字流中产生小于
0.1
的数字时,我们将简单地结束它。上述的约束有点象可以在“随机遍历”算法找到的约束,结束条件类似“统计”或“局部最小值”结果
― 但当然,这要比大多数现实世界中简单。
在 Python 2.1
或更早的版本中,我们有几种方法来解决这个问题。一种方法是,简单地生成流中的数字列表并返回它。可能看起来象:
RandomWalk_List.py
import
random
def
randomwalk_list
():
last, rand = 1, random.random()
# init candidate elements
nums = []
# empty list
while
rand > 0.1:
# threshhold terminator
if
abs(last-rand) >= 0.4:
# accept the number
last = rand
nums.append(rand)
# add latest candidate to nums
else
:
print
'*',
# display the rejection
rand = random.random()
# new candidate
nums.append(rand)
# add the final small element
return
nums
|
利用这个函数就象如下所示般简单:
随机遍历列表的迭代
for num in randomwalk_list():
print num,
|
上面这种方法中有几个值得注意的局限性。这个特定的示例中极不可能产生庞大的数字列表,但只通过将阀值终结符定义得较严格,就可以创建任意大流(随机精确大小,但可以预见数量级)。在某种程度上,内存和性能问题可能使得这种方法不切实际,以及没有必要。同样是这个问题,使得
Python 较早的版本中添加了
xrange() 和
xreadlines() 。更重要的是,许多流取决于外部事件,并且当每个元素可用时,才处理这些流。例如,流可以侦听一个端口,或者等待用户输入。
试图在流之外创建完整的列表并不就是这些情形中的某一种。
在 Python 2.1
和较早版本中,我们的诀窍是使用“静态”函数局部变量来记住关于函数的上一次调用的一些事情。显而易见,全局变量可以做同样的工作,但它们带来了大家熟知的全局性名称空间污染的问题,并会因非局部性而引起错误。这里,如果您不熟悉这个诀窍,可能会感到诧异
― Python
没有“正式”的静态范围声明。然而,如果赋予了命名参数可变的缺省值,那么参数就可以,用作以前调用的持久存储器。明确的讲,列表是一些便利的可变对象,他们甚至可以方便地保留多个值。
使用“静态”方法,可以编写如下的函数:
RandomWalk_Static.py
import
random
def
randomwalk_static
(last=[1]):
# init the "static" var(s)
rand = random.random()
# init a candidate value
if
last[0] < 0.1:
# threshhold terminator
return
None
# end-of-stream flag
while
abs(last[0]-rand) < 0.4:
# look for usable candidate
print
'*',
# display the rejection
rand = random.random()
# new candidate
last[0] = rand
# update the "static" var
return
rand
|
这个函数是十分友好的存储器。它只需要记住一个以前的值,返回一个单个数字(不是一个数字的大列表)。并且与此类似的一个函数可以返回取决于(部分地或完全地)外部事件的连续的值。不利的一面是,利用这个函数有点不够简练,且相当不灵活。
静态随机遍历的迭代
num = randomwalk_static()
while num is not None:
print num,
num = randomwalk_static()
|
新的遍历方法
实质上,Python 2.2 序列都是迭代器。Python 常见的习惯用法
for
elem in lst: 现在实际上让
lst
产生一个迭代器。然后,
for 循环反复调用这个迭代器的
.next() 方法,直到它遇到
StopIteration
异常为止。幸运的是,由于所有常见的内置类型自动产生它们的迭代器,所以
Python 程序员不需要知道这里发生了什么。实际上,现在字典里有
.iterkeys() 、
.iteritems() 和
.itervalues() 方法来产生迭代器;首要的是在新的习惯用法
for key in dct: 中使用了什么。同样,通过调用
.readline() 迭代器支持新的习惯用法
for line in
file: 。
但是如果实际所产生的是在 Python
解释器内,则显而易见要用定制类来产生它们自己的迭代器,而不是专使用内置类型的迭代器。定制类支持直接使用
randomwalk_list() 以及一次一个元素这种“极度节省”的
randomwalk_static ,它是简单易懂的:
RandomWalk_Iter.py
import
random
class
randomwalk_iter
:
def
__init__
(self):
self.last = 1
# init the prior value
self.rand = random.random()
# init a candidate value
def
__iter__
(self):
return
self
# simplest iterator creation
def
next
(self):
if
self.rand < 0.1:
# threshhold terminator
raise
StopIteration
# end of iteration
else
:
# look for usable candidate
while
abs(self.last-self.rand) < 0.4:
print
'*',
# display the rejection
self.rand = random.random()
# new candidate
self.last = self.rand
# update prior value
return
self.rand
|
这个定制迭代器看起来确实如同由函数生成的真实列表一样:
随机遍历类的迭代
for num in randomwalk_iter():
print num,
|
事实上,即使支持习惯用法
if elem in
iterator ,它仅尝试为确定真值所需要的那么多的迭代器的元素,(如果最终的值为 false,当然,它就需要测试所有元素)。
美中不足
上述方法对于手边的问题非常好用。但没有一种方法能很好地解决这样的情形:例程在运行中创建了大量的局部变量,并把它的运行简化为循环和条件的嵌套。如果带静态(或全局)变量的迭代器类或函数取决于多个数据状态,则出现两个问题。一个是一般性问题:创建多个实例属性或静态列表元素来保留每个数据值。更为重要的问题是计算如何确切地返回到与数据状态相符的流逻辑的相关部分。非常容易忘记不同数据间的相互作用和互相依存。
生成器完全绕过了整个问题。生成器“返回”时带关键字
yield ,但“记住”了它“返回”的所有确切执行位置。下次调用生成器时,它再接着上次的位置 — 包括函数流和变量值这两个方面。
在 Python 2.2+ 中,不直接
写生成器。
相反,编写一个函数,当调用它时,返回生成器。这可能看起来有点古怪,但“函数工厂”是
Python 的常见特性,并且“生成器工厂”明显是这个概念性扩展。在
Python 2.2+ 中使函数成为生成器工厂是它主体某处的一个或多个
yield 语句。如果
yield
发生,
return 一定只发生在没有伴随任何返回值的情况中。
然而,一个较好的选择是,安排函数体以便于完成所有
yield
之后,执行就“跳转到结束”。但如果遇到
return ,它导致产生的生成器抛出
StopIteration 异常,而不是进一步生成值。
从我的观点来看,过去对生成器工厂的语法选择有点欠缺。
yield
语句可以非常好地存在于函数体中,您可能无法确定是否函数一定会在函数体最初
N
行内的某处作为生成器工厂而存在。当然,对于函数工厂,也存在这样的问题,但是由于函数工厂不改变函数体的实际
语法(并且有时允许函数体返回普通值,尽管这可能不是出自良好的设计)。
对于我来说,新关键字 ― 比如
generator 代替
def ― 会是一个比较好的选择。
先不考虑语法,当调用生成器来担当迭代器时,生成器有良好的状况来自动担当迭代器。这里不需要象类的
.__iter__() 方法。遇到的每个
yield
都成为生成器的
.next()
方法的返回值。为了清楚起见,我们来看一个最简单的生成器:
最简单可行的 Python 2.2
生成器
>>>
from
__future__
import
generators
>>>
def
gen
():
yield 1
>>> g = gen()
>>> g.next()
1
>>> g.next()
Traceback (most recent call last):
File "<pyshell#15>", line 1,
in
?
g.next()
StopIteration
|
让我们使生成器工作在我们样本问题中:
RandomWalk_Generator.py
from
__future__
import
generators
# only needed for Python 2.2
import
random
def
randomwalk_generator
():
last, rand = 1, random.random()
# initialize candidate elements
while
rand > 0.1:
# threshhold terminator
print
'*',
# display the rejection
if
abs(last-rand) >= 0.4:
# accept the number
last = rand
# update prior value
yield rand
# return AT THIS POINT
rand = random.random()
# new candidate
yield rand
# return the final small element
|
这个定义的简单性是吸引人的。可以手工或者作为迭代器来利用这个生成器。在手工情形下,生成器可以在程序中传递,并且无论在哪里以及无论何时需要(这非常灵活),都可以调用。手工情形的一个简单示例是:
随机遍历生成器的手工使用
gen = randomwalk_generator()
try:
while 1: print gen.next(),
except StopIteration:
pass
|
然而,更多情况下,可能将生成器作为迭代器来使用,这样更为简练(并且看起来又象只是一个老式的序列):
作为迭代器的随机遍历生成器
for num in randomwalk_generator():
print_short(num)
|
结束语
Python
程序员需要花一点时间来熟悉生成器的来龙去脉。最初这样一个简单构造所增加的能力是令人惊奇的;并且我预言,甚至熟练的程序员(象
Python
开发人员自己)也需要花一些时间来继续发现使用生成器过程中的一些微妙的新技术。
作为结束,让我再介绍一个生成器示例,它来自随 Python 2.2 一同分发的
test_generators.py 模块。假定一个树对象,并且想要以从左到右的顺序搜索它的叶子。使用状态监控变量,让类或函数来做这个工作是困难的。而使用生成器做这件工作简单得几乎令人眉开眼笑:
>>>> # A recursive generator that generates Tree leaves 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
|
参考资料
关于作者
对本文的评价
|