生成器
圣灵所结的果子,就是仁爱、喜乐、和平、忍耐、恩慈、良善、信实、温柔、节制。这样的事,没有律法禁止。凡属基督耶稣的人,是已经把肉体连肉体的邪情私欲同钉在十字架上了。我们若是靠圣灵得生,就当靠圣灵行事。不要贪图虚名,彼此惹气,互相嫉妒。(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
语句的作用,就是在调用的时候返回相应的值。详细剖析一下上面的运行过程:
ge = g()
:返回生成器之外。ge.next()
:生成器才开始执行,遇到了第一个yield语句,将值返回,并暂停执行(有的称之为挂起)。ge.next()
:从上次暂停的位置开始,继续向下执行,遇到yield语句,将值返回,又暂停。gen.next()
:重复上面的操作。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>>> rr3
从函数被调用的过程可以清晰看出,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 yield3 #遇到yield,返回值,并暂停>>> yy.next() #从上次暂停位置开始继续执行after yieldbefore yield2 #又遇到yield,返回值,并暂停>>> yy.next() #重复上述过程after yieldbefore yield1>>> 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-8def fibs(max): """ 斐波那契数列的生成器 """ n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n = n + 1if __name__ == "__main__": f = fibs(10) for i in f: print i , #Python 3: print(i, end=',')
运行结果如下:
$ python 21501.py1 1 2 3 5 8 13 21 34 55
用生成器方式实现的斐波那契数列是不是跟以前的有所不同了呢?读者可以将本书中已经演示过的斐波那契数列实现方式做一下对比,体会各种方法的差异。
经过上面的各种例子,已经明确,一个函数中,只要包含了yield
语句,它就是生成器,也是迭代器。这种方式显然比前面写迭代器的类要简便多了。但,并不意味着上节的就被抛弃。是生成器还是迭代器,都是根据具体的使用情景而定。
最后一句,你在编程中,不用生成器也可以。