可迭代物件 vs 迭代器 vs 生成器

發表於2016-09-03

在使用Python的過程中,很容易混淆如下幾個關聯的概念:

  • 容器(container)
  • 可迭代物件(Iterable)
  • 迭代器(Iterator)
  • 生成器(generator)
  • 生成器表示式
  • {list, set, dict} 解析式

它們之間的關係如下表所示:

可迭代物件 vs 迭代器 vs 生成器

容器(container)

容器是用來儲存元素的一種資料結構,它支援隸屬測試,容器將所有資料儲存在記憶體中,在Python中典型的容器有:

  • list, deque, …
  • set,frozesets,…
  • dict, defaultdict, OrderedDict, Counter, …
  • tuple, namedtuple, …
  • str

容器相對來說很好理解,因為你可以把它當成生活中的箱子、房子、船等等。
一般的,通過判斷一個物件是否包含某個元素來確定它是否為一個容器。例如:

字典容器通過檢查是否包含鍵來進行判斷:

字串通過檢查是否包含某個子 串來判斷:

注意:並非所有的容器都是可迭代物件。

可迭代物件

正如前面所提到的,大部分容器都是可迭代的,但是還有其他一些物件也可以迭代,例如,檔案物件以及管道物件等等,容器一般來說儲存的元素是有限的,同樣的,可迭代物件也可以用來表示一個包含有限元素的資料結構。

可迭代物件可以為任意物件,不一定非得是基本資料結構,只要這個物件可以返回一個iterator。聽起來可能有點費解,但是可迭代物件與迭代器之間有一個顯著的區別。先看下面的例子:

在這裡,x是可迭代物件,而y和z都是迭代器,它們從可迭代物件x中獲取值。

注意:可迭代的類中,一般實現以下兩個方法,__iter__()以及__next()__方法,__iter__()方法返回self。

當我們執行以下程式碼的時候:

實際呼叫過程如下:

可迭代物件 vs 迭代器 vs 生成器

當我們反向編譯這段代Python碼的時候,可以發現它顯示呼叫了 GET_ITER,本質上跟呼叫iter(x)一樣,而FOR_ITER指令相等於呼叫next()方法來獲取每個元素。

迭代器(Iterators)

那麼什麼是迭代器呢?任何具有__next__()方法的物件都是迭代器,對迭代器呼叫next()方法可以獲取下一個值。而至於它使如何產生這個值的,跟它能否成為一個迭代器並沒有關係。

所以迭代器本質上是一個產生值的工廠,每次向迭代器請求下一個值,迭代器都會進行計算出相應的值並返回。

迭代器的例子很多,例如,所有itertools模組中的函式都會返回一個迭代器,有的還可以產生無窮的序列。

有的函式根據有限序列中生成無限序列:

有的函式根據無限序列中生成有限序列:

為了更好的理解迭代器的內部結構,我們先來定義一個生成斐波拉契數的迭代器:

注意這個類既是可迭代的 (因為具有__iter__()方法),也是它自身的迭代器(因為具有__next__()方法)。

迭代器內部狀態儲存在當前例項物件的prev以及cur屬性中,在下一次呼叫中將使用這兩個屬性。每次呼叫next()方法都會執行以下兩步操作:

  1. 修改狀態,以便下次呼叫next()方法
  2. 計算當前呼叫的結果

比喻:從外部來看,迭代器就像政府工作人員一樣,沒人找他辦事的時候(請求值),工作人員就閒著,當有人來找他的時候(請求值),工作人員就會忙一會,把請求的東西找出來交給請求的人。忙完之後,又沒事了,繼續閒著。

生成器

生成器其實就是一種特殊的迭代器。它使一種更為高階、更為優雅的迭代器。
使用生成器讓我們可以以一種更加簡潔的語法來定義迭代器。
讓我們先明確以下兩點:

  • 任意生成器都是迭代器(反過來不成立)
  • 任意生成器,都是一個可以延遲建立值的工廠

下面也是一個生成斐波那契序列的工廠函式,不過是以生成器的方式編寫的:

上面的程式碼是不是既優雅又簡潔?注意其中用到的魔法關鍵字

一起來剖析下上面的程式碼:首先,fib其實是一個很普通的函式,但是函式中沒有return語句,函式的返回值是一個生成器。

當呼叫f = fib()時,生成器被例項化並返回,這時並不會執行任何程式碼,生成器處於空閒狀態,注意這裡prev, curr = 0, 1並未執行。

然後這個生成器被包含在isslice()中,而這又是一個迭代器,所以還是沒有執行上面的程式碼。

然後這個迭代器又被包含在list()中,它會根據傳進來的引數生成一個列表。所以它首先對isslice()物件呼叫next()方法,isslice()物件又會對例項f呼叫next()
我們來看其中的一步操作,在第一次呼叫中,會執行prev, curr = 0, 1, 然後進入while迴圈,當遇到yield curr的時候,返回當前curr值,然後又進入空閒狀態。
生成的值傳遞給外層的isslice(),也相應生成一個值,然後傳遞給外層的list(),外層的list將這個值1新增到列表中。
然後繼續執行後面的九步操作,每步操作的流程都是一樣的。

然後執行到底11步的時候,isslice()物件就會丟擲StopIteration異常,意味著已經到達末尾了。注意生成器不會接收到第11次next()請求,後面會被垃圾回收掉。

生成器的型別

在Python中兩種型別的生成器:生成器函式以及生成器表示式。生成器函式就是包含yield引數的函式。生成器表示式與列表解析式類似。
假設使用如下語法建立一個列表:

使用set解析式也可以達到同樣的目的:

或者dict解析式:

還可以使用生成器表示式:

注意我們第一次呼叫next()之後,lazy_squares物件的狀態已經發生改變,所以後面後面地呼叫list()方法只會返回部分元素組成的列表。

總結

生成器是Python中一種非常強大的特性,它讓我們能夠編寫更加簡潔的程式碼,同時也更加節省記憶體,使用CPU也更加高效。
使用生成器的小提示:在你的程式碼中找到與下面程式碼類似的地方:

用以下程式碼進行替換:

相關文章