1. 程式人生 > >一張圖明白python 生成器/迭代器/可迭代物件 之間的關係

一張圖明白python 生成器/迭代器/可迭代物件 之間的關係

python中迭代器與生成器,相信學的時候,很多小夥伴都會感到頭疼,一會迭代器,一會生成器,一會可迭代物件,一會可迭代物件用iter方法轉換成迭代器.......,有沒有感覺像是繞口令,那麼久讓我告訴你Python中什麼是迭代器和生成器吧.........

本文的組織如下:1,我們簡單地介紹了Python中的迭代器協議;2,將會詳細介紹生成器的概念和語法;3,將會給出一個有用的例子,說明使用生成器的好處;4,簡單的討論了使用生成器的注意事項; 5,生成器/迭代器/可迭代物件  之間的關係。 1. 迭代器協議 由於生成器自動實現了迭代器協議,而迭代器協議對很多人來說,也是一個較為抽象的概念。所以,為了更好的理解生成器,我們需要簡單的回顧一下迭代器協議的概念。 迭代器協議是指:物件需要提供next方法,它要麼返回迭代中的下一項,要麼就引起一個StopIteration異常,以終止迭代 可迭代物件就是:實現了迭代器協議的物件 協議是一種約定,可迭代物件實現迭代器協議,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__', '__new__', 'writelines', '...' 2. 生成器 Python使用生成器對延遲操作提供了支援。所謂延遲操作,是指在需要的時候才產生結果,而不是立即產生結果。這也是生成器的主要好處。 Python有兩種不同的方式提供生成器: 生成器函式:常規函式定義,但是,使用yield語句而不是return語句返回結果。yield語句一次返回一個結果,在每個結果中間,掛起函式的狀態,以便下次重它離開的地方繼續執行 生成器表示式:類似於列表推導,但是,生成器返回按需產生結果的一個物件,而不是一次構建一個結果列表 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) 1 >>> 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的生成器: 語法上和函式類似:生成器函式和常規函式幾乎是一樣的。它們都是使用def語句進行定義,差別在於,生成器使用yield語句返回一個值,而常規函式使用return語句返回一個值 自動實現迭代器協議:對於生成器,Python會自動實現迭代器協議,以便應用到迭代背景中(如for迴圈,sum函式)。由於生成器自動實現了迭代器協議,所以,我們可以呼叫它的next方法,並且,在沒有值可以返回的時候,生成器自動產生StopIteration異常 狀態掛起:生成器使用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 這裡,至少有兩個充分的理由說明 ,使用生成器比不使用生成器程式碼更加清晰: 使用生成器以後,程式碼行數更少。大家要記住,如果想把程式碼寫的Pythonic,在保證程式碼可讀性的前提下,程式碼行數越少越好 不使用生成器的時候,對於每次結果,我們首先看到的是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,生成器/迭代器/可迭代物件  之間的關係