Iterator
はじめに
ここでは、イテラブルとイテレータとは何か、そして両者の関係について述べます。 また、その知識を前提として、Python における for 文の仕組みについて説明します。
イテラブル
イテラブル (iterable) は、英語としては、「繰り返し可能なもの」、「反復可能なもの」という意味の名詞です。 ここで「繰り返す」、「反復する」とは、Python の文脈では、値を連続的に取得し利用することを指します。 具体的には、
- for ループ
- リスト内包表記
in
演算子による包含関係の確認zip
関数やmap
関数
などは、いずれも値を連続的に取得して何らかの処理を実行しているといえるでしょう。 これらの処理の裏では、イテラブルが利用されています。 つまり、Python におけるイテラブルとは、for ループなどで繰り返し処理をおこなうことが可能なオブジェクトを意味するのです。
たとえば、list
はイテラブルの一つです。実際、
for i in [1, 2, 3]:
print(i)
のように、list
オブジェクトの各要素に対して for ループで走査することが可能です。
str
、tuple
、set
、dict
や file オブジェクトも、for ループを使用できることからイテラブルであることがわかります。
より厳密な定義は次のようになります。 すなわち、
__iter__
メソッドを定義しているクラスのオブジェクト (iter
関数を適用可能なオブジェクト)
をイテラブルと呼びます。
__iter__
メソッドは、後述するように、イテラブルからイテレータを取り出すためのメソッドです。
上の定義から、あるオブジェクトがイテラブルかどうかを確認するためには、
__iter__
メソッドが定義されているかどうかを調べればよいことがわかります。
この事実から、
>>> is_iterable = lambda x: hasattr(x, "__iter__")
>>> all(map(is_iterable, [list(), str(), tuple(), dict(), set()]))
True
のように、list
、str
、tuple
、dict
、set
がいずれもイテラブルであることを確認することができます。逆に、
>>> is_iterable(1)
False
>>> is_iterable(3.14)
False
>>> is_iterable(lambda x: x)
False
から、整数や浮動小数点数、関数などはイテラブルではありません。
イテレータ
イテレータ (iterator) は、組み込み関数である iter
をイテラブルに対して適用することで得られるオブジェクトです。
iter
は、適用対象の __iter__
メソッドを呼び出し、イテレータを返します。
つまり、イテラブルとイテレータの関係は次のようになっています:
iter(イテラブル) -> イテレータ
たとえば、イテラブルの一種である list
オブジェクトに対して iter
を適用すると、
list_iterator
というイテレータが生成されます:
>>> iter([1, 2, 3])
<list_iterator object at 0x10c6a73d0>
イテラブルとイテレータの関係が理解できたところで、次に、イテレータの役割について考えましょう。
イテレータは、連続するデータの流れを表します。 言い換えれば、イテレータは、データの集まりに順序という構造を与え、そこから値を一つずつ取り出すという機能をもちます。
実際にイテレータからデータを取得するためには、組み込み関数である next
を使用します。
next
は、イテレータの __next__
メソッドを呼び出します。
next
の役割はシンプルです:
- イテレータから次の値を取り出す
- 次の値がなくなれば
StopIteration
例外を返す
たとえば、上で生成した list_iterator
に対して、next
を適用すると次のようになります:
>>> i = iter([1, 2, 3])
>>> next(i)
1
>>> next(i)
2
>>> next(i)
3
>>> next(i)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
このように、イテレータは、イテラブルの値を next
関数により取り出す機能をもちます。
すなわち、イテレータとは
__next__
メソッドを定義しているクラスのオブジェクト (next
関数を適用可能なオブジェクト)
と定義できます。
__next__
メソッドの内部では、次の値の計算、計算結果の返却、StopIteration
例外の送出などがおこなわれます。
イテレータは、イテラブルというデータ構造に対して、そこから値を取り出していくためのロジックを付与したオブジェクトであるといえるでしょう。
なお、任意のイテレータはイテラブルです。
言い換えれば、イテレータに iter
関数を適用してイテレータを取得することが可能です。
これは、イテラブルを使用可能な場面でイテレータも使用することができるようにするためです。
あるイテレータに iter
を適用すると、同一のイテレータが返ります。
これらの事実をコードで説明すると、次のようになります:
>>> i = iter([1, 2, 3])
>>> hasattr(i, "__iter__") # イテレータはイテラブルである
True
>>> i is iter(i) # iterを適用すると同一のイテレータが返る
True
このことから、イテレータはイテラブルの特殊な形式であるということができます。
イテレータに関する補足
ここで、イテレータの使用に関して少し補足します。
まず、next
の適用により StopIteration
例外を送出するようになったイテレータから、もう一度値を取り出すことはできません。
値が再度必要である場合には、原則としてイテレータを再生成する必要があります。
また、値を __next__
メソッドにより都度計算するという性質上、イテレータに対して len
関数を適用することはできません:
>>> it = iter([1, 2, 3])
>>> len(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'list_iterator' has no len()
なお、これらの事実は、イテレータの一種であるジェネレータなどでも同様に当てはまります。
for 文
これまでの議論から、イテラブルからイテレータが生成されること、そして、イテレータに定義された __next__
メソッドを next
関数を利用して呼び出すことで、
値をイテラブルから連続的に取り出せることがわかりました。
続いてここでは、for 文の実際の動作を、これまでの知識をもとに説明していきます。
上にも書いた、次の for 文について考えましょう:
for i in [1, 2, 3]:
print(i)
このループを、イテラブルとイテレータを用いて表現すると次のようになります:
iterator = iter([1, 2, 3])
while True:
try:
i = next(iterator)
except StopIteration:
break
else:
print(i)
まず、イテラブルであるリスト [1, 2, 3]
に対して iter
関数を適用し、イテレータを取り出しています。
次に、このイテレータに対して next
関数を適用し、イテレータから値を取得します。
最後に、取得した値を print
関数により標準出力へと出力します。
取得できる値がなくなると、next
関数は StopIteration
例外を送出します。
この例外をキャッチすると、break
によりループを抜けます。
以上が、for 文実行時における実際の処理の流れとなります。
このように、for 文による繰り返し処理の裏側では、イテラブルとイテレータが利用されているのです。
for 文は、こうしたイテレータの生成や StopIteration
例外の発生時におけるループからの脱出などを自動でおこなってくれる、
一種の糖衣構文であるといえるでしょう。
実装例
最後に、イテラブルとイテレータの実装例を見ていきます。
イテラブルとは、__iter__
メソッドを定義したクラスのオブジェクトのことでした。
そして、__iter__
メソッドの役割は、対応するイテレータを返すことでした。
さらに、イテレータは、次の値を取得するための __next__
メソッドと自分自身を返す __iter__
メソッドが定義されたクラスのオブジェクトのことでした。
これらを念頭に、0 から与えられた整数まで順番に整数を返すイテレータを実装してみます:
class Counter:
def __init__(self, end):
self.end = end
def __iter__(self):
return CounterIterator(self)
class CounterIterator:
def __init__(self, counter):
self.n = 0
self.counter = counter
def __iter__(self):
return self
def __next__(self):
if self.n > self.counter.end:
raise StopIteration
n, self.n = self.n, self.n + 1
return n
if __name__ == "__main__":
for i in Counter(10):
print(i)
Counter
クラスには、__iter__
メソッドが定義されています。
よって、このクラスのオブジェクトはイテラブルです。
また、CounterIterator
クラスには、__next__
と __iter__
メソッドが定義されています。
よって、このクラスのオブジェクトはイテレータです。
Counter
クラスの __iter__
メソッドは、CounterIterator
クラスのオブジェクトを返しています。
つまり、イテレータを返しています。
CounterIterator
クラスの __next__
メソッドは、次の値を計算しそれを return
します。
また、終了条件が満たされる場合には StopIteration
例外を raise
します。
最後の二行に関しては、まず、Counter(10)
によりイテラブル e が生成されます。
このイテラブルが for 文の文脈において使用されることにより、CounterIterator
が生成されます。
そこから next
関数により値が取り出され、i
に代入されます。
こうして i
には次々と値が代入されていきますが、__next__
メソッドの実装から、self.n
の値が 11
となると StopIteration
が raise
されることがわかります。
このタイミングで、for 文の処理は終了します。
このプログラムの実行結果は次のようになります:
$ python it.py
0
1
2
3
4
5
6
7
8
9
10
このように、イテラブルとイテレータの仕様を満たすようにクラスを定義することで、そのオブジェクトに対して繰り返し処理を実行することができます。
なお、ここではイテラブルとイテレータを概念上区別するために上のように実装しましたが、任意のイテレータはイテラブルであるという事実を考慮すれば、実際はイテレータを一つ実装するだけで事足ります:
class Counter:
def __init__(self, end):
self.end = end
self.n = 0
def __iter__(self):
return self
def __next__(self):
if self.n > self.end:
raise StopIteration
n, self.n = self.n, self.n + 1
return n
if __name__ == "__main__":
for i in Counter(10):
print(i)
まとめ
- イテラブルは
- 繰り返し処理をおこなうことが可能なオブジェクトである
__iter__
メソッドが定義されたオブジェクト (iter
関数を適用可能なオブジェクト) であるiter
関数を適用すると、__iter__
メソッドが呼び出され、イテレータを返す
- イテレータは
- イテラブルから次の値を連続的に取り出すことが可能なオブジェクトである
__next__
メソッドが定義されたオブジェクト (next
関数を適用可能なオブジェクト) であるnext
関数を適用すると、__next__
メソッドが呼び出され、次の値を返す- 次の値がないときは、
StopIteration
例外を放出する - 自分自身もまたイテラブルである
- for 文は
iter
関数を使用し、イテレータを生成するnext
関数を使用し、イテレータから次の値を取得するStopIteration
例外を検知すると、ループを抜ける