生成器(英文:generator)是一個非常迷人的東西,也常被認(rèn)為是python的高級編程技能。不過,我依然很樂意在這里跟讀者——盡管你可能是一個初學(xué)者——探討這個話題,因為我相信讀者看本教程的目的,絕非僅僅將自己限制于初學(xué)者水平,一定有一顆不羈的心——要成為python高手。那么,開始了解生成器吧。
還記得上節(jié)的“迭代器”嗎?生成器和迭代器有著一定的淵源關(guān)系。生成器必須是可迭代的,誠然它又不僅僅是迭代器,但除此之外,又沒有太多的別的用途,所以,我們可以把它理解為非常方便的自定義迭代器。
最這個關(guān)系實在感覺有點(diǎn)糊涂了。稍安勿躁,繼續(xù)閱讀即明了。
>>> my_generator = (x*x for x in range(4))
這是不是跟列表解析很類似呢?仔細(xì)觀察,它不是列表,如果這樣的得到的才是列表:
>>> my_list = [x*x for x in range(4)]
以上兩的區(qū)別在于是[]
還是()
,雖然是細(xì)小的差別,但是結(jié)果完全不一樣。
>>> dir(my_generator)
['__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']
為了容易觀察,我將上述結(jié)果進(jìn)行了重新排版。是不是發(fā)現(xiàn)了在迭代器中必有的方法__inter__()
和next()
,這說明它是迭代器。如果是迭代器,就可以用for循環(huán)來依次讀出其值。
>>> for i in my_generator:
... print i
...
0
1
4
9
>>> for i in my_generator:
... print i
...
當(dāng)?shù)谝槐檠h(huán)的時候,將my_generator里面的值依次讀出并打印,但是,當(dāng)再讀一次的時候,就發(fā)現(xiàn)沒有任何結(jié)果。這種特性也正是迭代器所具有的。
如果對那個列表,就不一樣了:
>>> for i in my_list:
... print i
...
0
1
4
9
>>> for i in my_list:
... print i
...
0
1
4
9
難道生成器就是把列表解析中的[]
換成()
就行了嗎?這僅僅是生成器的一種表現(xiàn)形式和使用方法罷了,仿照列表解析式的命名,可以稱之為“生成器解析式”(或者:生成器推導(dǎo)式、生成器表達(dá)式)。
生成器解析式是有很多用途的,在不少地方替代列表,是一個不錯的選擇。特別是針對大量值的時候,如上節(jié)所說的,列表占內(nèi)存較多,迭代器(生成器是迭代器)的優(yōu)勢就在于少占內(nèi)存,因此無需將生成器(或者說是迭代器)實例化為一個列表,直接對其進(jìn)行操作,方顯示出其迭代的優(yōu)勢。比如:
>>> sum(i*i for i in range(10))
285
請讀者注意觀察上面的sum()
運(yùn)算,不要以為里面少了一個括號,就是這么寫。是不是很迷人?如果列表,你不得不:
>>> sum([i*i for i in range(10)])
285
通過生成器解析式得到的生成器,掩蓋了生成器的一些細(xì)節(jié),并且適用領(lǐng)域也有限。下面就要剖析生成器的內(nèi)部,深入理解這個魔法工具。
yield這個詞在漢語中有“生產(chǎn)、出產(chǎn)”之意,在python中,它作為一個關(guān)鍵詞(你在變量、函數(shù)、類的名稱中就不能用這個了),是生成器的標(biāo)志。
>>> def g():
... yield 0
... yield 1
... yield 2
...
>>> g
<function g at 0xb71f3b8c>
建立了一個非常簡單的函數(shù),跟以往看到的函數(shù)唯一不同的地方是用了三個yield語句。然后進(jìn)行下面的操作:
>>> ge = g()
>>> ge
<generator object g at 0xb7200edc>
>>> type(ge)
<type 'generator'>
上面建立的函數(shù)返回值是一個生成器(generator)類型的對象。
>>> 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']
在這里看到了__iter__()
和next()
,說明它是迭代器。既然如此,當(dāng)然可以:
>>> ge.next()
0
>>> ge.next()
1
>>> ge.next()
2
>>> ge.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
從這個簡單例子中可以看出,那個含有yield關(guān)鍵詞的函數(shù)返回值是一個生成器類型的對象,這個生成器對象就是迭代器。
我們把含有yield語句的函數(shù)稱作生成器。生成器是一種用普通函數(shù)語法定義的迭代器。通過上面的例子可以看出,這個生成器(也是迭代器),在定義過程中并沒有像上節(jié)迭代器那樣寫__inter__()
和next()
,而是只要用了yield語句,那個普通函數(shù)就神奇般地成為了生成器,也就具備了迭代器的功能特性。
yield語句的作用,就是在調(diào)用的時候返回相應(yīng)的值。詳細(xì)剖析一下上面的運(yùn)行過程:
ge = g()
:除了返回生成器之外,什么也沒有操作,任何值也沒有被返回。ge.next()
:直到這時候,生成器才開始執(zhí)行,遇到了第一個yield語句,將值返回,并暫停執(zhí)行(有的稱之為掛起)。ge.next()
:從上次暫停的位置開始,繼續(xù)向下執(zhí)行,遇到y(tǒng)ield語句,將值返回,又暫停。gen.next()
:重復(fù)上面的操作。gene.next()
:從上面的掛起位置開始,但是后面沒有可執(zhí)行的了,于是next()
發(fā)出異常。從上面的執(zhí)行過程中,發(fā)現(xiàn)yield除了作為生成器的標(biāo)志之外,還有一個功能就是返回值。那么它跟return這個返回值有什么區(qū)別呢?
為了弄清楚yield和return的區(qū)別,我們寫兩個沒有什么用途的函數(shù):
>>> def r_return(n):
... 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
從函數(shù)被調(diào)用的過程可以清晰看出,rr = r_return(3)
,函數(shù)體內(nèi)的語句就開始執(zhí)行了,遇到return,將值返回,然后就結(jié)束函數(shù)體內(nèi)的執(zhí)行。所以return后面的語句根本沒有執(zhí)行。這是return的特點(diǎn),關(guān)于此特點(diǎn)的詳細(xì)說明請閱讀《函數(shù)(2)》中的返回值相關(guān)內(nèi)容。
下面將return改為yield:
>>> def y_yield(n):
... print "You taked me."
... while n > 0:
... print "before yield"
... yield n
... n -= 1
... print "after yield"
...
>>> yy = y_yield(3) #沒有執(zhí)行函數(shù)體內(nèi)語句
>>> yy.next() #開始執(zhí)行
You taked me.
before yield
3 #遇到y(tǒng)ield,返回值,并暫停
>>> yy.next() #從上次暫停位置開始繼續(xù)執(zhí)行
after yield
before yield
2 #又遇到y(tǒng)ield,返回值,并暫停
>>> yy.next() #重復(fù)上述過程
after yield
before yield
1
>>> yy.next()
after yield #沒有滿足條件的值,拋出異常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
結(jié)合注釋和前面對執(zhí)行過程的分析,讀者一定能理解yield的特點(diǎn)了,也深知與return的區(qū)別了。
一般的函數(shù),都是止于return。作為生成器的函數(shù),由于有了yield,則會遇到它掛起,如果還有return,遇到它就直接拋出SoptIteration異常而中止迭代。
斐波那契數(shù)列已經(jīng)是老相識了。不論是循環(huán)、迭代都用它舉例過,現(xiàn)在讓我們還用它吧,只不過是要用上yield:
#!/usr/bin/env python
# coding=utf-8
def fibs(max):
"""
斐波那契數(shù)列的生成器
"""
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 ,
運(yùn)行結(jié)果如下:
$ python 21501.py
1 1 2 3 5 8 13 21 34 55
用生成器方式實現(xiàn)的斐波那契數(shù)列是不是跟以前的有所不同了呢?讀者可以將本教程中已經(jīng)演示過的斐波那契數(shù)列實現(xiàn)方式做一下對比,體會各種方法的差異。
經(jīng)過上面的各種例子,已經(jīng)明確,一個函數(shù)中,只要包含了yield語句,它就是生成器,也是迭代器。這種方式顯然比前面寫迭代器的類要簡便多了。但,并不意味著上節(jié)的就被拋棄。是生成器還是迭代器,都是根據(jù)具體的使用情景而定。
在python2.5以后,生成器有了一個新特征,就是在開始運(yùn)行后能夠為生成器提供新的值。這就好似生成器和“外界”之間進(jìn)行數(shù)據(jù)交流。
>>> def repeater(n):
... while True:
... n = (yield n)
...
>>> r = repeater(4)
>>> r.next()
4
>>> r.send("hello")
'hello'
當(dāng)執(zhí)行到r.next()
的時候,生成器開始執(zhí)行,在內(nèi)部遇到了yield n
掛起。注意在生成器函數(shù)中,n = (yield n)
中的yield n
是一個表達(dá)式,并將結(jié)果賦值給n,雖然不嚴(yán)格要求它必須用圓括號包裹,但是一般情況都這么做,請讀者也追隨這個習(xí)慣。
當(dāng)執(zhí)行r.send("hello")
的時候,原來已經(jīng)被掛起的生成器(函數(shù))又被喚醒,開始執(zhí)行n = (yield n)
,也就是講send()方法發(fā)送的值返回。這就是在運(yùn)行后能夠為生成器提供值的含義。
如果接下來再執(zhí)行r.next()
會怎樣?
>>> r.next()
什么也沒有,其實就是返回了None。按照前面的敘述,讀者可以看到,這次執(zhí)行r.next()
,由于沒有傳入任何值,yield返回的就只能是None.
還要注意,send()方法必須在生成器運(yùn)行后并掛起才能使用,也就是yield至少被執(zhí)行一次。如果不是這樣:
>>> s = repeater(5)
>>> s.send("how")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
就報錯了。但是,可將參數(shù)設(shè)為None:
>>> s.send(None)
5
這是返回的是調(diào)用函數(shù)的時傳入的值。
此外,還有兩個方法:close()和throw()
最后一句,你在編程中,不用生成器也可以。
更多建議: