Python進階技巧 (6) — 迭代那件小事:深入了解 Iteration / Iterable / Iterator / __iter__ / __getitem__ / __next__ / yield

Jack Cheng
整個程式都是我的咖啡館
10 min readOct 20, 2019

「徹底搞懂 Python 的 Iteration 機制!不再迷糊!」

難易度:★★★☆☆

實用度:★★★★☆

by pixabay.com

【本文章節】

  • 壹、定義 Python 的 Iteration / Iterable / Iterator
  • 貳、了解 __iter__ / __getitem__ / __next__
  • 參、深入了解 yield

【導言】

大家一定都對 for loop 很熟悉,也很會使用 enumerate() 等 iteration function (迭代函數)不陌生,絕大多數程式語言都有自己的 Iteration 功能與機制,今天會將 Python 中 Iteration / Iterable / Iterator 三個常混用的名詞釐清,並且深入理解 Iterable / Iterator 的運作機制,以及如何寫出自己的 Iterable / Iterator 運用到自己的程式碼裡。最後的最後,會針對常出現在Iteration 中 yieldGenerator 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, rangestr 都是 Iterable .但不是 Iterator ! 要檢驗一個 object 是不是 iterator 有以下方法測試:

  1. 使用 collections 中的 IterableIterator
python_iteration_0.py

2. 根據定義,利用 dir() 或是 hasattr() 去檢查 attributes __iter__ , __getitem____next__

python_iteration_1.py

由上面程式碼,兩種方法都顯示,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__ 的幾種常見寫法。

  1. 利用 __iter____next__ 實作一個 Iterator:
python_iteration_2.py

由範例程式碼可以看到實作了 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_3.py

上面的程式碼邏輯上和 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

python_iteration_4.py

這個方式直觀許多,首先要注意 __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 完整介紹囉!

先觀察下面兩段範例程式碼:

python_iteration_5.py
python_iteration_6.py

搭配上面的程式碼,介紹一下 yieldGenerator object:

  1. yield 只能出現在 function 裡,而 call 帶有 yield 的 function 會回傳一個 Generator object。
  2. Generator object 是一個 Iterator,帶有 __iter____next__ attributes。
  3. 第一次 call next(generator) 執行內容等價於將原 function 執行到第一次出現 yield 之處,「暫停」執行,並返回 yield 後的值。
  4. 第二次之後每次 call next(generator) 都會不斷從上次「暫停」處繼續執行,直到 function 全部執行完畢並 raise StopIteration 。因為 yield 沒有將原 function 從 call stack 中移除,故能暫停並重新回到上次暫停處繼續執行。這邏輯也是 yieldreturn 最核心不同之處,return 會直接將原 function 從 call stack 中移除,終止 function,不論 return 後面是是否還有其他程式碼。
  5. yeild 除了可傳出值外,也可以接受由外部輸入的值,利用 generator.send() 即可同時傳入值也傳出值。此設計,讓 Generator object 可和外部進行雙向溝通,可以傳出也可以傳入值。
  6. 關於 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,讓我們有動力繼續煮下一頓料理!

--

--

Jack Cheng
整個程式都是我的咖啡館

Interested in ML, algorithms, and back-end. Studied M.S. at NTU GICE.