帶你搞懂 Python 的 Iterable, Iterator 與 Generator

Posted on  May 3, 2024  in  Python 程式設計 - 中階  by  Amo Chen  ‐ 4 min read

Python 的 Iterable, Iterator 與 Generator 是經常會讓人產生混淆的事物,因為這 3 個都可以用 for 迴圈走訪,因此有些熟悉 Python 的面試官很喜歡問這 3 者之間的差異,追根究底是因為對這 3 者不熟悉的話,很容易寫出類似以下的低效率程式碼:

set([x for x in iterator])

本文將透過實際範例帶大家認識 Iterable, Iterator 與 Generator! 再也不搞混!

本文環境

  • Python 3

iterable 是什麼?

An object capable of returning its members one at a time. Examples of iterables include all sequence types (such as liststr, and tuple and some non-sequence types like dictfile objects, and objects of any classes you define with an iter() method or with a getitem() method that implements sequence semantics.

首先, iterable 包含 2 種型態:

  1. Sequence types
  2. Non-sequence types

第 1 種包含 list, str, tuple, 這幾種型態都有實作 __getitem__(index) 方法,因此我們可以試著建立 1 個 list 並呼叫它的 __getitem__(index) 取得其中 1 個值:

>>> a = [1, 2, 3]
>>> a.__getitem__(1)
2

第 2 種則包含 dict, file objects, 這幾種型態則有實作 __iter__() 方法,因此我們可以使用 for 迴圈,一次一次取得它的資料:

>>> a = {'a': 1, 'b': 2, 'c': 3}
>>> for k in a.__iter__():
...     print(k)
...
a
b
c

所以只要符合以下 2 個條件就可以稱為 iterable :

  1. 實作 __getitem__() 方法
  2. 實作 __iter__() 方法

而 iterable 就代表可以被 for 迴圈走訪!

知道這些資訊之後,我們可以試著做 1 個僅實作 __getitem__() 方法就能被 for 迴圈走訪的 class:

class A:
    def __init__(self):
        self.a, self.b, self.c = 1, 2, 3
    def __getitem__(self, idx):
        if idx == 0:
           return self.a
        elif idx == 1:
           return self.b
        elif idx == 2:
           return self.c
        else:
           raise StopIteration

上述程式碼 for 迴圈會依據此長度從 0 開始,依序呼叫 __getitem__(self, idx) , 直到遇到 StopIteration 例外就會停止。

接著,試看看用 for 迴圈走訪上述 class 的實例,可以看到它列印出 1, 2, 3 對應的正是 self.a , self.b , self.c 的值:

>>> a = A()
>>> for x in a:
...     print(x)
...
1
2
3

帥吧!

實作 __iter__() 方法的版本如下:

class A:
    def __init__(self):
        self.a, self.b, self.c = 1, 2, 3
    def __iter__(self):
        return iter([self.a, self.b, self.c])

上述範例也同樣能夠用 for 迴圈走訪:

>>> a = A()
>>> for v in a:
...     print(v)
...
1
2
3

看到這裡,有人可能會想問為什麼前述範例的 __iter__() return 之前,需要用 iter() 函式將 [self.a, self.b, self.c] 包起來?

這是因為 __iter__() 方法規定必須回傳 1 個 iterator, 所以我們用 iter() 函式將 [self.a, self.b, self.c] 轉成 1 個 iterator 。

iterator.iter()

Return the iterator object itself. This is required to allow both containers and iterators to be used with the for and in statements. 

iterable 跟 iterator 又是什麼關係?

談到這裡, iterableiterator 實在太像了,他們之間到底有什麼關係?

回顧一下, iterable 是 1 個 protocol, 這個 protocol 規定只要實作以下任一方法就屬於 iterable:

  1. 實作 __getitem__() 方法
  2. 實作 __iter__() 方法

其中選擇實作 __iter__() 而且又額外實作 __next__() 方法的型態/類別,就稱為 iterator !

所以 iter() 函式回傳的 object 是有實作 __iter__()__next__() 2 個方法的喔,可以用以下範例試試:

>>> x = iter([1, 2, 3])
>>> dir(x)
>>> hasattr(x, '__next__')
True
>>> hasattr(x, '__iter__')
True

之所以 iterator 還需要實作 __next__() ,是因為 Python 規定 iterator 也要能夠回應 next() 函式的呼叫:

iterator

An object representing a stream of data. Repeated calls to the iterator’s next() method (or passing it to the built-in function next()) return successive items in the stream. 

所以話說回來, iterator 也是 1 種 iterable , 因為它有實作 __iter__() 方法。

不過反過來 iterable 不一定是 iterator 喔!因為 iterable 有可能是沒有實作 __iter__() 方法的那種!

接著,我們實作 1 個符合 iterator 規定,有實作 __iter__()__next__() 2 個方法的 class 吧!

class Node:
    def __init__(self, value, next=None):
        self.value = value
        self.next = next

class LinkedList:
    def __init__(self):
        # node1 -> node2 -> node3
        # head^
        # curr^
        node3 = Node(3)
        node2 = Node(2, next=node3)
        self.head = Node(1, node2)
        self.curr = self.head
    def __iter__(self):
        return self
    def __next__(self):
        node = self.curr
        if not node:
            raise StopIteration
        self.curr = self.curr.next
        return node

上述範例定義 1 個 class Node ,接著在 class LinkedList__init__() 方法中將 3 個 Node 串起來,變成 node1 -> node2 -> node3 的 linked list (連結串列)。

接著在範例的 __iter__() 方法中, return self 代表這個類別本身就是 iterator, 它會有屬於自己的 __next__() 方法處理 for 迴圈走訪或者 next() 函式的呼叫,這也是為什麼這個範例不像先前的範例用 iter() 回傳的原因,因為它自己有額外實作 __next__() 方法。

__next__() 在這個方法裡,它依序走訪每 1 個 node 後並回傳,直到 node 為 None, 就拋出 StopIteration 告訴 for 迴圈或者 next() 走訪結束。

上述範例的 iterator 除了能用 for 迴圈走訪之外,也能夠使用 next() 走訪。

使用 for 迴圈版本:

>>> a = LinkedList()
>>> for x in a:
...     print(x.value)
...
1
2
3

使用 next() 版本:

>>> a = LinkedList()
>>> next(a).value
1
>>> next(a).value
2
>>> next(a).value
3
>>> next(a).value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 15, in __next__
StopIteration

以上就是 iterableiterator 的差別。

等等!那 generator 呢?

寫 Python 的人,除了會碰到 iterable 與 iterator 之外, generator 自然也是會碰到的常客!

generator 只是簡稱,其全名為 generator function

generator function 是 1 個 function, 只是這個 function 會包含使用 yield 語法。 generator function 可以用 for 迴圈產生一系列的值,也可以用 next() 函式呼叫 1 次只取得 1 個值。

下列是 1 個經典的 generator function:

def one2ten():
    for x in range(1, 11):
        yield x

如果在 Python 直譯器中呼叫上述函式,可以看到它回傳的是 generator object, 這個 generator object 又被稱為 generator iterator :

>>> x = one2ten()
>>> x
<generator object one2ten at 0x102b1c9e0>

generator iterator

An object created by a generator function.

這個 generator iterator 就能夠使用 for 或者 next() 走訪。

for 走訪:

>>> x = one2ten()
>>> for i in x:
...     print(i)
...
1
2
3
4
5
6
7
8
9
10

next() 走訪:

>>> x = one2ten()
>>> next(x)
1
>>> next(x)
2

那 generator iterator 可以當 iterator 使用嗎?

p.s. 注意,是 generator iterator ,不是 generator function 。

答案是,可以!

因為 generator iterator 有實作 __iter__()__next__() 2 個方法,generator iterator 是 iterator 無誤,所以可以當 iterator 使用!

>>> x = one2ten()
>>> hasattr(x, '__next__')
True
>>> hasattr(x, '__iter__')
True

同時 generator iterator 也是 iterable , 因為它有實作 __iter__()

這就是 iterable, iterator, generator function 與 generator iterator 之間錯綜複雜的關係!

為什麼要懂 iterable, iterator, generator function 與 generator iterator 的分別?

其實 Python 的官方文件寫的相當清楚,每 1 個函式或方法接受什麼類型的參數都會寫出來,譬如常用的 set(iterable), 文件上已經清楚表明它接受 iterable ,所以我們可以給它 iterable 就好,不需要像下列範例一樣又額外多執行一次 list comprehension 之後,才代入給 set() 執行:

def generate_values():
    for x in (1, 1, 2, 2, 3, 3):
        yield x

set([x for x in generate_values()])

上述效率肯定會比起下列正確的做法來得更慢,以下範例直接 generator iterator 交給 set() 即可,不需要額外執行一次 list comprehension:

set(generate_values())

當我們了解 iterable, iterator, generator function 與 generator iterator 的分別之後,就可以減少降低 Python 效率的寫法!

總結

Iterable, iterator, generator 是 3 個 Python 程式設計的重要概念,了解這 3 者的差異,不僅可以更瞭解 iter() , next() , for 迴圈等執行細節,還可以用正確的方式呼叫 Python 所提供的各種函式、方法,避免效率問題!

以上!

Enjoy!

References

Glossary

Iterator Types

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!