要了解python 哪些對象是可以迭代的,
可以先了解兩個相似的名詞
- Iterable
- Iterator
Iterable
可以被迭代、遍歷(loop, iteration)的物件對象可以被稱為iterable,
從官方文件得知,要實現__iter__或是__getitem__的方法即可。
包含了常見的list、tuple、set、dict、str、range,
>>> dir(str())
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', ......]
但若是使用collection去檢查是否是iterable
只有實現
__getitem__的對象可以被迭代但不會是iterable
只有 __getitem__ 的物件
Deck 裡放三張牌,只實作 __getitem__,把收到的索引轉給底下的 list(以下的輸出都來自 Python 3.14):
>>> class Deck:
... def __init__(self):
... self.cards = ["A", "K", "Q"]
... def __getitem__(self, i):
... return self.cards[i]
...
>>> for card in Deck():
... print(card)
...
A
K
Q
這個類別沒有 __iter__,for 迴圈照樣把三張牌跑完了。for 拿到一個物件的時候,會先呼叫 iter() 跟它要一個 iterator,接下來就一直對這個 iterator 呼叫 __next__,直到 StopIteration 出現為止。iter() 在物件身上找不到 __iter__ 的話,還有第二條路可以走,改成用索引一個一個取,這條路在 iter() 的說明裡跟 __iter__ 並列,「or it must support the sequence protocol (the __getitem__() method with integer arguments starting at 0)」。
這條路走起來長什麼樣子,把 __getitem__ 加一行 print 就看得到,同樣的迴圈再跑一次:
>>> class Deck:
... def __init__(self):
... self.cards = ["A", "K", "Q"]
... def __getitem__(self, i):
... print(f"__getitem__({i})")
... return self.cards[i]
...
>>> for card in Deck():
... print(card)
...
__getitem__(0)
A
__getitem__(1)
K
__getitem__(2)
Q
__getitem__(3)
索引從 0 開始,一次加一,三張牌拿完之後 for 又要了索引 3,底下的 list 這時丟出 IndexError,迴圈就在這裡停下來。這個停止條件寫在語言參考 __getitem__ 的說明底下,「The sequence iteration protocol (used, for example, in for loops), expects that an IndexError will be raised for illegal indexes to allow proper detection of the end of a sequence.」自己寫 __getitem__ 的時候,索引超出範圍要讓 IndexError 出來,回傳 None 或是丟別的例外,迴圈都停不下來。
中間那個 iterator 是 Python 自己補上的,也可以抓出來一步一步走:
>>> d = Deck()
>>> type(iter(d))
<class 'iterator'>
>>> it = iter(d)
>>> next(it)
'A'
>>> next(it)
'K'
>>> next(it)
'Q'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
~~~~^^^^
StopIteration
型別的名字就叫 iterator,裡面存著一個從 0 開始的計數器,每次 __next__ 拿這個數字去呼叫 Deck.__getitem__,接到 IndexError 之後換成 StopIteration 丟出來。Deck 自己沒有 __next__,前面那個 for 迴圈的每一步都發生在這個包裝物件上。每呼叫一次 iter(d) 就多一個新的包裝物件,計數器各自從 0 開始,所以同一個 Deck 連著跑兩次 for 迴圈,兩次都拿到完整的三張牌。
到這裡 Deck 的行為和一個普通序列沒什麼兩樣,換成用 collections.abc 去問它是不是 iterable,答案就不一樣了:
>>> from collections.abc import Iterable
>>> d = Deck()
>>> isinstance(d, Iterable)
False
>>> iter(d)
<iterator object at 0x000001363C1A40D0>
isinstance 回 False,iter() 卻拿得到東西,前面那個 for 也確實跑完了。差別在 Iterable 這個 ABC 檢查的範圍比較窄,標準庫裡它的 __subclasshook__ 只問一件事:
>>> import inspect
>>> from collections.abc import Iterable
>>> print(inspect.getsource(Iterable.__subclasshook__))
@classmethod
def __subclasshook__(cls, C):
if cls is Iterable:
return _check_methods(C, "__iter__")
return NotImplemented
_check_methods(C, "__iter__") 是到類別的 MRO 上找 __iter__,找不到就回 False,__getitem__ 完全不在檢查範圍內。這裡被問的是 Deck 這個類別,剛才那個包裝物件則兩個方法都有,拿它去問 isinstance,Iterable 和 Iterator 都會回 True。
平常用內建型別碰不到這個落差。前面 dir(str()) 印出來的清單裡,__iter__ 和 __getitem__ 兩個都在,list、tuple、range 也都有 __iter__,isinstance 問到的一律是 True。要自己寫一個只有 __getitem__ 的類別,兩邊的答案才會分開。
會踩到的地方是在函式入口做型別檢查那種寫法,收到參數先問 isinstance(x, Iterable),答案是 False 就把參數退回去,這樣會把只有 __getitem__ 的物件擋在外面,而那個物件用 for 跑得動。想知道一個東西能不能迭代,照文件的說法是呼叫 iter(obj) 看看,拿得到 iterator 就是可以,兩種協定都不支援的話 iter() 會自己丟出 TypeError。類別如果是自己寫的,補一個 __iter__ 進去,isinstance 也就通過了。
Iterator
https://docs.python.org/3/library/stdtypes.html#iterator-types
從官方文件看出,含有__iter__和__next__的對象可稱為iterator,
iterator是iterable的子集合,
上述提到的幾種方式是iterable但都不是iterator,可以使用上面用到的isinstance或是dir來確認,
>>> from collections.abc import Iterable, Iterator
>>> for i in ([1,2,3], "123", (1,2,3)):
... print(f"{i} is iterable: {isinstance(i, Iterable)}")
... print(f"{i} is iterator: {isinstance(i, Iterator)}")
...
[1, 2, 3] is iterable: True
[1, 2, 3] is iterator: False
123 is iterable: True
123 is iterator: False
(1, 2, 3) is iterable: True
(1, 2, 3) is iterator: False
而檔案物件則是 iterator
>>> file_path = os.path.abspath("test.py")
>>> with open(file_path) as ifile:
... isinstance(ifile, Iterator)
...
True
iterator 只能走一遍
iterator 除了 __next__ 之外還要有 __iter__,而且它的 __iter__ 回傳的是自己,所以每個 iterator 同時也是 iterable,可以直接丟進 for 迴圈。同一個 iterator 只能這樣走一遍,走完之後再丟進 for,一個值都拿不到。
把 list 和它的 iterator 擺在一起比較:
>>> nums = [1, 2, 3]
>>> iter(nums) is iter(nums)
False
>>> it = iter(nums)
>>> iter(it) is it
True
>>> list(it)
[1, 2, 3]
>>> list(it)
[]
iter(nums) 呼叫兩次拿到兩個不同的物件,is 比出來是 False,每次都是新的一個,各自從頭開始數。iter(it) 就不一樣,回來的是 it 本身,is 比出來是 True。第一次 list(it) 把三個值取完,第二次再取,it 已經走到底,list() 拿回來的是 []。
容器每次被丟進 iter() 或是 for 迴圈,都會交出一個新的 iterator;同樣的動作換成 iterator,拿回來的是上一輪已經走完的那一個,看起來就像一個空的容器。
這件事在函式之間傳參數的時候容易撞到。函式收到一個東西,先跑一次 for 算數量,再跑一次 for 做別的處理,參數是 list 的話兩次都正常,因為 for 每次都跟 list 要一個新的 iterator;換成 map()、filter()、zip() 回來的東西,或是一個 generator,或是前面那個用 open() 打開的檔案物件,第二次迴圈就一個值都拿不到。這不會丟出例外,資料看起來只是變成空的。需要走第二遍的話,先用 list() 把值收下來再用,或是每次迭代之前重新產生一個 iterator。
結語
了解了iterable和iterator,以後開發時,
若想創造出可以被迭代的對象或是迭代器,
則要知道必須要包含哪些基礎功能
那麼Generator呢?



