為什麼for循環可以遍歷list:Python中迭代器與生成器

  • 2019 年 10 月 3 日
  • 筆記

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中可迭代對象、迭代器、生成器知識,我相信,只要你認真消化我這篇博文,就能深刻領悟迭代器生成器。