この記事では、Pythonの「イテラブル」と「イテレータ」について解説します。
先に結論から言ってしまうとイテラブルとイテレータは以下のようなオブジェクトです。
これだけではよくわからないと思うのでもう少し深掘りして見ていきましょう!
イテラブルとは、for文で要素を1つずつ取り出せるような反復可能なオブジェクトのことを言います。「イテラブルオブジェクト」とも呼ばれたりもします。
Pythonには、以下のようなイテラブルが用意されています。
例として、様々なイテラブルの要素がfor文を用いて取り出せるか試してみます :
ls = [1, 2, 3] ds = {'a': 1, 'b': 2, 'c': 3} s = 'abc' bs = b'abc' for l, d, c, b in zip(ls, ds, s, bs): print(f'リスト: {l}, 辞書: {d}, 文字列: {c}, バイト: {b}')
実行結果
リスト: 1, 辞書: a, 文字列: a, バイト: 97 リスト: 2, 辞書: b, 文字列: b, バイト: 98 リスト: 3, 辞書: c, 文字列: c, バイト: 99
全てのオブジェクトで要素が1つずつ取り出せたのでイテラブルであることが確認できました!
for文が繰り返し処理する流れは以下のようになっています。
__next__メソッドを呼び出す__next__メソッドは順番に沿って要素を1つずつ返すStopIteration例外を送出するとループが終了するこのような手順で繰り返し処理を行なっています。つまり、イテラブルとは「イテレータオブジェクトを生成できるオブジェクトのこと」を言います。
任意のクラスにイテラブルとしての振る舞いを追加するには、__iter__()メソッドか__getitem()__メソッドを定義する必要があります。
__iter__()メソッド
このメソッドは、イテレータオブジェクトを返します。詳しくは後述しますが、イテレータとはデータを1つずつ取得できる型のことを言います。
__getitem__()メソッド
オブジェクト[key]で要素を取得できるようにする特殊メソッドです。ただし、keyが0から開始するシーケンスでなければならない。
この2つのメソッドのどちらかが定義されていれば、そのオブジェクトからイテレータオブジェクトを生成することができます。つまり、イテラブルとなります。
イテレータオブジェクトを生成するには、iter()関数を使います。
l = [1, 2, 3] # iter関数は__iter__メソッド呼び出す print(iter(l))
実行結果
<list_iterator object at 0x7f82e4a2ae50>
例として、適当なクラスに__getitem__()メソッドを定義し、イテレータオブジェクトを生成できるか試してみます :
class MyIterable: def __init__(self, x, y, z): self._x = x self._y = y self._z = z def __getitem__(self, item): return (self._x, self._y, self._z)[item] # インスタンス化 mi = MyIterable(1, 2, 3) # イテレータオブジェクト生成 iterator = iter(mi) print(iterator)
実行結果
<iterator object at 0x7fa5da234f10>
ユーザー定義クラスからイテレータオブジェクトを生成することができました。
イテレータとは型の1つで、順番に要素を取得できるオブジェクトのことを言います。
具体的には、__next__()メソッドが呼ばれるたびに要素を順番に返し、返せる要素がなくなった時にStopIteration例外を送出するオブジェクトです。
基本的にそれだけのオブジェクトです。
例として既存のイテラブルからiter()関数を使ってイテレータオブジェクトを生成し、for文で要素を取得する際の挙動の違いを見てみます :
l = [1, 2, 3] iterator = iter(l) for i in iterator: print(i)
実行結果
1 2 3
for文は、渡されたイテラブルからイテレータを生成してループ処理しているので初めからイテレータを渡したところで何ら変化はありません。
イテレータを使う上で注意しなければならないのは2度目の呼び出しです。イテレータは基本的に使い切りのオブジェクトなので1度要素を取り出してしまうと次からは取り出せなくなってしまいます。(2度目はすでに返せる要素がないので最初からStopIteration例外を送出する)
l = [1, 2, 3] iterator = iter(l) print('1度目の呼び出し') for i in iterator: print(i) print('2度目の呼び出し') for i in iterator: print(i)
実行結果
1度目の呼び出し 1 2 3 2度目の呼び出し
これはlist()などで変換した場合も同様なので注意。
l = [1, 2, 3] iterator = iter(l) # リストに変換 list(iterator) print('1度目の呼び出し') for i in iterator: print(i)
実行結果
1度目の呼び出し
リストなどが何度もfor文などで処理できていたのは、その都度イテレータを生成していたからということがよくわかりますね。
また、イテレータオブジェクトはnext()関数を使って次の要素を取得できます。
l = [1, 2, 3] iterator = iter(l) print(next(iterator)) print(next(iterator)) print(next(iterator)) print(next(iterator))
実行結果
1 2 3 Traceback (most recent call last): File "main.py", line 8, in <module> print(next(iterator)) StopIteration
__iter__()メソッドと__next__()メソッドを合わせて「イテレータプロトコル」と呼びます。
イテレータプロトコルを任意のクラスに実装することで、そのクラスがどのように反復処理するかを定義することができます。
それでは、この2つのメソッドがどのようなものなのか見ていきましょう!
__iter__()メソッドは、__next__()メソッドが定義されているオブジェクトを返すメソッド。
def __iter__(self): return __next__メソッドが定義されているオブジェクト
自身に__next__()メソッドが定義されている場合、selfを返すこともできる。
def __iter__(self): return self
__next__()メソッドは、要素をどのように返すかを定義するメソッドです。呼び出されるたびに要素を返します。返す要素がなければStopIteration例外を送出します。
def __next__(self): # 要素を返す # 返す要素がなければStopIteration例外を送出
イテレータプロトコルを実装したクラスを定義してみます。以下のコードでは、コンストラクタで受け取った不特定多数の引数を__next__()で1つずつ取り出せるようにしています :
class MyIterator: def __init__(self, *args): self._args = args self._index = 0 # __next__メソッドを定義しているオブジェクトを返す def __iter__(self): print('__iter__') return self # 呼び出されるたびに新しい要素を返す # 最後にStopIterationを返す def __next__(self): print('__next__') if self._index == len(self._args): print('StopIteration') raise StopIteration() arg = self._args[self._index] self._index += 1 return arg myiter = MyIterator(1, 2, 3) for arg in myiter: print(arg)
実行結果
__iter__ __next__ 1 __next__ 2 __next__ 3 __next__ StopIteration
簡単にメソッドの内容を見てみましょう!
自身に__next__()メソッドを定義したので__iter__メソッドではselfを返しました。
def __iter__(self): print('__iter__') return self
呼び出されるたびに_index変数をインクリメントし、毎回新たな要素を返します。_index変数と_args変数の長さが同じになったら返す要素がないのでStopIteration例外を送出する。
def __next__(self): print('__next__') if self._index == len(self._args): print('StopIteration') raise StopIteration() arg = self._args[self._index] self._index += 1 return arg
__iter__()メソッドと__next__()メソッドは、同じクラスに定義する必要はありません。先ほどのMyIterationクラスのイテレータプロトコルをバラバラに実装してみます。
# __iter__メソッドを定義したクラス class MyIter: def __init__(self, *args): self._args = args def __iter__(self): print('__iter__') # MyNextを呼び出す return MyNext(*self._args) # __next__メソッドを定義したクラス class MyNext: def __init__(self, *args): self._args = args self._index = 0 def __next__(self): print('__next__') if self._index == len(self._args): print('StopIteration') raise StopIteration() arg = self._args[self._index] self._index += 1 return arg myiter = MyIter(1, 2, 3) for arg in myiter: print(arg)
実行結果
__iter__ __next__ 1 __next__ 2 __next__ 3 __next__ StopIteration
他のクラスでも同じ__next__()メソッドを使いたい場合は、そのためのクラスを作成すると同じような実装を何度もしなくて済みます。
複雑になる場合も外部に実装することでクラスをすっきりさせることができます。
iter()関数は、反復プロトコルかシーケンスプロトコルからイテレータオブジェクトを返します。
iter(オブジェクト)
__iter__()メソッドを定義しているオブジェクト__getitem__()メソッドを定義しているオブジェクト以下のコードでは、iter()関数を使って可変長引数(タプル)のイテレータオブジェクトを自身の__iter__()メソッドで返しています。
class MyIter: def __init__(self, *args): self.__args = args def __iter__(self): return iter(self.__args) myiter = MyIter(1, 2, 3) for arg in myiter: print(arg)
実行結果
1 2 3
これを少し応用することで以下のようにインスタンス変数をまとめてfor文で取得できるようなクラスを作成できます。
class Person: def __init__(self, name, age, gender): self.name = name self.age = age self.gender = gender def __iter__(self): # インスタンス変数をタプルにまとめて、イテレータオブジェクトとして返す return iter((self.name, self.age, self.gender)) p = Person('田中太郎', '20', '男') for arg in p: print(arg)
実行結果
田中太郎 20 男
この記事では、Pythonのイテラブルとイテレータについて解説しました。
これらのことを覚えておけばイテレータを実装することはとても容易です。(実装する機会は少ないですが...)
それでは今回の内容はここまでです。ではまたどこかで〜( ・∀・)ノ
View Comments