爲何for循環能夠遍歷list:Python中迭代器與生成器

1 引言

只要你學了Python語言,就不會不知道for循環,也確定用for循環來遍歷一個列表(list),那爲何for循環能夠遍歷list,而不能遍歷int類型對象呢?怎麼讓一個自定義的對象可遍歷?shell

這篇博客中,咱們來一塊兒探索一下這個問題,在這個過程當中,咱們會介紹到迭代器、可迭代對象、生成器,更進一步的,咱們會詳細介紹他們的原理、異同。數據庫

2 迭代器與可迭代對象

在開始下面內容以前,咱們先說說標題中的「迭代」一詞。什麼是迭代?我認爲,迭代一個完整過程當中的一個重複,或者說每一次對過程的重複稱爲一次「迭代」,而每一次迭代獲得的結果會做爲下一次迭代的初始值,舉一個類比來講:一我的類家族的發展是一個完整過程,須要通過數代人的努力,每一代都會以接着上一代的成果繼續發展,因此每一代都是迭代。網絡

2.1 迭代器

(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__()方法,那麼它就是可迭代對象。

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

相關文章
相關標籤/搜索