tags: Python
學過其他程式語言的人一開始看到 Python 也有 for
, 一定很想這樣寫:
>>> for i in 20:
... print(i)
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>>
不過馬上就會看到 Python 噴出錯誤訊息, 告訴你 20 這個整數 (int) 並不是可走訪的 (iterable, 或稱『可迭代的』) 物件, 那麼到底什麼是 iterable
呢?
可走訪的 (iterable) 物件與走訪器 (iterator)
簡單的來說, 可走訪的物件就是任何抽象上內含多項資料, 並且符合特定的規範, 可以一個一個輪流取出資料的物件, 你可以很直覺地想到串列就是這樣的物件, 因此我們就藉由串列來說明可走訪物件。
首先我們先建立一個串列:
>>> a = [10, 20,30]
剛剛提到可走訪物件必須符合特定的規範, 就是指它必須具有 __iter__()
方法, 這個方法要傳回一種特別的物件, 叫做走訪器 (iterator, 或稱『迭代器』), 實際要一一取出資料靠的就是走訪器:
>>> it = a.__iter__()
走訪器也和可走訪的物件一樣, 有它必須符合的規範, 它必須具有 __next__()
方法, 每次叫用時會從容器中取得下一項資料, 如果已經全部取出, 就要引發 StopIteration
例外。以下就示範透過剛剛傳回的走訪器一一輪流取出串列中的資料:
>>> it.__next__()
10
>>> it.__next__()
20
>>> it.__next__()
30
>>> it.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
既然串列是可走訪的物件, 當然就可以搭配 for
使用:
>>> for i in a:
... print(i)
...
10
20
30
>>>
for
會捕捉 StopIteration
例外, 不會在取出所有資料時讓程式意外結束。實際上你也可以用 try
和 while
完成一樣的動作:
>>> it = a.__iter__()
>>> try:
... while True:
... i = it.__next__()
... print(i)
... except StopIteration:
... pass
...
10
20
30
>>>
走訪器之所以要在最後引發例外, 是要讓程式可以分辨是否有取完所有資料, 在 for
敘述裡就可以加上 else
來處理發生例外的狀況:
>>> for i in a:
... print(i)
... else:
... print('completed')
...
10
20
30
completed
>>>
只要有完整走訪所有的資料, 沒有因為 break
跳離迴圈, 就會執行 else
部分。相同的功能以 try
和 whle
實作如下:
>>> it = a.__iter__()
>>> try:
... while True:
... i = it.__next__()
... print(i)
... except StopIteration:
... print('complted')
...
10
20
30
complted
>>>
range 是抽象的容器
可走訪物件並不一定要是真的容器, 只要抽象上可以一一取出資料即可, 像是 range()
建立的物件就是最常見的例子:
>>> r = range(4)
>>> type(r)
<class 'range'>
range()
因為不符 Python 類別名稱首字母大寫的慣例, 看起來像是叫用函式, 但其實它是建立物件, 你可以看到傳回的物件是 range
類別。range
就是一種抽象的容器, 它實際上並不會真的產生所有的資料, 而是在走訪時才依照規則產生目前項目的資料:
>>> it = r.__iter__()
>>> it.__next__()
0
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
走訪器也必須是可走訪的物件
走訪器除了必須具備 __next__()
以外, 也必須具備 __iter__()
, 也就是走訪器自己也要是可走訪的物件, 它的 __iter__()
要傳回自己:
>>> r = range(4)
>>> it = r.__iter__()
>>> it1 = it.__iter__()
>>> it is it1
True
你可以看到透過走訪器取得的走訪器就是它自己, 因此不管是使用 it
還是 it1
都可以走訪資料:
>>> it1.__next__()
0
>>> it1.__next__()
1
>>> it1.__next__()
2
>>> it1.__next__()
3
>>> it1.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
不過因為兩個名稱指的都是同一個走訪器, 所以走訪結束後即使換另一個名稱也不能再繼續走訪了:
>>> it.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
使用內建函式走訪資料
以 '__' 開頭並且結尾的方法其實並不是要讓我們直接叫用, 而是配合 Python 整體運作自動被叫用, 這類方法統稱為特別方法 (special method)。以走訪物件來說, for
會幫我們叫用 __iter__()
以及 __next__()
, 如果我們要自己透過走訪器走訪資料, 正規的做法應該是要叫用內建函式 iter()
與 next()
, 由它們幫我們叫用 __iter__()
以及 __next__()
, 例如:
>>> r = range(4)
>>> it = iter(r)
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
像是可走訪物件以及走訪器這樣必須符合特定的規範, 也就是必須具備某些特定功用的方法, 搭配對應機制運作的作法常常會運用在 Python 的不同機制中, 你也可以仿照相同的做法運用在自己的程式中。
設計自己的可走訪物件
了解了可走訪物件的機制後, 我們也可以設計自己的可走訪物件, 底下我們以一個能產生指定範圍內隨機整數的物件為例, 它會在產生的亂數剛好位於指定範圍的中間時引發例外停止走訪。由於走訪器自己就必須是可走訪物件, 所以可走訪物件最簡單的實作方法就是直接實作成走訪器:
>>> class r_iter:
... def __init__(self, stop):
... self.stop = stop
... random.seed()
... def __iter__(self):
... return self
... def __next__(self):
... r = random.randrange(self.stop)
... if r == int(self.stop / 2):
... raise StopIteration(F'Stopped@{r}')
... else:
... return r
...
>>>
先來測試看看:
>>> r = r_iter(4)
>>> r.__next__()
3
>>> r.__next__()
3
>>> r.__next__()
0
>>> r.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 10, in __next__
StopIteration: Stopped@2
>>>
由於指定的範圍是 0~4, 可以看到產生的亂數是中間的 2 時, 就會引發 StopIteration
例外。確認無誤後, 就可以試著和 for
搭配看看:
>>> for i in r_iter(4):
... print(i)
...
0
0
0
0
3
>>>
看起來沒問題, 再多試幾次:
>>> for i in r_iter(4):
... print(i)
...
>>>
由於是亂數, 所以也有可能一開始就產生中間值結束走訪, 每次都不一樣:
>>> for i in r_iter(4):
... print(i)
...
3
3
0
1
3
0
0
1
0
3
>>>
你可以看到要實作可走訪物件並不難, 而且搭配 for
運作看起來就像是 Python 原生的物件。另外, 有些教材會說 for
是用來建立固定次數的迴圈, 但其實這主要是看走訪的物件而定, 迴圈次數並非絕對是固定的。
生成器 (generator) 也是走訪器
其實要實作剛剛的亂數走訪器, 使用生成器 (generator) 會比較簡單, 像是以下就是改用生成器實作產生亂數的走訪器:
>>> def r_generator(stop):
... random.seed()
... while True:
... r = random.randrange(stop)
... if r == int(stop / 2):
... return
... else:
... yield(r)
...
>>>
叫用生成器的傳回值就是一個走訪器, 我們可以從它同時具備 __iter__()
以及 __next__()
來確認:
>>> r = r_generator(4)
>>> r.__iter__
<method-wrapper '__iter__' of generator object at 0x000001777D0E35A0>
>>> r.__next__
<method-wrapper '__next__' of generator object at 0x000001777D0E35A0>
>>>
既然是走訪器, 當然是可以依照前面所說一一走訪:
>>> r = r_generator(4)
>>> r.__next__()
3
>>> r.__next__()
3
>>> r.__next__()
1
>>> r.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
也可以搭配 for
運作:
>>> for i in r_generator(4):
... print()
...
KeyboardInterrupt
>>> for i in r_generator(4):
... print(i)
...
1
>>> for i in r_generator(4):
... print(i)
...
1
1
>>> for i in r_generator(4):
... print(i)
...
3
0
>>>
除了自己實作生成器, 也可以利用生成式 (generator expression) 產生生成器後取得走訪器, 例如:
>>> g = (i ** 2 for i in [1, 2, 3])
>>> g.__iter__
<method-wrapper '__iter__' of generator object at 0x000001777D0E2810>
>>> g.__next__
<method-wrapper '__next__' of generator object at 0x000001777D0E2810>
>>> for i in g:
... print(i)
...
1
4
9
>>>
你可以看到生成器就跟 range
物件一樣是抽象的容器, 都是在實際走訪時才產生目前的資料。
使用 collections.abc 模組檢查物件是否可走訪
要判斷某個物件是否可走訪, 可以透過 collections.abc
模組:
>>> import collections.abc
>>>
雖然可走訪物件以及走訪器不必是特定類別的物件, 但是在 collections.abs
中提供有 Iterator
以及 Iterable
類別可以搭配 isinstance()
以及 issubclass()
內建函式判斷是否為走訪器或是可走訪的物件, 例如:
>>> a = [1, 2, 3]
>>> isinstance(a, collections.abc.Iterable)
True
>>> isinstance(a, collections.abc.Iterator)
False
>>> it = a.__iter__()
>>> isinstance(it, collections.abc.Iterator)
True
>>>
可以看到串列是可走訪物件, 但不是走訪器, 一定要先取得串列的走訪器才能走訪內含的物件。相同的道理, range
物件也是如此:
>>> issubclass(range, collections.abc.Iterable)
True
>>> issubclass(range, collections.abc.Iterator)
False
>>> r = range(4)
>>> isinstance(r, collections.abc.Iterable)
True
>>> isinstance(r, collections.abc.Iterator)
False
>>> it = r.__iter__()
>>> isinstance(it, collections.abc.Iterable)
True
>>> isinstance(it, collections.abc.Iterator)
True
>>>
我們也可以用同樣的方法檢查前面自己設計的走訪器:
>>> r = r_iter(4)
>>> isinstance(r, collections.abc.Iterable)
True
>>> isinstance(r, collections.abc.Iterator)
True
>>> issubclass(r_iter, collections.abc.Iterator)
True
>>> issubclass(r_iter, collections.abc.Iterable)
True
>>>
你可以看到雖然在定義 r_iter
類別時並沒有繼承 Iterator
和 Iterable
類別, 但是叫用 issubclass()
依然是傳回 True
。如果覺得這樣很怪, 也可以在定義類別的時候明確的繼承 Iterator
。
小結
可走訪的物件在 Python 中常常會看到, 像是在 list()
的說明中, 就告訴你必須傳入可走訪的物件, 因此本文所提到的各種物件都可以用 list()
建立串列, 甚至是我們自己設計的走訪器也可以, 例如:
>>> list(r_iter(4))
[0, 0, 1]
>>> list(r_iter(4))
[0, 3]
>>> list(r_iter(4))
[3, 0, 1, 3, 1, 1, 1, 3]
>>>
只要多了解 Python 的這些機制, 就可以讓你自己設計的物件像是內建的物件一樣, 完美融合在 Python 的語法之中, 也可以和內建的函式合作無間。
Top comments (0)