Python進階技巧 (6) — 迭代那件小事:深入了解 Iteration / Iterable / Iterator / __iter__ / __getitem__ / __next__ / yield
「徹底搞懂 Python 的 Iteration 機制!不再迷糊!」
難易度:★★★☆☆
實用度:★★★★☆
【本文章節】
- 壹、定義 Python 的 Iteration / Iterable / Iterator
- 貳、了解 __iter__ / __getitem__ / __next__
- 參、深入了解 yield
【導言】
大家一定都對 for loop 很熟悉,也很會使用 enumerate()
等 iteration function (迭代函數)不陌生,絕大多數程式語言都有自己的 Iteration 功能與機制,今天會將 Python 中 Iteration / Iterable / Iterator 三個常混用的名詞釐清,並且深入理解 Iterable / Iterator 的運作機制,以及如何寫出自己的 Iterable / Iterator 運用到自己的程式碼裡。最後的最後,會針對常出現在Iteration 中 yield
和 Generator
object 做補充。
【範例環境與建議先備知識】
OS Ubuntu 16.04
Python 3.7
Required Knowledge
- 了解 Python Class 以及 function/method 基礎概念
- 了解 Python magic function
【壹、定義 Python 的 Iteration / Iterable / Iterator】
每個程式語言(例如C++、Java等)都有自己一套運行 Iteration 的設計與機制,下方先定義 Python 的三個常用名詞 Iteration / Iterable / Iterator 以利同步討論以及書寫內容。
- Iteration:走訪/迭代/遍歷一個 object 裡面被要求的所有元素之「過程」或「機制」。是一個概念性的詞。
- Iterable:可執行 Iteration 的 objects 都稱為 Iterable(可當專有名詞)。參照 官方文件 提及,是指可以被 for loop 遍歷的 objects。以程式碼來說,只要具有
__iter__
或__getitem__
的 objects 就是 Iterable。 - Iterator:遵照 Python Iterator Protocol 的 objects。以 Python3 而言,參照 官方文件,只要具有
__iter__
和__next__
的 objects 皆為 Iterator。Iterator 是 Iterable 的 subset。
最容易被誤認的是 Python 常見的 list
, tuple
, range
和 str
都是 Iterable .但不是 Iterator ! 要檢驗一個 object 是不是 iterator 有以下方法測試:
- 使用
collections
中的Iterable
和Iterator
2. 根據定義,利用 dir()
或是 hasattr()
去檢查 attributes __iter__
, __getitem__
和 __next__
由上面程式碼,兩種方法都顯示,list
object 有 __iter__
以及 __getitem__
但沒有 __next__
,所以是 Iterable 但不是 Iterator。但是,透過 call iter(x)
(其實就是 call x.__iter__()
) ,會回傳一個同時帶有 __iter__
和 __next__
的新的 object x_iter
,此時 x_iter
就是一個 Iterator 了!
【貳、了解 __iter__ / __getitem__ / __next__】
要了解這三個 attributes 最直接的例子就是透過 for loop 的運作機制來解說。
製圖師(自己),來,請下圖!
當你輕鬆 call for loop 時,其實發生了上圖所繪的步驟。
值得注意的是,可以搭配 for loop 的 object,不需要一定是 Iterator,Iterable 就可以了。
接下來要示範如何在自己的 class 中實作 __iter__
, __next__
和 __getitem__
的幾種常見寫法。
- 利用
__iter__
和__next__
實作一個 Iterator:
由範例程式碼可以看到實作了 MyIterator
的 __iter__
和 __next__
,且 __iter__
僅僅是回傳 self
,原因是該 object 已經有 __next__
故本身就是一個 Iterator ,所以直接回傳即可。可以看到我透過 instance attribute self.index
來作為要 iterate 出去的值,而當觸發終止 iteration 條件時, raise StopIteration
即可終止。
但可以很輕易的知道,這樣的設計很怪,因為 self.index
是 instance scope variable ,產生的 my_iterator
只能用 for loop 一次。
為了解決這樣的問題,此時可以引進 yield
來實作,取代原本的 design pattern。(下一個例子中會示範)
2. 利用 __iter__
和Generator
(透過 yield
產生) 來實作一個 Iterable
上面的程式碼邏輯上和 python_iteration_2.py 程式碼相同。唯一比較奇怪的是,我拔掉了 __next__
且 __iter__
也沒有出現 return
等語法回傳任何東西的樣子,這樣不就違反前面對於 iterator 的定義了嗎?!其實,當一個 function (此處為 __iter__
)中帶有 yield
時,該 function 就會自動 return 一個 Gerenator
的 object,而這 generator 自帶 __next__
,是一個 Iterator。如此,這樣一切都又符合規範與定義了!注意,這裡是實作 Itareble 不是 Iterator,故自身不一定要具有 __next__
!
想要在 iteration 過程中保留某些值供計算輸出 (此處為 num
),就可以存於透過 yield
產生 generator 並存於該 scope。此外,因為每次 for loop 都會 call __iter__
並產生新的 generator,所以每次的 num
都可以刷新重新來過,解決的原先的問題了!
若想要更進一步了解 yield
的機制,可以參考本文中下一章節 【參、深入了解 yield】的介紹。
3. 利用 __getitem__
來實作一個 Iterable
這個方式直觀許多,首先要注意 __getitem__(self, key)
一定要有第二個positional argument key
,for loop 會自動生成從 0 開始到無窮的整數傳入作為 key
,故我們只需要根據 key
來輸出想要的值。當觸發欲終止條件時,raise IndexError
或是 StopIteration
皆可順利終止 iteration。
此外,補充一點 __getitem__
是常見的 magic function (即 Python 預設自訂帶有 double underscores 的 function),所以並不是只有在處理 iteration 時會使用到,如 index 和 slice 等功能都會需要使用到此 function,若在創建較為複雜的 object 時,要記得根據 __getitem__
的 arguments 兼容處理各種況狀。
最後,可能有人會問說為何 Iteration 要設計 __next__
和__getitem__
這兩種 pattern 呢?實作面來說,用 __next__
這種方式寫會更適合在處理、生成前後值相依或是與 streaming 概念相似的資料,例如「生成 Fibonacci Sequence」;而 __getitem__
一般使用情境是已經將所有資料儲存在某處、某變數中,然後透過 key 來取值。
【參、深入了解 yield】
在本文中的【貳、了解 __iter__ / __getitem__ / __next__】已提及 yield
在撰寫 Iteration 時的使用方式,這裏就一併補上關於 yield
完整介紹囉!
先觀察下面兩段範例程式碼:
搭配上面的程式碼,介紹一下 yield
與 Generator
object:
yield
只能出現在 function 裡,而 call 帶有yield
的 function 會回傳一個Generator
object。Generator
object 是一個 Iterator,帶有__iter__
和__next__
attributes。- 第一次 call
next(generator)
執行內容等價於將原 function 執行到第一次出現yield
之處,「暫停」執行,並返回yield
後的值。 - 第二次之後每次 call
next(generator)
都會不斷從上次「暫停」處繼續執行,直到 function 全部執行完畢並 raiseStopIteration
。因為yield
沒有將原 function 從 call stack 中移除,故能暫停並重新回到上次暫停處繼續執行。這邏輯也是yield
和return
最核心不同之處,return
會直接將原 function 從 call stack 中移除,終止 function,不論return
後面是是否還有其他程式碼。 yeild
除了可傳出值外,也可以接受由外部輸入的值,利用generator.send()
即可同時傳入值也傳出值。此設計,讓Generator
object 可和外部進行雙向溝通,可以傳出也可以傳入值。- 關於
Generator
object 的創建有兩種語法:一是在 function 內加入yield
」,二是形如x = (i for i in y)
的方式。其實大家常用的產生 list 的其中一種寫法x = [i for i in range(10)]
就是創建一個Generator
object 的變形。
概念性總結一下,原先和 return
搭配的 function,就是吃進 input 然後輸出 output,life cycle 就會消失,yield
的寫法可以視為擴充 function 的特性,使其具有「記憶性」、「延續性」的特質,可以不斷傳入 input 和輸出 output,且不殺死 function。未來要撰寫具有該特質的 function 時就可以考慮使用 yield
來取代「在外部存一堆 buffer 變數」的做法。
【結語】
了解 Iteration 各種名詞確切的定義並不是玩玩文字遊戲,而是在開發時能較快速正確解讀官方文件內容並更有效的撰寫程式碼,然後慢慢體會不同寫法在不同情境需求下的優缺點。
此外 Iteration 的概念也可以在許多 Python 套件或是框架下適用,例如 Pytorch 的 Dataset
就有提供 Dataset
(適用 __getitem__
)和 IterableDataset
(適用 __iter__
)兩種框架,此時釐清兩者差別後便可以根據需求挑選合適的框架。
【參考資料】
如果你也喜歡我們的文章,幫我們動動手部肌肉,按下掌聲Clap,讓我們有動力繼續煮下一頓料理!