今回は初心者が掴みにくい用語上位に来るであろう「イテレータ」と、それに付随する「イテラブルオブジェクト」のお話です。(僕もかなり苦労しました)
- イテレータは「要素を指し示すもの」と言われるけど、実体がよく分からない
- イテラブルオブジェクトは何がどうなって反復可能なの?
という方の参考になればと思います。
イテレータ
イテレータの正しい理解
まずはイテレータです。イテレータとは一言でいうと、「要素を1つずつ取り出せるオブジェクト」です。
入門書の中にはイテレータを「要素を指し示すポインター」と解説しているものもあります。リストを例に取って見てみましょう。
この赤枠の部分がイテレータだという解説ですが、これは大きな誤解を生む表現です。この認識では公式ドキュメントでよく目にする「イテレータを返す」や「イテレータを作る」という表現がなかなかしっくりきません。
なのでポインターという概念は捨ててしまいましょう。実際はこうです。
イテレータはイテレータオブジェクトという一つの型です。for文にかけられた場合、リストはイテレータオブジェクト(見た目は自分のコピー)を生成しますが、後はほとんど何もしません。「次の要素は?」ときかれて要素を提示しているのはイテレータオブジェクトです。
コードで見るイテレータ
作ってみる
このイテレータ、手動で簡単に生成することができます。組み込み関数iter
を使用してみましょう。
lst = ["H","e","l","l","o"]
iterator = iter(lst)
print(iterator) #<list_iterator object at 0x0000016DFB84CBB0>
リストを元にしたイテレータオブジェクトが出来上がりました。print
してもリストのように要素が見えるわけではありませんが、for文にかけてみると、
for i in iterator:
print(i)
#H
#e
#l
#l
#o
リストと全く同じように使用できます。
「次の要素は?」と尋ねる
イテレータオブジェクトにできてリストにできない機能として、「次の要素を指し示す」機能があります。
作成したイテレータを使って、「次の要素」を尋ねてみましょう。組み込み関数next
を使用します。
print(next(iterator)) #H
print(next(iterator)) #e
print(next(iterator)) #l
print(next(iterator)) #l
print(next(iterator)) #o
print(next(iterator)) #StopIteration
next
関数が呼び出される度に、イテレータは「次の要素」を提示します。「次の要素」が無くなった状態でさらにnext
関数を呼び出すとStopIteration
例外を送出します。これがつまりはfor文の終わりであり、イテレータからの「もう無いよ」という返答です。
なおこの関数は、リストなどのイテラブルオブジェクトには直接使用できません。
lst = ["H","e","l","l","o"]
print(next(lst))
#TypeError: 'list' object is not an iterator
イテレータ/イテラブルオブジェクトの内部
では次にイテレータとイテラブルオブジェクトの内部を覗いてみましょう。
ここで使用するのは組み込み関数dir
です。この関数の引数に何らかのオブジェクトを入れて呼び出せば、そのオブジェクトのプロパティ(属性)やメソッドが表示されます。
まずはイテレータをこの関数で調べてみましょう。
lst = ["H","e","l","l","o"]
iterator = iter(lst)
print(dir(iterator))
ズラリと出てくるプロパティ/メソッドの中に、__iter__
と__next__
があるのが確認できます。
アンダースコア2つで囲まれたものは特殊プロパティや特殊メソッドです。この2種の場合、それぞれ組み込み関数iter
とnext
を呼び出したときの挙動を規定しています。
__iter__
と__next__
は2つまとめてイテレータプロトコルと呼ばれ、この2つが実装されているということは、それ自身がイテレータを生成でき、「次の要素」を指し示せるオブジェクトであると言えます。これがイテレータの条件です。
__iter__
と__next__
両方のメソッドを実装している同じようにリストを調べてみましょう。
print(dir(lst))
イテレータを生成する__iter__
が特殊メソッドとして実装されていますが、__next__
はありません。リストは「イテレータを生成できるオブジェクト」であり、単体では「次の要素を指し示すことができないオブジェクト」です。
__iter__
が実装され、__next__
は実装されていないイテラブルオブジェクトの働き
ここまでの説明だと、こう感じる方もいるのではないでしょうか。
「じゃあfor文の中で、リストはただの入れ物なの?」
確かにリスト(イテラブルオブジェクト)はイテレータを生成しているだけに見えます。しかしイテレータを直接for文にかければいいかというと、そうでもありません。
lst = ["H","e","l","l","o"]
iterator = iter(lst)
for x in iterator:
print(x)
print(next(iterator))
#H
#e
#l
#l
#o
#StopIteration
for文が終了した後、改めてnext関数で「次の要素」をprint
していますが、結果はStopIteration例外です。
イテレータは次の要素を指し示すことができますが、その位置を戻したりリセットすることができません。そのため一度next
関数で使用されたイテレータは、二度と「まっさらな状態」で他で使い回すことができません。他で使うためには、
for x in iterator:
print(x)
iterator = iter(lst) #改めてイテレータを生成
print(next(iterator))
もう一度新しいイテレータを生成する必要があります。
リストや他のイテラブルオブジェクトであれば、このような挙動にはなりません。なぜならイテラブルオブジェクトは呼び出される度にイテラブルだからです。
lst = ["H","e","l","l","o"]
for x in lst:
print(x)
for x in lst:
print(x)
1つのソースコードでfor文を何度回してもリストの要素全てが処理されるのは、リストがイテレータそのものではなくイテラブルオブジェクトだからであり、for文で呼び出される度に、リストが新たにイテレータを生成してくれているおかげです。