玩转python(4)生成器
生成器是python中一个很有趣的概念,不只是有趣,而且很实用。
我们经常需要生成数组,一般来说,数组占据内存的大小和数组的长度有直接关系。数组中的元素越多,长度越长,占据的空间也越大。下面的代码中,我们初始化3个列表,并分别存放10,100,1000个元素:
import sys
l1 = list()
l2 = list()
l3 = list()
for i in range(10):
l1.append(i)
for i in range(100):
l2.append(i)
for i in range(1000):
l3.append(i)
print('size of l1:'+str(sys.getsizeof(l1)))
print('size of l2:'+str(sys.getsizeof(l2)))
print('size of l3:'+str(sys.getsizeof(l3)))
输出如下结果:
size of l1:192
size of l2:912
size of l3:9024
可以预见,当数组内的元素变得更多时,数组占用的空间会继续增大。如果开发者在程序运行之前不能确定数组长度的范围,那么这种动态数组会为程序的运行带来极高的风险。类似的还有读取文件,如果文件很大,直接读入内存可能会直接导致程序的崩溃。对于这个问题,聪明的程序员们自然不会坐以待毙,他们提出了这样的解决方案:如果列表元素可以按照某种算法推算出来,那就可以在循环的过程中不断推算出后续的元素,避免创建完整的数组,从而节省空间。于是生成器的概念就产生了。接下来举一个很有代表性的例子:计算斐波那契数列。网上关于迭代器的资料几乎都提到了这个例子。
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print b
a, b = b, a + b
n = n + 1
fib(6)
得到的结果为:
1
1
2
3
5
8
fib函数定义了斐波拉契数列的推算规则,每一次循环都可以打印对应的值。不过用print()并不适合函数的复用,而return又会直接导致函数返回。于是python提供了yield关键字,它的功能和return类似,也是向调用者“返回”值,一个函数一旦带上yield关键字,它就不再是一个普通函数,而是一个生成器(具体原理以后介绍)。大致的流程是,生成器在调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。切记不要从函数的角度去看待生成器,这样会很难理解,实在难以接受的话可以把函数当作一个“线程”,遇到yield则“中断”,调用next()继续执行,直至结束为止。接下来,我们写一个生成器,看看它到底有什么好处:
import sys
def generator():
i = 0
while i < 10000:
yield i
i += 1
g = generator
print('size of generator:'+str(sys.getsizeof(g)))
输出结果为:
size of generator:136
这个生成器可以生成0-9999共10000个数字,占用存储空间比上面的数组l1还要小。其实,第一个例子中的range()函数也是生成器:
import sys
print('size of range(10):'+str(sys.getsizeof(range(10))))
print('size of range(100):'+str(sys.getsizeof(range(100))))
print('size of range(1000):'+str(sys.getsizeof(range(1000))))
结果显示range(10),range(100),range(1000)占用的空间是相同的。
size of range(10):48
size of range(100):48
size of range(1000):48
所以,如果一个可迭代对象能够以生成器的方式实现,那就尽量用生成器实现。下一篇博文我会具体说说生成器的原理。