17.7. plural.py, 第 6 阶段

现在你已准备好探讨生成器 (Generator) 了。

例 17.17. plural6.py


import re

def rules(language):                                                                 
    for line in file('rules.%s' % language):                                         
        pattern, search, replace = line.split()                                      
        yield lambda word: re.search(pattern, word) and re.sub(search, replace, word)

def plural(noun, language='en'):      
    for applyRule in rules(language): 
        result = applyRule(noun)      
        if result: return result      

这里使用了被称作生成器的技术,我不打算在你看过一个简单例子之前试图解释它。

例 17.18. 介绍生成器

>>> def make_counter(x):
...     print 'entering make_counter'
...     while 1:
...         yield x               1
...         print 'incrementing x'
...         x = x + 1
...     
>>> counter = make_counter(2) 2
>>> counter                   3
<generator object at 0x001C9C10>
>>> counter.next()            4
entering make_counter
2
>>> counter.next()            5
incrementing x
3
>>> counter.next()            6
incrementing x
4
1 make_counter 中出现关键字 yield 意味着这不是一个普通的函数。它是一种每次生成一个值的特殊函数。你可以把它看成是一个可恢复函数。调用它会返回一个生成器,它可以返回 x 的连续值。
2 想要创建一个 make_counter 生成器的实例,只要像其它函数一样调用。注意这并没有真正执行函数代码。你可以分辨出这一点,因为 make_counter 的第一行是 print 语句,然而没有任何内容输出。
3 make_counter 函数返回一个生成器对象。
4 你第一次调用生成器对象的 next() 方法,将执行 make_counter 中的代码执行到第一个 yield 语句,然后返回生产 (yield) 出来的值。在本例中,这个值是 2,因为你是通过 make_counter(2) 来创建最初的生成器的。
5 不断调用生成器对象的 next() 将从你上次离开的位置重新开始 并继续下去直到你又一次遇到 yield 语句。接下来执行 print 语句来打印 incrementing x,然后执行 x = x + 1 语句来真正地增加。然后你进入 while 的又一次循环,你所做的第一件事是 yield x,返回目前的 x 值 (现在是3)。
6 第二次你调用 counter.next() 时,你又做一遍相同的事情,但是这次 x4。如此继续。因为 make_counter 设置的是一个无限循环,理论上你可以永远这样继续下去,不断地递增并弹出 x 值。现在让我们看看生成器更具意义的应用。

例 17.19. 使用生成器替代递归


def fibonacci(max):
    a, b = 0, 1       1
    while a < max:
        yield a       2
        a, b = b, a+b 3
1 斐波纳契数列 (Fibonacci sequence) 是每个数都是前面两个数值和的一个数列。它从 01 开始,开始增长得很慢,但越来越快。开始这个数列你需要两个变量:a0开始,b1 开始。
2 a 是数列的当前值,弹出它。
3 b 是数列的下一个数,把它赋值给 a,同时计算出 (a+b) 并赋值给 b 放在一边稍后使用。注意这是并行发生的,如果 a3b5,那么 a, b = b, a+b 将会设置 a5 (b 的原值),b8 (ab 之和)。

这样你就有了生成连续的 Fibonacci 数的函数了。当然你也可以通过递归做到,但是这里的方法更加易读。并且也与 for 工作得很好。

例 17.20. for 循环中的生成器

>>> for n in fibonacci(1000): 1
...     print n,              2
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
1 你可以在 for 循环中直接使用 fibonacci 这样的生成器。for 循环将会创建一个生成器对象并连续调用其 next() 方法获得值并赋予 for 循环变量 (n)。
2 每轮 for 循环 n 都从 fibonacciyield 语句获得一个新的值。当 fibonacci 超出数字限定 (a 超过 max,你在这里限定的是 1000) 很自然地退出 for 循环。

好了,让我们回到 plural 函数看看如何可以把它用起来。

例 17.21. 生成器生成动态函数


def rules(language):                                                                 
    for line in file('rules.%s' % language):                                          1
        pattern, search, replace = line.split()                                       2
        yield lambda word: re.search(pattern, word) and re.sub(search, replace, word) 3

def plural(noun, language='en'):      
    for applyRule in rules(language):  4
        result = applyRule(noun)      
        if result: return result      
1 for line in file(...) 是从文件中一行行读取的通用方法,每次一行。它能正常工作是因为 file 实际上返回一个生成器,它的 next() 方法返回文件中的下一行。简直太酷了,光是想想就让我满头大汗。
2 这没有什么神奇之处。还记得规则文件的每一行都用空白分开三个值吗?所以 line.split() 返回一个三元素元组,你把这些值赋给了 3 个局部变量。
3 然后你不断地弹出。 你弹出什么呢?一个使用 lambda 动态生成的函数,而这个函数实际上是一个闭合 (把本地变量 patternsearchreplace 作为常量)。换句话说,rules 是一个弹出规则函数的生成器。
4 既然 rules 是一个生成器,你就可以在 for 循环中直接使用它。for 循环的第一轮你调用 rules 函数,打开规则文件,读取第一行,动态构建一个根据规则文件第一行匹配并应用规则的函数。for 循环的第二轮将会从上一轮 rules 中停下的位置 (for line in file(...) 循环内部) 读取规则文件的第二行,动态构建根据规则文件第二行匹配并应用规则的另一个函数。如此继续下去。

你在第 5 阶段得到的是什么?第 5 阶段中,你读取整个规则文件并在使用第一条规则之前构建一个所有规则组成的列表。现在有了生成器,你可以更舒适地做到这一切:你打开并读取第一条规则,根据它创建函数并使用之,如果它适用则根本不去读取规则文件剩下的内容,也不去建立另外的函数。

进一步阅读