玩转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

所以,如果一个可迭代对象能够以生成器的方式实现,那就尽量用生成器实现。下一篇博文我会具体说说生成器的原理。

posted @ 2018-05-18 09:09  bubingy  阅读(190)  评论(0编辑  收藏  举报