Python 系列学习之五:生成器 Generator ( Yield )

前言

打算写一系列文章来记录自己学习 Python 3 的点滴;本篇将会重点介绍 Python 有关生成器 Generator 以及yield方面的内容;

本文为作者的原创作品,转载需注明出处;

Generator

生成器,Python 提供了这样一种机制,并不一次性的返回所有批量计算的结果,而是通过这样的一种方式,逐步的,以计算一个结果,然后缓存,调取结果,然后进行下一次计算,得到结果,缓存结果,调取结果,然后进行下一次计算…. 这样直到将所有的计算结果全部取出;第一次接触到 Generator 的读者也许会很难理解,其实,它完全可以类比于数据库的 Cursor 既游标,通过游标,一个一个的返回批量的数据;这样做有什么好处呢?对,那就是内存占用,试想,如果某一次批量计算或者是批量查询有“百万 + ”的数据呢?如果不分步骤的去获取结果,那么势必导致内存溢出,程序异常退出;这就是一个典型的用时间换空间的典型用例;

所以,在 Python 编程中,使用好了生成器 Generator,那么就能够对内存进行充分有效的控制;

来看一个非常简单的例子,来体会一下什么是 Generator,

1
2
3
>>> l = [x * x for x in range(5)]
>>> l
[0, 1, 4, 9, 16]

上述的方式,将一次性返回一个包含 5 个数的列表;那么,如果我要每计算一次得到一个结果呢,而不是批量的返回计算的所有结果?

1
2
3
>>> g = (x * x for x in range(5))
>>> g
<generator object <genexpr> at 0x1022ef630>

由此,我们可以看到,打印变量g不再返回队列的所有元素,取而代之返回的是一个generator对象的引用;那么我们如何来输出其对应的结果呢?答案是,将generator引用作为参数调用方法next()逐步的一个一个的输出计算的结果,

1
2
3
4
5
6
7
8
9
10
11
12
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

总共输出了五个元素,当最后一个元素输出以后,再次输出,会遇到错误StopIteration;注意,这里的有关方法next(g)的调用的过程,是计算一次,输出一个结果,而不是批量将所有的数组元素计算出来,再依次返回结果,这里和普通的迭代过程是有着本质区别的;

当然,上述的调用方式,也可以改为,因为 generator 对象是可以被迭代的,在迭代的过程中,会依次调用next方法;

1
2
3
4
5
6
7
>>> for n in g:
print(n)
0
1
4
9
16

Yield

yield是 Python 对生成器的一种实现,很难三言两语就直接说清楚它的作用,还是从例子入手吧;

下面这个 Python 函数将打印出前 N 个斐波那契数列的元素,

1
2
3
4
5
6
7
8
def fab(max): 
n, a, b = 0, 0, 1
L = []
while n < max:
L.append(b)
a, b = b, a + b
n = n + 1
return L

是的,我们可以很容易一次性的打印出前 5 个元素;

1
2
3
4
5
6
7
8
>>> for n in fab(5): 
... print(n)
...
1
1
2
3
5

是的,你会问了,如果我像一次性打印 100 万个呢?是的,列表 L 会保存一次性批量的计算出来 100 万个元素,是的,也许,你的 python 程序就会因为内存不足而被 Crash 掉了;那该怎么办呢?是的,有 Java 编程经验的开发者很容易想到,将其改造成迭代的方式;好了,于是我们有了下面这个方式,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Fab(object): 

def __init__(self, max):
self.max = max
self.n, self.a, self.b = 0, 0, 1

def __iter__(self):
return self

def next(self):
if self.n < self.max:
r = self.b
self.a, self.b = self.b, self.a + self.b
self.n = self.n + 1
return r
raise StopIteration()

有关 Python 类的相关内容将会在后续进行介绍,不过有过 Java 编程经验的人看懂上述代码不是大的问题,关键点在于对象 Fab 的next方法,该方法提供了迭代计算的能力,每计算一次,返回一个结果,而不再像上面那样,通过一个列表 list _L_ 缓存了所有的计算结果以后,再返回;说了这么多,我们来验证以下执行的结果呗,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> f = Fab(5)
>>> f
<__main__.Fab object at 0x102bbd898>
>>> f.next()
1
>>> f.next()
1
>>> f.next()
2
>>> f.next()
3
>>> f.next()
5
>>> f.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 16, in next
StopIteration

可以看到,我们实现了计算一次,返回一个结果的机制,当计算完毕以后,返回一个错误StopIteration;是的,我们做到了,We saved the world;Java 工程师至此就结束了,因为我们拯救了内存;不过 Python 工程师还不满足,上面的代码的确可以工作,但是,它还是显得太过于复杂了,相对于最初的版本太复杂了,原来最初的版本是一个 Function,但是为了解决这个问题,我们需要将它重写成一个类;的确,从这个角度比较以后,它太过于复杂了,于是乎yield诞生了。下面我们看看,上面的方式是如何通过yield关键字来进行持续的改进,

1
2
3
4
5
6
def fab(max): 
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1

什么东东?yield b是来干嘛的?是玉皇大帝派来搞笑的吗?嗯,有可能;先来执行以下的方法来试试,

1
2
3
>>> f = fab(5)
>>> f
<generator object fab at 0x102bbc258>

哟西,原来它返回的是一个 generator 的引用;真的是吗?我不信,再试试,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> next(f)
1
>>> next(f)
1
>>> next(f)
2
>>> next(f)
3
>>> next(f)
5
>>> next(f)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

K.A.O,仅仅通过一个关键字yield,我们就做到了上面需要重构成一个类Fab想要达到的目的;现在,我们得到的结果是,没计算一次,返回一个结果;Wait,是的,我看到了这个结果,但是,你没有告诉我,Python 是如何做到的?是呀,Python 是如何做到的呢?

好问题,Python 是如何做到的呢?原理其实也不复杂,以上述例子为例,当 Python 解释器解释执行到yield的时候,它做了两件事情;第一件事情,返回结果 b,第二件事情,执行中断,知道操作系统原理的朋友这里应该就非常清楚了,不过笔者打算还是简要的描述一下,Python 会通过中断的方式让当前程序休眠,既是在内存中保存当前程序执行的栈帧相关的所有信息,当且仅当执行到方法next(g)的时候,唤醒被中断的 generator,并恢复栈帧相关的信息,并继续执行,直到遇到yield以后,又会发生一次中断,直到所有的结果都得以返回为止….

类型判断

这里我们依然使用 Yield 小节中的通过yield改造后的函数 fab 为例进行描述,

1
2
3
4
5
6
def fab(max): 
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1

要注意的是,fab 和 fab(5) 的区别,

1
>>> import types
1
2
3
>>> f = fab(5)
>>> isinstance(f, types.GeneratorType)
True
1
2
3
4
5
6
7
>>> f = fab
>>> isinstance(f, types.GeneratorType)
False
>>> isinstance(f, types.FunctionType)
True
>>> isinstance(f(5), types.GeneratorType)
True

由此可知,fab 依然表示的是普通的 Function 类型,而 fab(5) 则返回的是一个 generator;

总结

说实话,当笔者学习到 Generator 以及 Yield 的时候,被它的简单之美给深深的折服;