圣灵所结的果子,就是仁爱、喜乐、和平、忍耐、恩慈、良善、信实、温柔、节制。这样的事,没有律法禁止。凡属基督耶稣的人,是已经把肉体连肉体的邪情私欲同钉在十字架上了。我们若是靠圣灵得生,就当靠圣灵行事。不要贪图虚名,彼此惹气,互相嫉妒。(GALATIANS 5:22-26)

生成器

上节中,我们曾经做过这样的操作:

>>> my_tup = (x**x for x in range(4))
>>> my_tup
<generator object <genexpr> at 0x02B7C2B0>

generator,翻译过来是生成器。

生成器是一个非常迷人的东西,也常被认为是Python的高级编程技能。不过,我依然很乐意在这里跟读者——尽管你可能是一个初学者——探讨这个话题,因为我相信读者看本教程的目的,绝非仅仅将自己限制于初学者水平,一定有一颗不羁的心——要成为Python高手。那么,开始了解生成器吧。

既然在探讨“迭代器”的时候出现了生成器,这就说明生成器和迭代器有着一定的渊源关系。

没错!生成器必须是可迭代的,它首先是迭代器。

但,它毕竟还是生成器,具有生成器的特质。

定义生成器

定义生成器,必须使用yield关键词。yield这个词在汉语中有“生产、出产”之意,在Python中,它作为一个关键词,是生成器的标志。

>>> def g():
...     yield 0
...     yield 1
...     yield 2

>>> g
<function g at 0xb71f3b8c>

建立了一个非常简单的函数,里面有yield发起的三个语句。下面看如何使用它:

>>> ge = g()
>>> ge
<generator object g at 0xb7200edc>
>>> type(ge)
<type 'generator'>

调用函数,得到了一个生成器(generator)对象。

Python 2:

>>> dir(ge)
['__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']

Python 3:

>>> dir(ge)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

在这里看到了__iter__()next()__next__(),虽然我们在函数体内并没有显示地写出__iter__()next()__next__(),仅仅写了yield语句,它就已经成为迭代器了。

既然如此,当然可以:

>>> ge.next()        #Python 3: ge.__next__(),下同,从略
0
>>> ge.next()
1
>>> ge.next()
2
>>> ge.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

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

于是可以这样定义:把含有yield语句的函数称作生成器。

生成器是一种用普通函数语法定义的迭代器。

通过上面的例子可以看出,这个生成器(也是迭代器),在定义过程中并没有像上节迭代器那样写__iter__()next(),而是只要用了yield语句,那个普通函数就神奇般地成为了生成器,也就具备了迭代器的功能特性。

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

  1. ge = g():返回生成器之外。
  2. ge.next():生成器才开始执行,遇到了第一个yield语句,将值返回,并暂停执行(有的称之为挂起)。
  3. ge.next():从上次暂停的位置开始,继续向下执行,遇到yield语句,将值返回,又暂停。
  4. gen.next():重复上面的操作。
  5. gene.next():从上面的挂起位置开始,但是后面没有可执行的了,于是next()发出异常。

从上面的执行过程中,发现yield除了作为生成器的标志之外,还有一个功能就是返回值。那么它跟return这个返回值有什么区别呢?

yield

函数返回值,本来已经有了一个return,现在又出现了yield,这两个有什么区别?

为了区别,我们写两个没有什么用途的函数:

>>> def r_return(n):
...     print "You taked me."        #Python 3: print("You taked me."),下同,从略
...     while n > 0:
...         print "before return"
...         return n
...         n -= 1
...         print "after return"
... 
>>> rr = r_return(3)
You taked me.
before return
>>> rr
3

从函数被调用的过程可以清晰看出,rr = r_return(3),函数体内的语句就开始执行了,遇到return,将值返回,然后就结束函数体内的执行。所以return后面的语句根本没有执行。这是return的特点,关于此特点的详细说明请阅读《函数(2)》中的返回值相关内容

下面将return改为yield:

>>> def y_yield(n):
...     print "You taked me."    #Python 3: print("You taked me."),下同,从略
...     while n > 0:
...         print "before yield"
...         yield n
...         n -= 1
...         print "after yield"
... 
>>> yy = y_yield(3)    #没有执行函数体内语句
>>> yy.next()          #Python 3: yy.__next__(),下同,从略
You taked me.
before yield
3                      #遇到yield,返回值,并暂停
>>> yy.next()          #从上次暂停位置开始继续执行
after yield
before yield
2                      #又遇到yield,返回值,并暂停
>>> yy.next()          #重复上述过程
after yield
before yield
1
>>> yy.next()
after yield            #没有满足条件的值,抛出异常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

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

一般的函数,都是止于return。作为生成器的函数,由于有了yield,则会遇到它挂起。

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

#!/usr/bin/env python
# coding=utf-8

def fibs(max):
    """
    斐波那契数列的生成器
    """
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1

if __name__ == "__main__":
    f = fibs(10)
    for i in f:
        print i ,        #Python 3: print(i, end=',')

运行结果如下:

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

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

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

最后一句,你在编程中,不用生成器也可以。


总目录   |   上节:迭代器   |   下节:上下文管理器

如果你认为有必要打赏我,请通过支付宝:[email protected],不胜感激。

results matching ""

    No results matching ""