標籤:

Python 的迭代器和生成器

事實上,當你在使用Python的第一天,你很有可能就已經和迭代器打交道了。這篇文章從淺入深的聊聊Python的迭代器和生成器。

=========================================================================================================================================================================================================

1. 從for語句開始

有一定C語言基礎的朋友在剛剛學習Python的時候,常常會詢問Python有沒有類似這樣的寫法:

for i = 0; i < 10 ; i += 1: print i

更有意思的是,當被告知『沒有』的時候,一些人就打起了歪主意,寫出了合法的代碼:

i = 0while i < 10: print i i += 1

這是用Python寫C程序,非常的不推薦。實際上,for總是和in關鍵字一起使用

for i in range(10): print i

range不是Python的關鍵字,而是一個內建的函數(Python 2.X),它實際上返回了一個列表

>>> print range(10)[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

可是一些老司機還推薦這樣寫(僅限於Python 2.X)

for i in xrange(10): print i

想了解其中的奧義,繼續閱讀本文吧!

2.迭代器

有一些Python對象,我們可以從中按一定次序提取出其中的元素。這些對象稱之為可迭代對象。比如,字元串、列表、元組都是可迭代對象。

我們回憶一下從可迭代對象中提取元素的過程。這次,我們顯式的使用列表的下標

my_str = abcfor i,_ in enumerate(my_str): print my_str[i]

同樣的,以下是通過下標對list的元素進行改寫:

my_list = [1,2,3]for i,_ in enumerate(my_list): my_list[i] = my_list[i] + 1print my_list

我們知道,在以上兩個例子中,讀取和寫入元素,是通過list的操作符[]實現的。而下標只是作為參數出現。我們不妨把這種模式,稱作『可迭代對象是一等公民』。

與之相對的,有沒有可能將下標作為一等公民?換句話說,元素的提取只和下標打交道,而和可迭代對象無關。答案是有的。這樣的一種設計模式,就是迭代器模式。那個升級版的下標,稱之為迭代器

迭代器模式特別適合於以下情形:

  1. 不關心元素的隨機訪問
  2. 元素的個數不可提前預測

下面的代碼,說明了如何顯式的使用迭代器。利用Python內建函數iter,可以得到一個可迭代對象的迭代器

my_list = [1,2,3]i = iter(my_list)while True: try: print next(i) except StopIteration: break

一旦迭代器建立起來,提取元素的過程就不再依賴於原始的可迭代對象,而是僅僅依賴於迭代器本身。Python內建的next函數作用於迭代器上,會執行三個操作:

  1. 返回當前『位置』的元素,第一次next調用,當前位置是可迭代對象的起始位置
  2. 將『位置』向後遞增
  3. 如果到達可迭代對象的末尾,即沒有元素可以提取,則拋出StopIteration異常

實際上,你並不需要這麼麻煩的方法來使用迭代器,Python中的循環語句會自動進行迭代器的建立、next的調用和StopIteration的處理。換句話說,遍歷一個可迭代對象的元素,這樣寫就對了:

my_list = [1,2,3]for v in my_list: print v

換句話說,Python的for...in語句,隱藏了大量的細節。

所以,Python 2.X 的 range和xrange有何區別?答案是,range的返回值就是一個list,在你調用range的時候,Python會產生所有的元素。而xrange是一個特別設計的可迭代對象,它在建立的時候僅僅保存終止值。你可比較以下兩種寫法的實際運行結果:

for v in range(1000000000000): #possible Memory Error if v == 2: break for v in xrange(1000000000000): #fine if v == 2: break

在Python 3.X 中,不再有內建的xrange,其range等效於Python 2.X 的xrange

3. 自定義迭代器

那麼Python的iter函數和next函數都做了什麼呢?答案是,基本上什麼都沒做!它們的內部實現是(邏輯上)這樣的:

def iter(obj): return obj.__iter__()#Python 2.Xdef next(obj): return obj.next()#Python 3.Xdef next(obj): return obj.__next__()

所以利用這一點,我們很容易寫一個自己的可迭代對象和迭代器。下面就是一個例子,這個迭代器有隨機的長度。測試它的方法就用for...in...

import randomclass demo_iterator(object): def __next__(self): return self.next() def next(self): v = random.randint(0,10) if v < 5: raise StopIteration() else: return v class demo_iterable(object): def __iter__(self): return demo_iterator() for v in demo_iterable(): print(v)

為了使得它在Python 2和Python 3都可用,我們同時實現了next和__next__。

這個故事告訴我們,若想讓你自己的對象支持for...in...,你可以實現它的迭代器介面。

使用迭代器有何好處?用默認的列表不也很好嗎?實際上,使用迭代器最大的優點是,能夠及時處理『未知』的事件(例如,用戶的輸入,網路上的信號),並在迭代推進的過程中隨時可以終止迭代。就拿range為例,我們如果想儘可能多的利用系統內存產生儘可能多的數據。那麼使用迭代器的方法,可以在每一次迭代的時候都檢查一下剩餘可用的內存,從而決定要不要進行下一次迭代。而使用list的方法,過程中使用了多少內存,是很難預見的。

4 生成器

生成器是創建迭代器的一種簡便的方法。生成器是一個特殊的函數。我們可以從靜態和動態兩個角度理解生成器函數。

首先,從靜態的角度,生成器函數在代碼中表現為:

  1. 含有yield語句(無論yield是否可能會被執行)
  2. 無return或者僅有無值return(一旦函數里存在yield語句,有值return會視為語法錯誤)

這裡注意,Python的yield關鍵字的唯一作用,就是把一個普通的函數變成生成器。當一個函數內出現yield關鍵字後,就會變異為生成器,其行為與普通函數不同。

其次,從動態的角度,生成器在運行過程中:

  1. 當生成器函數被調用的時候,生成器函數不執行內部的任何代碼,直接立即返回一個迭代器。
  2. 當所返回的迭代器第一次調用next的時候,生成器函數從頭開始執行,如果遇到了執行yield x,next立即返回yield值x。
  3. 當所返回的迭代器繼續調用next的時候,生成器函數從上次yield語句的下一句開始執行,直到遇到下一次執行yield
  4. 任何時候遇到函數結尾,或者return語句,拋出StopIteration異常

特別的,生成器返回的迭代器,其__iter__返回其自身。

舉一個最簡單的例子:

def g(): yield 1 yield 2 yield 3 for v in g(): print v

如何理解這個程序呢?

把for循環展開,並在生成器函數中插入一些print語句。

def g(): print L1 yield 1 print L2 yield 2 print L3 yield 3 print L4 it = iter(g())v = next(it);print vv = next(it);print vv = next(it);print vv = next(it);print v

g()已經返回了一個迭代器,iter這個迭代器將得到迭代器自身。所以it依然是生成器函數g返回的迭代器。

it = g()print id(it)print id(iter(it)) #same value as previous line

在執行g()的時候,並沒有輸出L1。而是第一次調用next的時候,L1輸出,next返回1。以此類推,之後L4被列印出來,最後一個next拋出SropIteration異常。

利用生成器,我們可以重寫之前的隨機序列,看起來簡單多了。

import random def demo_generator(n): while True: v = random.randint(0,n) if v > 5: yield v else: break for i in demo_generator(10): print i

5 應用、itertools、以及其他

迭代器由於其不定長的特性,特別適合表達數學中的『無窮序列』

比如說,我們要尋找前10組直角三角形的邊長。演算法是暴利枚舉每一邊的長度。然而,我們並不知道邊長的搜索邊界,用列表做循環顯然不合適。

所以,我們首先產生一個正整數無窮序列:

def pint(): i = 1 while True: yield i i += 1

然後進行窮舉。因為直角三角形的直角邊總小於斜邊,所以直角邊的範圍不用取無窮序列。另外,為了避免對稱重複,最內層循環的直角邊只取比另一個直角邊小的值。

def tri(): for h in pint(): #hypotenuse for c1 in xrange(1,h): #cathetus1 for c2 in xrange(1,c1): #cathetus2 if c1 * c1 + c2 * c2 == h * h: yield (c1,c2,h)

tri也是個生成器,得到的迭代器也是個無窮序列。取前10個,用for循環就好:

for i,v in enumerate(tri()): if i == 10: break print v

實際上想取多少就取多少。這種無窮序列在解決數學問題的時候特別方便。

Python內建庫itertools有很多很方便的函數,大部分函數都支持無窮序列的運算。若自己寫一些迭代器(無論用類的方法,還是用生成器),就可以很方便的利用這些itertools函數了。具體的計算功能可見相關文檔。

Python中除了可迭代對象,還有『容器』對象的概念。儘管很多內建對象即是容器又是可迭代對象,但這兩個概念是相互獨立的。容器對象無非是實現了__contains__成員,使得能夠接受in操作符的運算。一個對象是不是容器,和它是不是可迭代,沒有任何關係。

class cont(object): def __contains__(self,x): return True if 2 in cont(): print Here for x in cont(): #TypeError: cont object is not iterable print x

推薦閱讀:

Python3《機器學習實戰》學習筆記(九):支持向量機實戰篇之再撕非線性SVM
(Python進階必看)賴明星的Python精品圖書重磅發布!!
Python黑帽編程2.5 函數
《機器學習實戰》學習總結(四)——貝葉斯分類
怎樣用五十行Python代碼自造比特幣?

TAG:Python |