[Python]イテレータとは何者なのか?

今回は初心者が掴みにくい用語上位に来るであろう「イテレータ」と、それに付随する「イテラブルオブジェクト」のお話です。(僕もかなり苦労しました)

  • イテレータは「要素を指し示すもの」と言われるけど、実体がよく分からない
  • イテラブルオブジェクトは何がどうなって反復可能なの?

という方の参考になればと思います。

スポンサーリンク

イテレータ

イテレータの正しい理解

まずはイテレータです。イテレータとは一言でいうと、「要素を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種の場合、それぞれ組み込み関数iternextを呼び出したときの挙動を規定しています。

__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文で呼び出される度に、リストが新たにイテレータを生成してくれているおかげです。

タイトルとURLをコピーしました