Python 的迭代器和生成器
事實上,當你在使用Python的第一天,你很有可能就已經和迭代器打交道了。這篇文章從淺入深的聊聊Python的迭代器和生成器。
1. 從for語句開始
有一定C語言基礎的朋友在剛剛學習Python的時候,常常會詢問Python有沒有類似這樣的寫法:
for i = 0; i < 10 ; i += 1:
print i
更有意思的是,當被告知『沒有』的時候,一些人就打起了歪主意,寫出了合法的代碼:
i = 0
while 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 = 'abc'
for i,_ in enumerate(my_str):
print my_str[i]
同樣的,以下是通過下標對可迭代對象的元素進行改寫:
my_list = [1,2,3]
for i,_ in enumerate(my_list):
my_list[i] = my_list[i] + 1
print my_list
我們知道,在以上兩個例子中,讀取和寫入元素,是通過可迭代對象的操作符[]實現的。而下標只是作為參數出現。我們不妨把這種模式,稱作『可迭代對象是一等公民』。
與之相對的,有沒有可能將下標作為一等公民?換句話說,元素的提取只和下標打交道,而和可迭代對象無關。答案是有的。這樣的一種 設計模式 ,就是 迭代器模式 。那個升級版的下標,稱之為 迭代器 。
下面的代碼,說明了如何顯式的使用迭代器。利用Python內建函數iter,可以得到一個可迭代對象的 迭代器 。
my_list = [1,2,3]
i = iter(my_list)
while True:
try:
print next(i)
except StopIteration:
break
一旦迭代器建立起來,提取元素的過程就不再依賴于原始的可迭代對象,而是僅僅依賴于迭代器本身。Python內建的next函數作用于迭代器上,會執行三個操作:
- 返回當前『位置』的元素,第一次next調用,當前位置是可迭代對象的起始位置
- 將『位置』向后遞增
- 如果到達可迭代對象的末尾,即沒有元素可以提取,則拋出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.X
def next(obj):
return obj.next()
#Python 3.X
def next(obj):
return obj.__next__()
所以利用這一點,我們很容易寫一個自己的可迭代對象和迭代器。下面就是一個例子,這個迭代器有隨機的長度。測試它的方法就用for...in...
import random
class 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 生成器
生成器是創建迭代器的一種簡便的方法。生成器是一個特殊的函數。我們可以從靜態和動態兩個角度理解生成器函數。
首先,從靜態的角度,生成器函數在代碼中表現為:
- 含有yield語句(無論yield是否可能會被執行)
- 無return或者僅有無值return(一旦函數里存在yield語句,有值return會視為語法錯誤)
其次,從動態的角度,生成器函數在運行過程中:
- 當生成器函數被調用的時候,生成器函數不執行內部的任何代碼,直接立即返回一個迭代器。
- 當所返回的迭代器第一次調用next的時候,生成器函數從頭開始執行,如果遇到了執行yield x,next立即返回yield值x。
- 當所返回的迭代器繼續調用next的時候,生成器函數從上次yield語句的下一句開始執行,直到遇到下一次執行yield
- 任何時候遇到函數結尾,或者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 v
v = next(it);print v
v = next(it);print v
v = 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
來自:https://zhuanlan.zhihu.com/p/24499698