鍍金池/ 教程/ Python/ 生成器
標(biāo)準(zhǔn)庫 (4)
如何成為 Python 高手
標(biāo)準(zhǔn)庫 (6)
標(biāo)準(zhǔn)庫 (3)
類(2)
Pandas 使用 (2)
xml
用 tornado 做網(wǎng)站 (5)
文件(1)
練習(xí)
列表(3)
從小工到專家
除法
錯誤和異常 (2)
函數(shù)(1)
用 tornado 做網(wǎng)站 (7)
為做網(wǎng)站而準(zhǔn)備
函數(shù)練習(xí)
標(biāo)準(zhǔn)庫 (8)
Pandas 使用 (1)
回顧 list 和 str
字典(1)
用 tornado 做網(wǎng)站 (3)
字符串(1)
函數(shù)(2)
寫一個簡單的程序
將數(shù)據(jù)存入文件
語句(5)
SQLite 數(shù)據(jù)庫
集成開發(fā)環(huán)境(IDE)
集合(1)
類(1)
用 tornado 做網(wǎng)站 (6)
用 tornado 做網(wǎng)站 (2)
自省
語句(4)
錯誤和異常 (1)
用 tornado 做網(wǎng)站 (4)
集合(2)
列表(1)
標(biāo)準(zhǔn)庫 (1)
生成器
mysql 數(shù)據(jù)庫 (1)
第三方庫
實戰(zhàn)
運算符
類(3)
字典(2)
語句(1)
數(shù)和四則運算
語句(2)
文件(2)
MySQL 數(shù)據(jù)庫 (2)
電子表格
迭代器
mongodb 數(shù)據(jù)庫 (1)
特殊方法 (2)
特殊方法 (1)
字符編碼
編寫模塊
用 tornado 做網(wǎng)站 (1)
標(biāo)準(zhǔn)庫 (5)
函數(shù)(4)
類(5)
字符串(2)
關(guān)于 Python 的故事
函數(shù)(3)
字符串(4)
處理股票數(shù)據(jù)
常用數(shù)學(xué)函數(shù)和運算優(yōu)先級
字符串(3)
為計算做準(zhǔn)備
多態(tài)和封裝
類(4)
迭代
語句(3)
錯誤和異常 (3)
分析 Hello
Python 安裝
標(biāo)準(zhǔn)庫 (2)
列表(2)
元組

生成器

生成器(英文:generator)是一個非常迷人的東西,也常被認(rèn)為是 Python 的高級編程技能。不過,我依然很樂意在這里跟讀者——盡管你可能是一個初學(xué)者——探討這個話題,因為我相信讀者看本教程的目的,絕非僅僅將自己限制于初學(xué)者水平,一定有一顆不羈的心——要成為 Python 高手。那么,開始了解生成器吧。

還記得上節(jié)的“迭代器”嗎?生成器和迭代器有著一定的淵源關(guān)系。生成器必須是可迭代的,誠然它又不僅僅是迭代器,但除此之外,又沒有太多的別的用途,所以,我們可以把它理解為非常方便的自定義迭代器。

最這個關(guā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() 運算,不要以為里面少了一個括號,就是這么寫。是不是很迷人?如果列表,你不得不:

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

通過生成器解析式得到的生成器,掩蓋了生成器的一些細(xì)節(jié),并且適用領(lǐng)域也有限。下面就要剖析生成器的內(nèi)部,深入理解這個魔法工具。

定義和執(zhí)行過程

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ì)剖析一下上面的運行過程:

  1. ge = g():除了返回生成器之外,什么也沒有操作,任何值也沒有被返回。
  2. ge.next():直到這時候,生成器才開始執(zhí)行,遇到了第一個 yield 語句,將值返回,并暫停執(zhí)行(有的稱之為掛起)。
  3. ge.next():從上次暫停的位置開始,繼續(xù)向下執(zhí)行,遇到 yield 語句,將值返回,又暫停。
  4. gen.next():重復(fù)上面的操作。
  5. gene.next():從上面的掛起位置開始,但是后面沒有可執(zhí)行的了,于是 next() 發(fā)出異常。

從上面的執(zhí)行過程中,發(fā)現(xiàn) yield 除了作為生成器的標(biāo)志之外,還有一個功能就是返回值。那么它跟 return 這個返回值有什么區(qū)別呢?

yield

為了弄清楚 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 的特點,關(guā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                      #遇到 yield,返回值,并暫停
>>> yy.next()          #從上次暫停位置開始繼續(xù)執(zhí)行
after yield
before yield
2                      #又遇到 yield,返回值,并暫停
>>> 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 的特點了,也深知與 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 ,

運行結(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 以后,生成器有了一個新特征,就是在開始運行后能夠為生成器提供新的值。這就好似生成器和“外界”之間進(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ā)送的值返回。這就是在運行后能夠為生成器提供值的含義。

如果接下來再執(zhí)行 r.next() 會怎樣?

>>> r.next()

什么也沒有,其實就是返回了 None。按照前面的敘述,讀者可以看到,這次執(zhí)行 r.next(),由于沒有傳入任何值,yield 返回的就只能是 None.

還要注意,send() 方法必須在生成器運行后并掛起才能使用,也就是 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()

  • throw(type, value=None, traceback=None):用于在生成器內(nèi)部(生成器的當(dāng)前掛起處,或未啟動時在定義處)拋出一個異常(在 yield 表達(dá)式中)。
  • close():調(diào)用時不用參數(shù),用于關(guān)閉生成器。

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


總目錄   |   上節(jié):迭代器   |   下節(jié):錯誤和異常(1)

如果你認(rèn)為有必要打賞我,請通過支付寶:qiwsir@126.com,不勝感激。

上一篇:數(shù)和四則運算下一篇:除法