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

奧辰發表於2019-08-02

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

 
 

相關文章