迭代器和生成器

1. 迭代器協(xié)議

由于生成器自動(dòng)實(shí)現(xiàn)了迭代器協(xié)議,而迭代器協(xié)議對(duì)很多人來(lái)說(shuō),也是一個(gè)較為抽象的概念。所以,為了更好的理解生成器,我們需要簡(jiǎn)單的回顧一下迭代器協(xié)議的概念。

1)迭代器協(xié)議是指:對(duì)象需要提供next方法,它要么返回迭代中的下一項(xiàng),要么就引起一個(gè)StopIteration異常,以終止迭代
2)可迭代對(duì)象就是:實(shí)現(xiàn)了迭代器協(xié)議的對(duì)象
3)協(xié)議是一種約定,可迭代對(duì)象實(shí)現(xiàn)迭代器協(xié)議,Python的內(nèi)置工具(如for循環(huán),sum,min,max函數(shù)等)使用迭代器協(xié)議訪問(wèn)對(duì)象。

舉個(gè)例子:在所有語(yǔ)言中,我們都可以使用for循環(huán)來(lái)遍歷數(shù)組,Python的list底層實(shí)現(xiàn)是一個(gè)數(shù)組,所以,我們可以使用for循環(huán)來(lái)遍歷list。如下所示:

>>> for n in [1, 2, 3, 4]:
...     print n

但是,對(duì)Python稍微熟悉一點(diǎn)的朋友應(yīng)該知道,Python的for循環(huán)不但可以用來(lái)遍歷list,還可以用來(lái)遍歷文件對(duì)象,如下所示:

>>> with open(‘/etc/passwd’) as f: # 文件對(duì)象提供迭代器協(xié)議
...     for line in f: # for循環(huán)使用迭代器協(xié)議訪問(wèn)文件
...         print line

為什么在Python中,文件還可以使用for循環(huán)進(jìn)行遍歷呢?這是因?yàn)?,在Python中,文件對(duì)象實(shí)現(xiàn)了迭代器協(xié)議,for循環(huán)并不知道它遍歷的是一個(gè)文件對(duì)象,它只管使用迭代器協(xié)議訪問(wèn)對(duì)象即可。正是由于Python的文件對(duì)象實(shí)現(xiàn)了迭代器協(xié)議,我們才得以使用如此方便的方式訪問(wèn)文件,如下所示:

>>> f = open('/etc/passwd')
>>> dir(f)
['__class__', '__enter__', '__exit__', '__iter__', '__next__', 'writelines', '...'

2. 生成器

Python使用生成器對(duì)延遲操作提供了支持。所謂延遲操作,是指在需要的時(shí)候才產(chǎn)生結(jié)果,而不是立即產(chǎn)生結(jié)果。這也是生成器的主要好處。

Python有兩種不同的方式提供生成器:
1)生成器函數(shù):常規(guī)函數(shù)定義,但是,使用yield語(yǔ)句而不是return語(yǔ)句返回結(jié)果。yield語(yǔ)句一次返回一個(gè)結(jié)果,在每個(gè)結(jié)果中間,掛起函數(shù)的狀態(tài),以便下次重它離開(kāi)的地方繼續(xù)執(zhí)行
2)生成器表達(dá)式:類似于列表推導(dǎo),但是,生成器返回按需產(chǎn)生結(jié)果的一個(gè)對(duì)象,而不是一次構(gòu)建一個(gè)結(jié)果列表

2.1 生成器函數(shù)
我們來(lái)看一個(gè)例子,使用生成器返回自然數(shù)的平方(注意返回的是多個(gè)值):

def gensquares(N):
    for i in range(N):
        yield i ** 2

for item in gensquares(5):
    print item,

使用普通函數(shù):

def gensquares(N):
    res = []
    for i in range(N):
        res.append(i*i)
    return res

for item in gensquares(5):
    print item,

可以看到,使用生成器函數(shù)代碼量更少。

2.2 生成器表達(dá)式
使用列表推導(dǎo),將會(huì)一次產(chǎn)生所有結(jié)果:

>>> squares = [x**2 for x in range(5)]
>>> squares
[0, 1, 4, 9, 16]

將列表推導(dǎo)的中括號(hào),替換成圓括號(hào),就是一個(gè)生成器表達(dá)式:

>>> squares = (x**2 for x in range(5))
>>> squares
<generator object at 0x00B2EC88>
>>> next(squares)
0
...
>>> next(squares)
4
>>> list(squares)
[9, 16]

Python不但使用迭代器協(xié)議,讓for循環(huán)變得更加通用。大部分內(nèi)置函數(shù),也是使用迭代器協(xié)議訪問(wèn)對(duì)象的。例如, sum函數(shù)是Python的內(nèi)置函數(shù),該函數(shù)使用迭代器協(xié)議訪問(wèn)對(duì)象,而生成器實(shí)現(xiàn)了迭代器協(xié)議,所以,我們可以直接這樣計(jì)算一系列值的和:

>>> sum(x ** 2 for x in xrange(4)) 

而不用多此一舉的先構(gòu)造一個(gè)列表:

>>> sum([x ** 2 for x in xrange(4)]) 

