迭代器和生成器

1. 迭代器協議

由于生成器自動實現了迭代器協議,而迭代器協議對很多人來說,也是一個較為抽象的概念。所以,為了更好的理解生成器,我們需要簡單的回顧一下迭代器協議的概念。

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

舉個例子:在所有語言中,我們都可以使用for循環來遍歷數組,Python的list底層實現是一個數組,所以,我們可以使用for循環來遍歷list。如下所示:

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

但是,對Python稍微熟悉一點的朋友應該知道,Python的for循環不但可以用來遍歷list,還可以用來遍歷文件對象,如下所示:

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

為什么在Python中,文件還可以使用for循環進行遍歷呢?這是因為,在Python中,文件對象實現了迭代器協議,for循環并不知道它遍歷的是一個文件對象,它只管使用迭代器協議訪問對象即可。正是由于Python的文件對象實現了迭代器協議,我們才得以使用如此方便的方式訪問文件,如下所示:

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

2. 生成器

Python使用生成器對延遲操作提供了支持。所謂延遲操作,是指在需要的時候才產生結果,而不是立即產生結果。這也是生成器的主要好處。

Python有兩種不同的方式提供生成器:
1)生成器函數:常規函數定義,但是,使用yield語句而不是return語句返回結果。yield語句一次返回一個結果,在每個結果中間,掛起函數的狀態,以便下次重它離開的地方繼續執行
2)生成器表達式:類似于列表推導,但是,生成器返回按需產生結果的一個對象,而不是一次構建一個結果列表

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

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

for item in gensquares(5):
    print item,

使用普通函數:

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

for item in gensquares(5):
    print item,

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

2.2 生成器表達式
使用列表推導,將會一次產生所有結果:

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

將列表推導的中括號,替換成圓括號,就是一個生成器表達式:

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

Python不但使用迭代器協議,讓for循環變得更加通用。大部分內置函數,也是使用迭代器協議訪問對象的。例如, sum函數是Python的內置函數,該函數使用迭代器協議訪問對象,而生成器實現了迭代器協議,所以,我們可以直接這樣計算一系列值的和:

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

而不用多此一舉的先構造一個列表:

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

2.3 再看生成器
前面已經對生成器有了感性的認識,我們以生成器函數為例,再來深入探討一下Python的生成器:
1)語法上和函數類似:生成器函數和常規函數幾乎是一樣的。它們都是使用def語句進行定義,差別在于,生成器使用yield語句返回一個值,而常規函數使用return語句返回一個值
2)自動實現迭代器協議:對于生成器,Python會自動實現迭代器協議,以便應用到迭代背景中(如for循環,sum函數)。由于生成器自動實現了迭代器協議,所以,我們可以調用它的next方法,并且,在沒有值可以返回的時候,生成器自動產生StopIteration異常
3)狀態掛起:生成器使用yield語句返回一個值。yield語句掛起該生成器函數的狀態,保留足夠的信息,以便之后從它離開的地方繼續執行

3. 示例

我們再來看兩個生成器的例子,以便大家更好的理解生成器的作用。

首先,生成器的好處是延遲計算,一次返回一個結果。也就是說,它不會一次生成所有的結果,這對于大數據量處理,將會非常有用。

大家可以在自己電腦上試試下面兩個表達式,并且觀察內存占用情況。對于前一個表達式,我在自己的電腦上進行測試,還沒有看到最終結果電腦就已經卡死,對于后一個表達式,幾乎沒有什么內存占用。

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

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

不使用生成器的情況:

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

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

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

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

4. 使用生成器的注意事項

相信通過這篇文章,大家已經能夠理解生成器的作用和好處。但是,還沒有結束,使用生成器,也有一點注意事項。

我們直接來看例子,假設文件中保存了每個省份的人口總數,現在,需要求每個省份的人口占全國總人口的比例。顯然,我們需要先求出全國的總人口,然后在遍歷每個省份的人口,用每個省的人口數除以總人口數,就得到了每個省份的人口占全國人口的比例。

如下所示:

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

執行上面這段代碼,將不會有任何輸出,這是因為,生成器只能遍歷一次。在我們執行sum語句的時候,就遍歷了我們的生成器,當我們再次遍歷我們的生成器的時候,將不會有任何記錄。所以,上面的代碼不會有任何輸出。
因此,生成器的唯一注意事項就是:生成器只能遍歷一次。

5. 總結

本文深入淺出地介紹了Python中,一個容易被大家忽略的重要特性,即Python的生成器。為了講解生成器,本文先介紹了迭代器協議,然后介紹了生成器函數和生成器表達式,并通過示例演示了生成器的優點和注意事項。在實際工作中,充分利用Python生成器,不但能夠減少內存使用,還能夠提高代碼可讀性。掌握生成器也是Python高手的標配。希望本文能夠幫助大家理解Python的生成器。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容