只要你學了Python語言,就不會不知道for循環,也確定用for循環來遍歷一個列表(list),那爲何for循環能夠遍歷list,而不能遍歷int類型對象呢?怎麼讓一個自定義的對象可遍歷?shell
這篇博客中,咱們來一塊兒探索一下這個問題,在這個過程當中,咱們會介紹到迭代器、可迭代對象、生成器,更進一步的,咱們會詳細介紹他們的原理、異同。數據庫
在開始下面內容以前,咱們先說說標題中的「迭代」一詞。什麼是迭代?我認爲,迭代一個完整過程當中的一個重複,或者說每一次對過程的重複稱爲一次「迭代」,而每一次迭代獲得的結果會做爲下一次迭代的初始值,舉一個類比來講:一我的類家族的發展是一個完整過程,須要通過數代人的努力,每一代都會以接着上一代的成果繼續發展,因此每一代都是迭代。網絡
(1)怎麼判斷是否可迭代app
做爲一門設計語言,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
因此,整型、布爾不可迭代,字符串、列表、字典、元組可迭代。spa
怎麼讓一個對象可迭代呢?畢竟,不少時候,咱們須要用到的對象不止Python內置的這些數據類型,還有自定義的數據類型。答案就是實現__iter__()方法,只要一個對象定義了__iter__()方法,那麼它就是可迭代對象。設計
from collections.abc import Iterable class A(): def __iter__(self): pass print('A()是可迭代對象嗎:',isinstance(A(),Iterable))
結果輸出爲:code
A()是可迭代對象嗎: True對象
瞧,咱們在__iter__()方法裏面甚至沒寫任何東西,反正咱們在類A中定義則__iter__()方法,那麼,它就是一個可迭代對象。
重要的事情說3遍:
只要一個對象定義了__iter__()方法,那麼它就是可迭代對象。
只要一個對象定義了__iter__()方法,那麼它就是可迭代對象。
只要一個對象定義了__iter__()方法,那麼它就是可迭代對象。
迭代器是對可迭代對象的改造升級,上面說過,一個對象定義了__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循環的本質來解答這些問題。
說到__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循環的本質。
若是一個函數體內部使用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()方法纔會產生下一個元素。
使用過列表解析式嗎?語法格式爲:[返回值 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
能夠看到,生成器解析式返回的就是一個生成器對象,換句話說生成器解析式是生成器的一種定義方式,這種方式簡單快捷,固然實現的功能不能太複雜。
本文全面總結了Python中可迭代對象、迭代器、生成器知識,我相信,只要你認真消化我這篇博文,就能深入領悟迭代器生成器。