1 引言
只要你學了Python語言,就不會不知道for迴圈,也肯定用for迴圈來遍歷一個列表(list),那為什麼for迴圈可以遍歷list,而不能遍歷int型別物件呢?怎麼讓一個自定義的物件可遍歷?
這篇部落格中,我們來一起探索一下這個問題,在這個過程中,我們會介紹到迭代器、可迭代物件、生成器,更進一步的,我們會詳細介紹他們的原理、異同。
2 迭代器與可迭代物件
在開始下面內容之前,我們先說說標題中的“迭代”一詞。什麼是迭代?我認為,迭代一個完整過程中的一個重複,或者說每一次對過程的重複稱為一次“迭代”,而每一次迭代得到的結果會作為下一次迭代的初始值,舉一個類比來說:一個人類家族的發展是一個完整過程,需要經過數代人的努力,每一代都會以接著上一代的成果繼續發展,所以每一代都是迭代。
2.1 迭代器
(1)怎麼判斷是否可迭代
作為一門設計語言,Python提供了許多必要的資料型別,例如基本資料型別int、bool、str,還有容器型別list、tuple、dict、set。這些型別當中,有些是可迭代的,有些不可迭代,怎麼判斷呢?
在Python中,我們把所有可以迭代的物件統稱為可迭代物件,有一個類專門與之對應:Iterable。所以,要判斷一個類是否可迭代,只要判斷是否是Iterable類的例項即可。
>>> from collections import Iterable >>> isinstance(123, Iterable) False >>> isinstance(True, Iterable) False >>> isinstance('abc', Iterable) True >>> isinstance([], Iterable) True >>> isinstance({}, Iterable) True >>> isinstance((), Iterable) True
所以,整型、布林不可迭代,字串、列表、字典、元組可迭代。
怎麼讓一個物件可迭代呢?畢竟,很多時候,我們需要用到的物件不止Python內建的這些資料型別,還有自定義的資料型別。答案就是實現__iter__()方法,只要一個物件定義了__iter__()方法,那麼它就是可迭代物件。
from collections.abc import Iterable class A(): def __iter__(self): pass print('A()是可迭代物件嗎:',isinstance(A(),Iterable))
結果輸出為:
A()是可迭代物件嗎: True
瞧,我們在__iter__()方法裡面甚至沒寫任何東西,反正我們在類A中定義則__iter__()方法,那麼,它就是一個可迭代物件。
重要的事情說3遍:
只要一個物件定義了__iter__()方法,那麼它就是可迭代物件。
只要一個物件定義了__iter__()方法,那麼它就是可迭代物件。
只要一個物件定義了__iter__()方法,那麼它就是可迭代物件。
2.2 迭代器
迭代器是對可迭代物件的改造升級,上面說過,一個物件定義了__iter__()方法,那麼它就是可迭代物件,進一步地,如果一個物件同時實現了__iter__()和__next()__()方法,那麼它就是迭代器。
來,跟我讀三遍:
如果一個物件同時實現了__iter__()和__next()__()方法,那麼它就是迭代器。
如果一個物件同時實現了__iter__()和__next()__()方法,那麼它就是迭代器。
如果一個物件同時實現了__iter__()和__next()__()方法,那麼它就是迭代器。
在Python中,也有一個類與迭代器對應:Iterator。所以,要判斷一個類是否是迭代器,只要判斷是否是Iterator類的例項即可。
from collections.abc import Iterable from collections.abc import Iterator class B(): def __iter__(self): pass def __next__(self): pass print('B()是可迭代物件嗎:',isinstance(B(), Iterable)) print('B()是迭代器嗎:',isinstance(B(), Iterator))
結果輸出如下:
B()是可迭代物件嗎: True
B()是迭代器嗎: True
可見,迭代器一定是可迭代物件,但可迭代物件不一定是迭代器。
所以整型、布林一定不是迭代器,因為他們連可迭代物件都算不上。那麼,字串、列表、字典、元組是迭代器嗎?猜猜!
>>> from collections.abc import Iterator >>> isinstance('abc', Iterator) False >>> isinstance([], Iterator) False >>> isinstance({}, Iterator) False >>> isinstance((), Iterator) False
驚不驚喜,意不意外,字串、列表、字典、元組都不是迭代器。那為什麼它們可以在for迴圈中遍歷呢?而且,我想,看到這裡,就算你已經可以在形式上區分可迭代物件和迭代器,但是你可能會問,這有什麼卵用嗎?確實,沒多少卵用,因為我們還不知道__iter__()、__next__()到底是個什麼鬼東西。
接下來,我們通過繼續探究for迴圈的本質來解答這些問題。
2.3 for迴圈的本質
說到__iter__()和__next__()方法,就很有必要介紹一下iter()和next()方法了。
(1)iter()與__iter__()
__iter__()的作用是返回一個迭代器,雖然上面說過,只要實現了__iter__()方法就是可迭代物件,但是,沒有實現功能(返回迭代器)總歸是有問題的,就像一個村長,當選之後,那就是村長了,但是如果尸位素餐不做事,那總是有問題的。
__iter__()方法畢竟是一個特殊方法,不適合直接呼叫,所以Python提供了iter()方法。iter()是Python提供的一個內建方法,可以不用匯入,直接呼叫即可。
from collections.abc import Iterator class A(): def __iter__(self): print('A類的__iter__()方法被呼叫') return B() class B(): def __iter__(self): print('B類的__iter__()方法被呼叫') return self def __next__(self): pass a = A() print('對A類物件呼叫iter()方法前,a是迭代器嗎:', isinstance(a, Iterator)) a1 = iter(a) print('對A類物件呼叫iter()方法後,a1是迭代器嗎:', isinstance(a1, Iterator)) b = B() print('對B類物件呼叫iter()方法前,b是迭代器嗎:', isinstance(b, Iterator)) b1 = iter(b) print('對B類物件呼叫iter()方法後,b1是迭代器嗎:', isinstance(b1, Iterator))
執行結果如下:
對A類物件呼叫iter()方法前,a是迭代器嗎: False
A類的__iter__()方法被呼叫
對A類物件呼叫iter()方法後,a1是迭代器嗎: True
對B類物件呼叫iter()方法前,b是迭代器嗎: True
B類的__iter__()方法被呼叫
對B類物件呼叫iter()方法後,b1是迭代器嗎: True
對於B類,因為B類本身就是迭代器,所以可以直接返回B類的例項,也就是說self,當然,你要是返回其他迭代器也沒毛病。對於類A,它只是一個可迭代物件,__iter__()方法需要返回一個迭代器,所以返回了B類的例項,如果返回的不是一個迭代器,呼叫iter()方法時就會報以下錯誤:
TypeError: iter() returned non-iterator of type 'A'
(2)next()與__next__()
__next__()的作用是返回遍歷過程中的下一個元素,如果沒有下一個元素則主動丟擲StopIteration異常。而next()就是Python提供的一個用於呼叫__next__()方法的內建方法。
下面,我們通過next()方法來遍歷一個list:
>>> list_1 = [1, 2, 3] >>> next(list_1) Traceback (most recent call last): File "<pyshell#19>", line 1, in <module> next(list_1) TypeError: 'list' object is not an iterator >>> list_2 = iter(list_1) >>> next(list_2) 1 >>> next(list_2) 2 >>> next(list_2) 3 >>> next(list_2) Traceback (most recent call last): File "<pyshell#24>", line 1, in <module> next(list_2) StopIteration
因為列表只是可迭代物件,不是迭代器,所以對list_1直接呼叫next()方法會產生異常。對list_1呼叫iter()後就可以獲得是迭代器的list_2,對list_2每一次呼叫next()方法都會取出一個元素,當沒有下一個元素時繼續呼叫next()就丟擲了StopIteration異常。
>>> class A(): def __init__(self, lst): self.lst = lst def __iter__(self): print('A.__iter__()方法被呼叫') return B(self.lst) >>> class B(): def __init__(self, lst): self.lst = lst self.index = 0 def __iter__(self): print('B.__iter__()方法被呼叫') return self def __next__(self): try: print('B.__next__()方法被呼叫') value = self.lst[self.index] self.index += 1 return value except IndexError: raise StopIteration() >>> a = A([1, 2, 3]) >>> a1 = iter(a) A.__iter__()方法被呼叫 >>> next(a1) B.__next__()方法被呼叫 1 >>> next(a1) B.__next__()方法被呼叫 2 >>> next(a1) B.__next__()方法被呼叫 3 >>> next(a1) B.__next__()方法被呼叫 Traceback (most recent call last): File "<pyshell#78>", line 11, in __next__ value = self.lst[self.index] IndexError: list index out of range During handling of the above exception, another exception occurred: Traceback (most recent call last): File "<pyshell#84>", line 1, in <module> next(a1) File "<pyshell#78>", line 15, in __next__ raise StopIteration() StopIteration
A類例項化出來的例項a只是可迭代物件,不是迭代器,呼叫iter()方法後,返回了一個B類的例項a1,每次對a1呼叫next()方法,都用呼叫B類的__next__()方法。
接下來,我們用for迴圈遍歷一下A類例項:
>>> for i in A([1, 2, 3]): print('for迴圈中取出值:',i) A.__iter__()方法被呼叫 B.__next__()方法被呼叫 for迴圈中取出值: 1 B.__next__()方法被呼叫 for迴圈中取出值: 2 B.__next__()方法被呼叫 for迴圈中取出值: 3 B.__next__()方法被呼叫
通過for迴圈對一個可迭代物件進行迭代時,for迴圈內部機制會自動通過呼叫iter()方法執行可迭代物件內部定義的__iter__()方法來獲取一個迭代器,然後一次又一次得迭代過程中通過呼叫next()方法執行迭代器內部定義的__next__()方法獲取下一個元素,當沒有下一個元素時,for迴圈自動捕獲並處理StopIteration異常。如果你還沒明白,請看下面用while迴圈實現for迴圈功能,整個過程、原理都是一樣的:
>>> a = A([1, 2, 3]) >>> a1 = iter(a) A.__iter__()方法被呼叫 >>> while True: try: i = next(a1) print('for迴圈中取出值:', i) except StopIteration: break B.__next__()方法被呼叫 for迴圈中取出值: 1 B.__next__()方法被呼叫 for迴圈中取出值: 2 B.__next__()方法被呼叫 for迴圈中取出值: 3 B.__next__()方法被呼叫 作為一個迭代器,B類物件也可以通過for迴圈來迭代: >>> for i in B([1, 2, 3]): print('for迴圈中取出值:',i) B.__iter__()方法被呼叫 B.__next__()方法被呼叫 for迴圈中取出值: 1 B.__next__()方法被呼叫 for迴圈中取出值: 2 B.__next__()方法被呼叫 for迴圈中取出值: 3 B.__next__()方法被呼叫 看出來了嗎?這就是for迴圈的本質。
3 生成器
3.1 迭代器與生成器
如果一個函式體內部使用yield關鍵字,這個函式就稱為生成器函式,生成器函式呼叫時產生的物件就是生成器。生成器是一個特殊的迭代器,在呼叫該生成器函式時,Python會自動在其內部新增__iter__()方法和__next__()方法。把生成器傳給 next() 函式時, 生成器函式會向前繼續執行, 執行到函式定義體中的下一個 yield 語句時, 返回產出的值, 並在函式定義體的當前位置暫停, 下一次通過next()方法執行生成器時,又從上一次暫停位置繼續向下……,最終, 函式內的所有yield都執行完,如果繼續通過yield呼叫生成器, 則會丟擲StopIteration 異常——這一點與迭代器協議一致。
>>> from collections.abc import Iterable >>> from collections.abc import Iterator >>> def gen(): print('第1次執行') yield 1 print('第2次執行') yield 2 print('第3次執行') yield 3 >>> g = gen() >>> isinstance(g, Iterable) True >>> isinstance(g, Iterator) True >>> g <generator object gen at 0x0000021CE9A39A98> >>> next(g) 第1次執行 1 >>> next(g) 第2次執行 2 >>> next(g) 第3次執行 3 >>> next(g) Traceback (most recent call last): File "<pyshell#120>", line 1, in <module> next(g) StopIteration
可以看到,生成器的執行機制與迭代器是極其相似的,生成器本就是迭代器,只不過,有些特殊。那麼,生成器特殊在哪呢?或者說,有了迭代器,為什麼還要用生成器?
從上面的介紹和程式碼中可以看出,生成器採用的是一種惰性計算機制,一次呼叫也只會產生一個值,它不會將所有的值一次性返回給你,你需要一個那就呼叫一次next()方法取一個值,這樣做的好處是如果元素有很多(數以億計甚至更多),如果用列表一次性返回所有元素,那麼會消耗很大記憶體,如果我們只是想要對所有元素依次一個一個取出來處理,那麼,使用生成器就正好,一次返回一個,並不會佔用太大記憶體。
舉個例子,假設我們現在要取1億以內的所有偶數,如果用列表來實現,程式碼如下:
def fun_list(): index = 1 temp_list = [] while index < 100000000: if index % 2 == 0: temp_list.append(index) print(index) index += 1 return temp_list
上面程式會先獲取所有符合要求的偶數,然後一次性返回。如果你執行了程式碼,你就會發現兩個問題——執行時間很長、消耗很多記憶體。
有時候,我們並不一定需要一次性獲得所有的物件,需要一個使用一個就可以,這樣的話,可以用生成器來實現:
>>> def fun_gen(): index = 1 while index < 100000000: if index % 2 == 0: yield index index += 1 >>> fun_gen() <generator object fun_gen at 0x00000222DC2F4360> >>> g = fun_gen() >>> next(g) 2 >>> next(g) 4 >>> next(g) 6
看到了嗎?對生成器沒執行一次next()方法,就會返回一個元素,這樣的話無論在速度上還是機器效能消耗上都會好很多。如果你還沒感受到生成器的優勢,我再說一個應用場景,假如需要取出遠端資料庫中的100萬條記錄進行處理,如果一次性獲取所有記錄,網路頻寬、記憶體都會有很大消耗,但是如果使用生成器,就可以取一條,就在本地處理一條。
不過,生成器也有不足,正因為採用了惰性計算,你不會知道下一個元素是什麼,更不會知道後面還有多少元素,所以,對於列表、元組等結構,我們能呼叫len()方法獲知長度,但是對於生成器卻不能。
總結一下迭代器與生成器的異同:
(1)生成器是一種特殊的迭代器,擁有迭代器的所有特性;
(2)迭代器使用return返回值而生成器使用yield返回值每一次對生成器執行next()都會在yield處暫停;
(3)迭代器和生成器雖然都執行next()方法時返回下一個元素,迭代器在例項化前就已知所有元素,但是採用惰性計算機制,共有多少元素,下一個元素是什麼都是未知的,每一次對生成器物件執行next()方法才會產生下一個元素。
3.2 生成器解析式
使用過列表解析式嗎?語法格式為:[返回值 for 元素 in 可迭代物件 if 條件]
看下面程式碼:
>>> li = [] >>> for i in range(5): if i%2==0: li.append(i**2) >>> li [0, 4, 16]
我們可以用列表解析式實現同樣功能:
>>> li = [i**2 for i in range(5) if i%2==0] >>> li [0, 4, 16] >>> type(li) <class 'list'>
很簡單對不對?簡潔了很多,返回的li就是一個列表。咳咳……偏題了,我們要說的是生成器解析式,而且我相信開啟我這篇博文的同學大多都熟悉列表解析式,迴歸正題。
生成器解析式語法格式為:(返回值 for 元素 in 可迭代物件 if 條件)
你沒看錯,跟列表解析式相比,生成器解析式只是把方括號換成了原括號。來感受一下:
>>> g = (i**2 for i in range(5) if i%2==0) >>> g <generator object <genexpr> at 0x00000222DC2F4468> >>> next(g) 0 >>> next(g) 4 >>> next(g) 16 >>> next(g) Traceback (most recent call last): File "<pyshell#38>", line 1, in <module> next(g) StopIteration
可以看到,生成器解析式返回的就是一個生成器物件,換句話說生成器解析式是生成器的一種定義方式,這種方式簡單快捷,當然實現的功能不能太複雜。
4 總結
本文全面總結了Python中可迭代物件、迭代器、生成器知識,我相信,只要你認真消化我這篇博文,就能深刻領悟迭代器生成器。