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
 
02-14 04:13