Python 中的生成器 (generator) 是一个十分有用的工具,它让我们能方便地生成迭代器 (iterator)。这篇文章里,我们就来说说什么是生成器,生成器有什么作用以及如何使用。
本文需要你对 Python 基本的语法有一定的了解,并知道 iterator 是什么,且我们可以通过 next(iterator) 来获取 iterator 的下一个值。
iterator 简介
想象这样一个需求,我们需要从网上获取一些图片,这些图片的名字的规律是数字递增,因此我们有类似下面的代码:
[Python] 纯文本查看 复制代码
1 2 3 4 5 6 7 | def get_images(n): result = [] for i in range (n): result.append(get_image_by_id(i)) return result images = get_images(n) |
现在,假设我们需要对图片进行一些操作,但依当前图片的情况不同,我们也许不需要后续的图片,并且, get_image_by_id 是一个很耗时的操作,我们希望在不需要的情况下尽量避免调用它。
换句话说,我们希望能对 get_image_by_id 进行懒执行 (lazy evalution)。这也不难,我们可以这么做
[Python] 纯文本查看 复制代码
1 2 3 4 5 6 7 8 | image_id = - 1 def next_image(): global image_id image_id + = 1 return get_image_by_id(image_id) image0 = next_image() image1 = next_image() |
这里函数 next_image 使用了全局的变量保存当前已获取的图片的 id,使用全局变量决定了 next_image 无法被两个个体使用。例如两个人都想从头获取图片,这是没法完成的,因此我们定义一个类来解决这个问题:
[Python] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 | class ImageRepository: def __init__( self ): self .image_id = - 1 def next_image( self ): self .image_id + = 1 return get_image_by_id( self .image_id) repo = ImageRepository() image0 = repo.next_image() image1 = repo.next_image() |
如果你熟悉 iterator 的话,应该知道上面这个需求是一个典型的 iterator,因此我们可以实现 __iter__ 及 __next__ 方法来将它变成一个 iterator,从而充分利用 iterator 现成的一些工具:
[Python] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 | class ImageRepository: def __init__( self ): self .image_id = - 1 def __iter__( self ): return self def __next__( self ): self .image_id + = 1 return get_image_by_id( self .image_id) for image in ImageRepository(): # some operation on each image |
从 Iterator 到 Generator
上面的 iterator 的例子有一个特点,就是它需要我们自己去管理 iterator 的状态,即 image_id。这种写法跟我们的思维差异较大,因此懒惰的我们希望有一些更好,更方便的写法,这就是我们要介绍的 genrator 。
在 Python 中,只要一个函数中使用了 yeild 这个关键字,就代表这个函数是一个生成器 (generator)。而 yield 的作用就相当于让 Python 帮我们把一个“串行”的逻辑转换成 iterator 的形式。例如,上面的例子用 generator 的语法写就变成了:
[Python] 纯文本查看 复制代码
1 2 3 4 5 6 7 8 | def image_repository() image_id = - 1 while True : image_id + = 1 yield get_image_by_id(image_id) for image in image_repository(): # do some operation |
首先,就写法上,这种写法与我们最先开始的循环写法最为类似;其次,在功能上,调用这个函数 image_repository() 返回的是一个 generator object,它实现了 iterator 的方法,因此可以将它作为普通的 iterator 使用 (for ... in ...);最后,注意到我们所要做的,就是把平时使用的 return 换成 yield 就可以了。
再举个例子:
[Python] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 | def fibonacci(): a, b = ( 0 , 1 ) while True : yield a a, b = b, a + b fibos = fibonacci() next (fibos) #=> 0 next (fibos) #=> 1 next (fibos) #=> 1 next (fibos) #=> 2 |
通过 generator ,我们很轻松地就写出了一个无限的斐波那契数列函数。如果要手写的话,它相当于:
[Python] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | class Fibonacci(): def __init__( self ): self .a, self .b = ( 0 , 1 ) def __iter__( self ): return self def __next__( self ): result = self .a self .a, self .b = self .b, self .a + self .b return result fibos = Fibonacci() next (fibos) #=> 0 next (fibos) #=> 1 next (fibos) #=> 1 next (fibos) #=> 2 |
显然 generator 的写法更为清晰,且符合我们平时书写顺序结构的习惯。
Generator 与控制流前面我们提到,Generator 的作用其实是实现了懒执行 (lazy evalution) ,即在真正需要某个值的时候才真正去计算这个值。因此,更进一步,Generator 其实是返回了控制流。当一个 generator 执行到 yeild 语句时,它便保存当前的状态,返回所给的结果(也可以没有),并将当前的执行流还给调用它的函数,而当再次调用它时,Generator 就从上次 yield 的位置继续执行。例如:[Python] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | def generator(): print ( 'before' ) yield # break 1 print ( 'middle' ) yield # break 2 print ( 'after' ) x = generator() next (x) #=> before next (x) #=> middle next (x) #=> after #=> exception StopIteration |
可以看到,第一次调用 next(x),程序执行到了 break 1 处就返回了,第二次调用 next(x) 时从之前 yield 的位置(即 break 1) 处继续执行。同理,第三次调用 next(x) 时从 break 2 恢复执行,最终退出函数时,抛出 StopIteration 异常,代表 generator 已经退出。
为什么要提到 generator 的“控制流”的特点呢?因为 genrator 表允许我们从“顺序”执行流中暂时退出,利用这个特性我们能做一些很有意义的事。
例如,我们提供一个 API,它要求调用者首先调用 call_this_first 然后做一些操作,然后再调用 call_this_second,再做一些操作,最后调用 call_this_last。也就是说这些 API 的调用是有顺序的。但 API 的提供者并没有办法强制使用者按我们所说的顺序去调用这几个 API。但有了 generator,我们可以用另一种形式提供 API,如下:
[Python] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | class API: def call_this_first(): pass def call_this_second(): pass def call_this_last(): pass def api(): first() yield second() yield last() |
通过这种方式提供的 API 能有效防止使用者的误用。这也是 generator 能 “从控制流中返回” 这个特性的一个应用。
yield 加强版上面我们说到 Generator 允许我们暂停控制流,并返回一些数据,之后能从暂停的位置恢复。那我们就会有疑问,既然暂停控制流时能返回数据,那恢复控制流的时候能不能传递数据到暂停的位置呢? PEP 342中就加入了相关的支持。这个需求说起来比较抽象,我们举个例子:
想象我们要写一个函数,计算多个数的平均值,我们称它为 averager。我们希望每次调用都提供一个新的数,并返回至今为止所有提供的数的平均值。让我们先来看看用 generator 的加强版语法怎么实现:
[Python] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | def averager(): sum = 0 num = 0 while True : sum + = ( yield sum / num if num > 0 else 0 ) num + = 1 x = averager() x.send( None ) #=> 0 x.send( 1 ) #=> 1.0 x.send( 2 ) #=> 1.5 x.send( 3 ) #=> 2.0 |
这个加强版的语法是这么工作的: yield 之前是语句,现在是表达式,是表达式就意味着我们能这么写 x = yield 10, y = 10 + (yield), foo(yield 42)。Python 规定,除非 yield 左边直接跟着等号(不准确),否则必须用扩号括起来。
当 Python 执行到 yield 表达式时,它首先计算 yield 右边的表达式,上例中即为 sum / num if num > 0 else 0 的值,暂停当前的控制流,并返回。之后,除了可以用 next(generator) 的方式(即 iterator 的方式)来恢复控制流之外,还可以通过 generator.send(some_value) 来传递一些值。例如上例中,如果我们调用 x.send(3) 则 Python 恢复控制流, (yield sum/sum ...) 的值则为我们赋予的 3,并接着执行 sum += 3 以及之后的语句。注意的是,如果这时我们用的是 next(generator) 则它等价为 generator.send(None)。
最后要注意的是,刚调用 generator 生成 generator object 时,函数并没有真正运行,也就是说这时控制流并不在 yield 表达式上
更多技术资讯可关注:gzitcast