Make組ブログ

Python、Webサービスや製品開発、ライブラリー開発についてhirokikyが書きます

Pythonのリストはイテレーターでない。わかりやすい(はずの)イテレーターとイテラブルの説明

こんにちは。 Pythonに関する書籍を書く際や、人に説明するときに「リストはイテレーター」と言っていませんか? それは間違いです。僕自身も勘違いしちゃうことがよくあるので、記憶を整理する意味でもブログにまとめておきます (もし間違えていたら教えてくれると嬉しいです)。

とくに、「リストはイテレーターです」、「rangeはイテレーターです」、「dict.keys()はイテレーターを返します」、など間違いが多いので注意しましょう。

リストはイテレーターではない

まず、リストはイテレーターではないということを感じてもらいます。 以下をPythonインタプリターで実行してください。 TypeError: 'list' object is not an iterator とエラーが表示されるはずです。

>>> l = [0, 1, 2, 3]
>>> next(l)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'list' object is not an iterator

はい。Pythonさんもリストはイテレーターではないと言っています。

「forループで使えるものはイテレーターじゃないの?」 という疑問があるかもしれません。うーん、惜しい。 その答えは イテラブル(Iterable) です。

リストもrangeオブジェクトもdict.keys()も、「イテラブル(Iterable)」と言えば間違いありません。 イテラブルは for in ... に渡せるものです。

早く答えが欲しい人へ

早い説明をします。イテラブルかイテレーターかの簡単な判別方法をお伝えします。 超ざっくりと説明すると、以下です。

  • iter(...) できるものは、イテラブル
  • next(...) できるものは、イテレータ

本に説明を書くときとかは、一度上記を実行しておくと安心かもしれません。 往々にして説明したいのは「イテラブル」だと思いますが。

じゃぁイテレーターって何?

イテレーターは反復するもので、その進行状況を持っているものです。 forループは指定されたイテラブルからイテレーターを取り出して、ループの1回1回を処理していきます。 おっと、意味がわかりませんね!(ブラウザを閉じないで!)

ではfor文の気持ちになってリストの処理をしましょう。 for文は以下のようにして、リストをループします。

>>> l = [0, 1, 2, 3]  # これはリスト
>>> l_iter = iter(l)  # リストからlist_iteratorを取り出す
>>> next(l_iter)
0
>>> next(l_iter)
1
>>> next(l_iter)
2
>>> next(l_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

このようにリストイテレータを順次 next(...) することで1つ1つリストの要素を取り出しています。 すべての要素が取り出されると、イテレーターは StopIteration を送出します。

なぜこのようになっているかというと、簡単に言えばforループの繰り返し処理を制御するためです。 リストをループするときに、ループがどこまで進んでいるかを管理するためにリストイテレーターが使われています。

もしリスト自身がイテレーターであれば、1度ループしただけでリストはループできなくなってしまいます(ループの進行状況が管理されるので)。 そのために、リスト自身ではなくリストイテレーターさん(つまりイテレーター)にループの状況を管理してもらっています。Pythonってすごい!

イテレーターは同時にイテラブルでもあります。 なので、イテレーターは for in ... に渡せます。この場合、イテレーターは iter(my_iterator) をすると my_iterator 自身を返します。

また、for文は以下のようにも模式的に書けます。 この my_for 関数は、第一引数にイテラブルを、第二引数にforブロックの処理に相当する関数を渡します。

>>> def my_for(iterable, iter_func):
...     it = iter(iterable)
...     try:
...         while True:
...             el = next(it)
...             iter_func(el)
...     except StopIteration:
...         pass
...

そうすると、forを使わずにforのような動きができます (あくまでも模式的にですが)。

>>> def print_pow(el):
...     print(el ** 2)
...
>>> my_for([0, 1, 2, 3], print_pow)
0
1
4
9

イテレーターって何があるの

じゃぁイテレーターって何があるの?と思われるかもしれません。 たとえばジェネレーターや BytesIO などがイテレーターです。よく「ループしたら使い切られる」やつがイテレーターです。

  • イテレーターではない(イテラブル)
    • 文字列
    • リスト
    • タプル
    • rangeオブジェクト
    • 辞書、辞書.keys()、辞書.values()、辞書.items()
    • 集合
  • イテレーター(イテラブル)
    • リストとかタプルとかrangeとかを iter したら返ってくるやつ
    • ジェネレーター
    • ファイルとかのI/O

ちなみに yield を使った関数は「ジェネレーターを返す関数」であり、イテラブルではありません。