前言
打算写一系列文章来记录自己学习 Python 3 的点滴;本篇将会重点介绍 Python 有关生成器 Generator 以及yield
方面的内容;
本文为作者的原创作品,转载需注明出处;
Generator
生成器,Python 提供了这样一种机制,并不一次性的返回所有批量计算的结果,而是通过这样的一种方式,逐步的,以计算一个结果,然后缓存,调取结果,然后进行下一次计算,得到结果,缓存结果,调取结果,然后进行下一次计算…. 这样直到将所有的计算结果全部取出;第一次接触到 Generator 的读者也许会很难理解,其实,它完全可以类比于数据库的 Cursor 既游标,通过游标,一个一个的返回批量的数据;这样做有什么好处呢?对,那就是内存占用,试想,如果某一次批量计算或者是批量查询有“百万 + ”的数据呢?如果不分步骤的去获取结果,那么势必导致内存溢出,程序异常退出;这就是一个典型的用时间换空间
的典型用例;
所以,在 Python 编程中,使用好了生成器 Generator,那么就能够对内存进行充分有效的控制;
来看一个非常简单的例子,来体会一下什么是 Generator,
1 | for x in range(5)] l = [x * x |
上述的方式,将一次性返回一个包含 5 个数的列表;那么,如果我要每计算一次得到一个结果呢,而不是批量的返回计算的所有结果?
1 | for x in range(5)) g = (x * x |
由此,我们可以看到,打印变量g
不再返回队列的所有元素,取而代之返回的是一个generator
对象的引用;那么我们如何来输出其对应的结果呢?答案是,将generator
引用作为参数调用方法next()
逐步的一个一个的输出计算的结果,
1 | next(g) |
总共输出了五个元素,当最后一个元素输出以后,再次输出,会遇到错误StopIteration
;注意,这里的有关方法next(g)
的调用的过程,是计算一次,输出一个结果,而不是批量将所有的数组元素计算出来,再依次返回结果,这里和普通的迭代过程是有着本质区别的;
当然,上述的调用方式,也可以改为,因为 generator 对象是可以被迭代的,在迭代的过程中,会依次调用next
方法;
1 | for n in g: |
Yield
yield
是 Python 对生成器的一种实现,很难三言两语就直接说清楚它的作用,还是从例子入手吧;
下面这个 Python 函数将打印出前 N 个斐波那契数列的元素,
1 | def fab(max): |
是的,我们可以很容易一次性的打印出前 5 个元素;1
2
3
4
5
6
7
8for n in fab(5):
print(n)
1
1
2
3
5
是的,你会问了,如果我像一次性打印 100 万个呢?是的,列表 L 会保存一次性批量的计算出来 100 万个元素,是的,也许,你的 python 程序就会因为内存不足而被 Crash 掉了;那该怎么办呢?是的,有 Java 编程经验的开发者很容易想到,将其改造成迭代的方式;好了,于是我们有了下面这个方式,
1 | class Fab(object): |
有关 Python 类的相关内容将会在后续进行介绍,不过有过 Java 编程经验的人看懂上述代码不是大的问题,关键点在于对象 Fab 的next
方法,该方法提供了迭代计算的能力,每计算一次,返回一个结果,而不再像上面那样,通过一个列表 list _L_ 缓存了所有的计算结果以后,再返回;说了这么多,我们来验证以下执行的结果呗,
1 | 5) f = Fab( |
可以看到,我们实现了计算一次,返回一个结果的机制,当计算完毕以后,返回一个错误StopIteration
;是的,我们做到了,We saved the world;Java 工程师至此就结束了,因为我们拯救了内存;不过 Python 工程师还不满足,上面的代码的确可以工作,但是,它还是显得太过于复杂了,相对于最初的版本太复杂了,原来最初的版本是一个 Function,但是为了解决这个问题,我们需要将它重写成一个类;的确,从这个角度比较以后,它太过于复杂了,于是乎yield
诞生了。下面我们看看,上面的方式是如何通过yield
关键字来进行持续的改进,
1 | def fab(max): |
什么东东?yield b
是来干嘛的?是玉皇大帝派来搞笑的吗?嗯,有可能;先来执行以下的方法来试试,
1 | 5) f = fab( |
哟西,原来它返回的是一个 generator 的引用;真的是吗?我不信,再试试,
1 | next(f) |
K.A.O,仅仅通过一个关键字yield
,我们就做到了上面需要重构成一个类Fab
想要达到的目的;现在,我们得到的结果是,没计算一次,返回一个结果;Wait,是的,我看到了这个结果,但是,你没有告诉我,Python 是如何做到的?是呀,Python 是如何做到的呢?
好问题,Python 是如何做到的呢?原理其实也不复杂,以上述例子为例,当 Python 解释器解释执行到yield
的时候,它做了两件事情;第一件事情,返回结果 b,第二件事情,执行中断
,知道操作系统原理的朋友这里应该就非常清楚了,不过笔者打算还是简要的描述一下,Python 会通过中断
的方式让当前程序休眠
,既是在内存中保存当前程序执行的栈帧相关的所有信息,当且仅当执行到方法next(g)
的时候,唤醒被中断的 generator,并恢复栈帧相关的信息,并继续执行,直到遇到yield
以后,又会发生一次中断
,直到所有的结果都得以返回为止….
类型判断
这里我们依然使用 Yield 小节中的通过yield
改造后的函数 fab 为例进行描述,
1 | def fab(max): |
要注意的是,fab 和 fab(5) 的区别,
1 | import types |
1 | 5) f = fab( |
1 | f = fab |
由此可知,fab 依然表示的是普通的 Function 类型,而 fab(5) 则返回的是一个 generator;
总结
说实话,当笔者学习到 Generator 以及 Yield 的时候,被它的简单之美给深深的折服;