for 的奧秘--可走訪 (可迭代, iterable) 物件
Tags: python, iterator, generator
Published: 2022-03-13 10:51:01

###### tags: `Python`

# 

學過其他程式語言的人一開始看到 Python 也有 [`for`](https://docs.python.org/3.10/reference/compound_stmts.html#the-for-statement), 一定很想這樣寫：

```python
>>> 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`](https://docs.python.org/3/library/stdtypes.html#iterator-types) 呢？

## 可走訪的 (iterable) 物件與走訪器 (iterator)

簡單的來說, 可走訪的物件就是任何抽象上**內含多項資料**, 並且符合**特定的規範**, 可以**一個一個輪流**取出資料的物件, 你可以很直覺地想到串列就是這樣的物件, 因此我們就藉由串列來說明可走訪物件。

首先我們先建立一個串列：

```python
>>> a = [10, 20,30]
```

剛剛提到可走訪物件必須符合**特定的規範**, 就是指它必須具有 [`__iter__()`](https://docs.python.org/3/library/stdtypes.html#container.__iter__)  方法, 這個方法要傳回一種特別的物件, 叫做**走訪器 (iterator, 或稱『迭代器』)**, 實際要一一取出資料靠的就是走訪器：

```python
>>> it = a.__iter__()
```

走訪器也和可走訪的物件一樣, 有它必須符合的規範, 它必須具有 [`__next__()`](https://docs.python.org/3/library/stdtypes.html#iterator.__next__) 方法, 每次叫用時會從容器中取得下一項資料, 如果已經全部取出, 就要引發 [`StopIteration`](https://docs.python.org/3/library/exceptions.html#StopIteration) 例外。以下就示範透過剛剛傳回的走訪器一一輪流取出串列中的資料：

```python
>>> it.__next__()
10
>>> it.__next__()
20
>>> it.__next__()
30
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>
```

既然串列是可走訪的物件, 當然就可以搭配 `for` 使用：

```python
>>> for i in a:
...     print(i)
...
10
20
30
>>>
```

`for` 會捕捉 `StopIteration` 例外, 不會在取出所有資料時讓程式意外結束。實際上你也可以用 `try` 和 `while` 完成一樣的動作：

```python
>>> it = a.__iter__()
>>> try:
...     while True:
...         i = it.__next__()
...         print(i)
... except StopIteration:
...     pass
...
10
20
30
>>>
```

走訪器之所以要在最後引發例外, 是要讓程式可以分辨是否有取完所有資料, 在 `for` 敘述裡就可以加上 `else` 來處理發生例外的狀況：

```python
>>> for i in a:
...     print(i)
... else:
...     print('completed')
...
10
20
30
completed
>>>
```

只要有完整走訪所有的資料, 沒有因為 `break` 跳離迴圈, 就會執行 `else` 部分。相同的功能以 `try` 和 `whle` 實作如下：

```python
>>> it = a.__iter__()
>>> try:
...     while True:
...         i = it.__next__()
...         print(i)
... except StopIteration:
...     print('complted')
...
10
20
30
complted
>>>
```

## range 是抽象的容器

可走訪物件並不一定要是真的容器, 只要抽象上可以一一取出資料即可, 像是 [`range()`](https://docs.python.org/3/library/stdtypes.html?highlight=range#range) 建立的物件就是最常見的例子：

```python
>>> r = range(4)
>>> type(r)
<class 'range'>
```

`range()` 因為不符 Python 類別名稱首字母大寫的慣例, 看起來像是叫用函式, 但其實它是建立物件, 你可以看到傳回的物件是 `range` 類別。`range` 就是一種抽象的容器, 它實際上並不會真的產生所有的資料, 而是在走訪時才依照規則產生目前項目的資料：

```python
>>> 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__()` 要傳回自己： 

```python
>>> r = range(4)
>>> it = r.__iter__()
>>> it1 = it.__iter__()
>>> it is it1
True
```

你可以看到透過走訪器取得的走訪器就是它自己, 因此不管是使用 `it` 還是 `it1` 都可以走訪資料：

```python
>>> 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
>>>
```

不過因為兩個名稱指的都是同一個走訪器, 所以走訪結束後即使換另一個名稱也不能再繼續走訪了：

```python
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>
```

## 使用內建函式走訪資料

以 '\_\_' 開頭並且結尾的方法其實並不是要讓我們直接叫用, 而是配合 Python 整體運作自動被叫用, 這類方法統稱為[**特別方法 (special method)**](https://docs.python.org/3/glossary.html#term-special-method)。以走訪物件來說, `for` 會幫我們叫用 `__iter__()` 以及 `__next__()`, 如果我們要自己透過走訪器走訪資料, 正規的做法應該是要叫用內建函式 [`iter()`](https://docs.python.org/3/library/functions.html#iter) 與 [`next()`](https://docs.python.org/3/library/functions.html#next), 由它們幫我們叫用 `__iter__()` 以及 `__next__()`, 例如：

```python
>>> 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 的不同機制中, 你也可以仿照相同的做法運用在自己的程式中。

## 設計自己的可走訪物件

了解了可走訪物件的機制後, 我們也可以設計自己的可走訪物件, 底下我們以一個能產生指定範圍內隨機整數的物件為例, 它會在產生的亂數剛好位於指定範圍的中間時引發例外停止走訪。由於走訪器自己就必須是可走訪物件, 所以可走訪物件最簡單的實作方法就是直接實作成走訪器：

```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
...
>>>
```

先來測試看看：

```python
>>> 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` 搭配看看：

```python
>>> for i in r_iter(4):
...     print(i)
...
0
0
0
0
3
>>>
```

看起來沒問題, 再多試幾次：

```python
>>> for i in r_iter(4):
...     print(i)
...
>>>
```

由於是亂數, 所以也有可能一開始就產生中間值結束走訪, 每次都不一樣：

```python
>>> for i in r_iter(4):
...     print(i)
...
3
3
0
1
3
0
0
1
0
3
>>>
```

你可以看到要實作可走訪物件並不難, 而且搭配 `for` 運作看起來就像是 Python 原生的物件。另外, 有些教材會說 `for` 是用來建立固定次數的迴圈, 但其實這主要是看走訪的物件而定, 迴圈次數並非絕對是固定的。

## 生成器 (generator) 也是走訪器

其實要實作剛剛的亂數走訪器, 使用[**生成器 (generator)**](https://docs.python.org/3/glossary.html#term-generator) 會比較簡單, 像是以下就是改用生成器實作產生亂數的走訪器:

```python
>>> def r_generator(stop):
...     random.seed()
...     while True:
...         r = random.randrange(stop)
...         if r == int(stop / 2):
...              return
...         else:
...              yield(r)
...
>>>
```

叫用生成器的傳回值就是一個走訪器, 我們可以從它同時具備 `__iter__()` 以及 `__next__()` 來確認：

```python
>>> r = r_generator(4)
>>> r.__iter__
<method-wrapper '__iter__' of generator object at 0x000001777D0E35A0>
>>> r.__next__
<method-wrapper '__next__' of generator object at 0x000001777D0E35A0>
>>>
```

既然是走訪器, 當然是可以依照前面所說一一走訪：

```python
>>> 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`運作：

```python
>>> 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)** 產生生成器後取得走訪器, 例如：

```python
>>> 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`](https://docs.python.org/3.10/library/collections.abc.html) 模組：

```python
>>> import collections.abc
>>>
```

雖然可走訪物件以及走訪器不必是特定類別的物件, 但是在 `collections.abs` 中提供有 [`Iterator`](https://docs.python.org/3.10/library/collections.abc.html#collections.abc.Iterator) 以及 [`Iterable`](https://docs.python.org/3.10/library/collections.abc.html#collections.abc.Iterable) 類別可以搭配 [`isinstance()`](https://docs.python.org/3/library/functions.html#isinstance) 以及 [`issubclass()`](https://docs.python.org/3/library/functions.html#issubclass) 內建函式判斷是否為走訪器或是可走訪的物件, 例如：

```python
>>> 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` 物件也是如此：

```python
>>> 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
>>>
```

我們也可以用同樣的方法檢查前面自己設計的走訪器：

```python
>>> 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()`](https://docs.python.org/3/library/functions.html#func-list) 的說明中, 就告訴你必須傳入可走訪的物件, 因此本文所提到的各種物件都可以用 `list()` 建立串列, 甚至是我們自己設計的走訪器也可以, 例如：

```python
>>> 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 的語法之中, 也可以和內建的函式合作無間。