2.3 再看生成器
前面已經(jīng)對(duì)生成器有了感性的認(rèn)識(shí),我們以生成器函數(shù)為例,再來(lái)深入探討一下Python的生成器:
1)語(yǔ)法上和函數(shù)類似:生成器函數(shù)和常規(guī)函數(shù)幾乎是一樣的。它們都是使用def語(yǔ)句進(jìn)行定義,差別在于,生成器使用yield語(yǔ)句返回一個(gè)值,而常規(guī)函數(shù)使用return語(yǔ)句返回一個(gè)值
2)自動(dòng)實(shí)現(xiàn)迭代器協(xié)議:對(duì)于生成器,Python會(huì)自動(dòng)實(shí)現(xiàn)迭代器協(xié)議,以便應(yīng)用到迭代背景中(如for循環(huán),sum函數(shù))。由于生成器自動(dòng)實(shí)現(xiàn)了迭代器協(xié)議,所以,我們可以調(diào)用它的next方法,并且,在沒(méi)有值可以返回的時(shí)候,生成器自動(dòng)產(chǎn)生StopIteration異常
3)狀態(tài)掛起:生成器使用yield語(yǔ)句返回一個(gè)值。yield語(yǔ)句掛起該生成器函數(shù)的狀態(tài),保留足夠的信息,以便之后從它離開(kāi)的地方繼續(xù)執(zhí)行

3. 示例

我們?cè)賮?lái)看兩個(gè)生成器的例子,以便大家更好的理解生成器的作用。

首先,生成器的好處是延遲計(jì)算,一次返回一個(gè)結(jié)果。也就是說(shuō),它不會(huì)一次生成所有的結(jié)果,這對(duì)于大數(shù)據(jù)量處理,將會(huì)非常有用。

大家可以在自己電腦上試試下面兩個(gè)表達(dá)式,并且觀察內(nèi)存占用情況。對(duì)于前一個(gè)表達(dá)式,我在自己的電腦上進(jìn)行測(cè)試,還沒(méi)有看到最終結(jié)果電腦就已經(jīng)卡死,對(duì)于后一個(gè)表達(dá)式,幾乎沒(méi)有什么內(nèi)存占用。

sum([i for i in xrange(10000000000)])
sum(i for i in xrange(10000000000))

除了延遲計(jì)算,生成器還能有效提高代碼可讀性。例如,現(xiàn)在有一個(gè)需求,求一段文字中,每個(gè)單詞出現(xiàn)的位置。

不使用生成器的情況:

def index_words(text):
    result = []
    if text:
        result.append(0)
    for index, letter in enumerate(text, 1):
        if letter == ' ':
            result.append(index)
    return result

使用生成器的情況:

def index_words(text):
    if text:
        yield 0
    for index, letter in enumerate(text, 1):
        if letter == ' ':
            yield index

這里,至少有兩個(gè)充分的理由說(shuō)明 ,使用生成器比不使用生成器代碼更加清晰:
1)使用生成器以后,代碼行數(shù)更少。大家要記住,如果想把代碼寫(xiě)的Pythonic,在保證代碼可讀性的前提下,代碼行數(shù)越少越好

2)不使用生成器的時(shí)候,對(duì)于每次結(jié)果,我們首先看到的是result.append(index),其次,才是index。也就是說(shuō),我們每次看到的是一個(gè)列表的append操作,只是append的是我們想要的結(jié)果。使用生成器的時(shí)候,直接yield index,少了列表append操作的干擾,我們一眼就能夠看出,代碼是要返回index。

這個(gè)例子充分說(shuō)明了,合理使用生成器,能夠有效提高代碼可讀性。只要大家完全接受了生成器的概念,理解了yield語(yǔ)句和return語(yǔ)句一樣,也是返回一個(gè)值。那么,就能夠理解為什么使用生成器比不使用生成器要好,能夠理解使用生成器真的可以讓代碼變得清晰易懂。

4. 使用生成器的注意事項(xiàng)

相信通過(guò)這篇文章,大家已經(jīng)能夠理解生成器的作用和好處。但是,還沒(méi)有結(jié)束,使用生成器,也有一點(diǎn)注意事項(xiàng)。

我們直接來(lái)看例子,假設(shè)文件中保存了每個(gè)省份的人口總數(shù),現(xiàn)在,需要求每個(gè)省份的人口占全國(guó)總?cè)丝诘谋壤?。顯然,我們需要先求出全國(guó)的總?cè)丝冢缓笤诒闅v每個(gè)省份的人口,用每個(gè)省的人口數(shù)除以總?cè)丝跀?shù),就得到了每個(gè)省份的人口占全國(guó)人口的比例。

如下所示:

def get_province_population(filename):
    with open(filename) as f:
        for line in f:
            yield int(line)
gen = get_province_population('data.txt')
all_population = sum(gen)
#print all_population
for population in gen:
    print population / all_population

執(zhí)行上面這段代碼,將不會(huì)有任何輸出,這是因?yàn)?,生成器只能遍歷一次。在我們執(zhí)行sum語(yǔ)句的時(shí)候,就遍歷了我們的生成器,當(dāng)我們?cè)俅伪闅v我們的生成器的時(shí)候,將不會(huì)有任何記錄。所以,上面的代碼不會(huì)有任何輸出。
因此,生成器的唯一注意事項(xiàng)就是:生成器只能遍歷一次。

5. 總結(jié)

本文深入淺出地介紹了Python中,一個(gè)容易被大家忽略的重要特性,即Python的生成器。為了講解生成器,本文先介紹了迭代器協(xié)議,然后介紹了生成器函數(shù)和生成器表達(dá)式,并通過(guò)示例演示了生成器的優(yōu)點(diǎn)和注意事項(xiàng)。在實(shí)際工作中,充分利用Python生成器,不但能夠減少內(nèi)存使用,還能夠提高代碼可讀性。掌握生成器也是Python高手的標(biāo)配。希望本文能夠幫助大家理解Python的生成器。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容