帶你搞懂 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 list, str, and tuple and some non-sequence types like dict, file objects, and objects of any classes you define with an iter() method or with a getitem() method that implements sequence semantics.
首先, iterable 包含 2 種型態:
- Sequence types
- 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 :
- 實作
__getitem__()
方法 - 實作
__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 又是什麼關係?
談到這裡, iterable 與 iterator 實在太像了,他們之間到底有什麼關係?
回顧一下, iterable 是 1 個 protocol, 這個 protocol 規定只要實作以下任一方法就屬於 iterable:
- 實作
__getitem__()
方法 - 實作
__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
以上就是 iterable 與 iterator 的差別。
等等!那 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>
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!