4.9 生成器

生成器(generator)是一个非常迷人的东西,也常被认为是Python的高级编程技能。我很乐意在这里跟读者探讨这个话题,因为我相信读者看本书的目的绝非仅仅将自己限制于初学者水平,一定有一颗不羁的心——要成为Python高手。于是乎,需要了解生成器。

“迭代器”已经是很熟悉了吧?生成器和迭代器有着一定的渊源。首先生成器必须是可迭代的,但它又不完全等同于迭代器。

4.9.1 简单的生成器

  1. >>> my_generator = (x*x for x in range(4))

这是不是跟列表解析很类似呢?仔细观察,它不是列表,像下面这样得到的才是列表:

  1. >>> my_list = [x*x for x in range(4)]

以上两者的区别在于前者是方括号“[]”后者是圆括号“()”,虽然是细小的差别,但是结果完全不一样。

  1. >>> dir(my_generator)
  2. ['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__',
  3. '__iter__',
  4. '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running',
  5. 'next',
  6. 'send', 'throw']

为了容易观察,将原来得到的结果进行了重新排版。是不是发现了在迭代器中必有的方法inter()和next()?这说明my_generator是可迭代的,可以用for循环来依次读出其值。

  1. >>> for i in my_generator:
  2. ... print i
  3. ...
  4. 0
  5. 1
  6. 4
  7. 9
  8. >>> for i in my_generator:
  9. ... print i
  10. ...

当第一遍循环的时候,将my_generator里面的值依次读出并打印,但是,若再读一次,就发现没有任何结果(游标已经移动到最后了),这种特性也正是迭代器所具有的。

如果对那个列表,就不一样了:

  1. >>> for i in my_list:
  2. ... print i
  3. ...
  4. 0
  5. 1
  6. 4
  7. 9
  8. >>> for i in my_list:
  9. ... print i
  10. ...
  11. 0
  12. 1
  13. 4
  14. 9

难道生成器就是把列表解析中的“[]”换成“()”这么简单吗?这仅仅是生成器的一种表现形式和基本使用方法罢了,仿照列表解析式的命名,可以称之为“生成器解析式”(或者:生成器推导式、生成器表达式)。

生成器解析式有很多用途,在不少地方可以替代列表解析,特别是针对大数据的时候,Python处理列表时,将全部数据都读入到内存,而迭代器(生成器是迭代器)的优势就在于只将所需要的读入内存里,因此生成器解析式比列表解析式少占内存,再看实例:

  1. >>> sum(i*i for i in range(10))
  2. 285

这个例子是计算1到10以内的自然数的平方和,请观察sum()运算,这样做是不是感觉很迷人?如果是列表,你不得不:

  1. >>> sum([i*i for i in range(10)])
  2. 285

虽然生成器解析式貌似不错,但是对其真正的含义,还需要我们做深入探究才能揭晓。

4.9.2 定义和执行过程

yield这个词在汉语中有“生产、出产”之意,在Python中,它作为一个关键词(变量、函数、类的名称中不能用它来命名),是生成器的标志。

  1. >>> def g():
  2. ... yield 0
  3. ... yield 1
  4. ... yield 2
  5. ...
  6. >>> g
  7. <function g at 0xb71f3b8c>

建立了一个非常简单的函数,跟以往看到的函数唯一不同的地方是用了三个yield语句。然后进行下面的操作:

  1. >>> ge = g()
  2. >>> ge
  3. <generator object g at 0xb7200edc>
  4. >>> type(ge)
  5. <type 'generator'>

上面建立的函数返回值是一个生成器(generator)类型的对象。

  1. >>> dir(ge)
  2. ['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']

在这里看到了iter()和next(),说明它是迭代器。既然如此,当然可以:

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

从例子中可以看出,含有yield关键词的函数是一个生成器类型的对象,这个生成器对象是可迭代的。

我们把含有yield语句的函数称作生成器,生成器是一种用普通函数语法定义的迭代器。

通过上面的例子可以看出,这个生成器在定义过程中并没有显化地使用inter()和next(),而是只要用了yield语句(yield关键词发起的语句),那个普通函数就神奇般地成为了生成器,也就具备了迭代器的功能特性。

yield语句的作用就是在调用的时候返回相应的值。下面详细剖析一下上面的运行过程。

  • ge=g():除了返回生成器之外,什么也没有操作,任何值也没有被返回。
  • ge.next():直到这时候,生成器才开始执行,遇到了第一个yield语句,将值返回,并暂停执行(有的称之为挂起)。
  • ge.next():从上次暂停的位置开始,继续向下执行,遇到yield语句,将值返回,又暂停。
  • gen.next():含义与ge.next()相同。
  • gene.next():从上面的挂起位置开始,但是后面没有可执行的了,于是next()发出异常。从上面的执行过程中会发现yield除了作为生成器的标志之外,还有一个功能就是返回值。那么它跟return这个返回值有什么区别呢?

4.9.3 yield

为了弄清楚yield和return的区别,我们写两个没有什么用途的函数:

  1. >>> def r_return(n):
  2. ... print "You taked me."
  3. ... while n > 0:
  4. ... print "before return"
  5. ... return n
  6. ... n -= 1
  7. ... print "after return"
  8. ...
  9. >>> rr = r_return(3)
  10. You taked me.
  11. before return
  12. >>> rr
  13. 3

从函数被调用的过程可以清晰看出,从rr=r_return(3)开始,就执行函数体内容了,当遇到return的时候执行该语句,将值返回,然后就结束函数体内的执行,所以return后面的语句根本没有执行。

如果将return改为yield:

  1. >>> def y_yield(n):
  2. ... print "You taked me."
  3. ... while n > 0:
  4. ... print "before yield"
  5. ... yield n
  6. ... n -= 1
  7. ... print "after yield"
  8. ...
  9. >>> yy = y_yield(3) #没有执行函数体内语句
  10. >>> yy.next() #开始执行
  11. You taked me.
  12. before yield
  13. 3 #遇到yield,返回值,并暂停
  14. >>> yy.next() #从上次暂停位置开始继续执行
  15. after yield
  16. before yield
  17. 2 #又遇到yield,返回值,并暂停
  18. >>> yy.next()
  19. after yield
  20. before yield
  21. 1
  22. >>> yy.next()
  23. after yield #没有满足条件的值,抛出异常
  24. Traceback (most recent call last):
  25. File "<stdin>", line 1, in <module>
  26. StopIteration

结合注释和前面对执行过程的分析,读者一定能理解yield的特点了,也深知与return的区别了。

一般的函数,都是止于return。作为生成器的函数,由于有了yield,遇到它则程序挂起,如果在之后还有return,遇到它就直接抛出SoptIteration异常而中止迭代。

斐波那契数列已经是老相识了,不论是循环还是迭代都用它举例过,现在还用它举例,只不过要用上yield。

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3.  
  4. def fibs(max):
  5. n, a, b = 0, 0, 1
  6. while n < max:
  7. yield b
  8. a, b = b, a + b
  9. n = n + 1
  10.  
  11. if __name__ == "__main__":
  12. f = fibs(10)
  13. for i in f:
  14. print i ,

运行结果如下:

  1. $ python 21501.py
  2. 1 1 2 3 5 8 13 21 34 55

用生成器方式实现的斐波那契数列是不是跟以前的有所不同了呢?读者可以将本书中已经演示过的斐波那契数列实现做对比,体会各种方法的差异。

至此,已经明确,一个函数中,只要包含了yield语句,它就是生成器,也是迭代器。这种方式显然比前面写迭代器的类要简便多了,但这并不意味着迭代器就被抛弃,是用生成器还是用迭代器要根据具体的使用情景而定。

4.9.4 生成器方法

在Python2.5以后,生成器有了一个新特征,就是在开始运行后能够为生成器提供新的值。这就好似生成器和“外界”之间进行数据交流。

  1. >>> def repeater(n):
  2. ... while True:
  3. ... n = (yield n)
  4. ...
  5. >>> r = repeater(4)
  6. >>> r.next()
  7. 4
  8. >>> r.send("hello")
  9. 'hello'

当执行到r.next()的时候,生成器开始执行,在内部遇到了yield n挂起。注意在生成器函数中,n=(yield n)中的yield n是一个表达式,并将结果赋值给n,虽然不严格要求它必须用圆括号包裹,但是一般情况都这么做,请读者也追随这个习惯。

当执行r.send("hello")的时候,原来已经被挂起的生成器(函数)又被唤醒,开始执行n=(yield n),并将send()方法发送的值返回,这就是在运行后能够为生成器提供值的含义。

如果接下来再执行r.next()会怎样?

  1. >>> r.next()

什么也没有,其实就是返回了None。按照前面的叙述,这次执行r.next(),由于没有给函数的参数传入任何值,yield返回的就只能是None.

还要注意,send()方法必须在生成器运行后并挂起才能使用,即yield至少被执行一次。如果像下面一样就要报错了。

  1. >>> s = repeater(5)
  2. >>> s.send("how")
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. TypeError: can't send non-None value to a just-started generator

承接上面的操作,如果将send()的参数设为None,就会把刚才输入的数值返回。

  1. >>> s.send(None)
  2. 5

此外,还有两个方法:close()和throw()。

  • throw(type,value=None,traceback=None):用于在生成器内部(生成器的当前挂起处或未启动时在定义处)抛出一个异常(在yield表达式中)。
  • close():调用时不用参数,用于关闭生成器。本节最后一句:你在编程中,当然可以不用生成器。