Pythonを学び始めると、イテレータやイテラブルという言葉にどこかで出会います。似た名前が2つ並んでいて、どう違うのかよくわからない。そう感じたことはありませんか?
じつはこの2つ、毎日書いているfor文の裏側でずっと働いています。仕組みを知らなくてもコードは動くので、つい後回しにしがちな話題です。
ところが、この違いを知らないままだと、ある日突然ふしぎなバグに出会います。同じデータを2回ループしたら、2回目だけ何も起きない。そんな現象です。
この記事では、iter()とnext()という2つの組み込み関数を使って、for文の中身を一歩ずつ再現していきます。サンプルの実行結果は、手元のPython 3.11で確かめたものです。
イテラブルとイテレータをひと言でいうと¶
まずは、2つの言葉の意味をざっくり押さえておきましょう。たとえ話を使うと、イメージがつかみやすくなります。
イテラブルは、本棚のようなものです。中に本が並んでいて、何度でも最初から眺められます。リスト、タプル、文字列、辞書、rangeなどがこの仲間です。
イテレータは、本棚の前に立って1冊ずつ本を手渡してくれる係の人です。いま何冊目まで渡したかを覚えていて、次をお願いされると続きの1冊を渡します。
ここで大事なのは、係の人は後戻りしないという点です。最後の1冊を渡し終えたら、それ以上は何も出てきません。
2つの違いを表にまとめると、次のようになります。
| 観点 | イテラブル | イテレータ |
|---|---|---|
| たとえ | 本が並んだ本棚 | 1冊ずつ手渡す係の人 |
| 代表例 | list、tuple、str、dict、range | iter()の戻り値、map、zip、filter、ファイル |
| 持っている特殊メソッド | __iter__ |
__iter__と__next__ |
| 何度もループできるか | できる | 一度使い切ると空になる |
| 今どこまで進んだか | 覚えていない | 覚えている |
公式ドキュメントの用語集でも、イテラブルはメンバーを1つずつ返せるオブジェクト、イテレータはデータの流れを表すオブジェクトと説明されています。表の右側の列が、この記事の主役です。
iter()とnext()でfor文の中身を再現する¶
言葉だけではピンとこないので、実際に手を動かしてみましょう。次のコードは、リストからイテレータを取り出し、1つずつ中身を受け取る例です。
fruits = ["りんご", "みかん", "ぶどう"]
it = iter(fruits) # 本棚から係の人を呼ぶ
print(type(it)) # <class 'list_iterator'>
print(next(it)) # りんご
print(next(it)) # みかん
print(next(it)) # ぶどう
print(next(it)) # StopIteration が発生する
iter()にイテラブルを渡すと、イテレータが返ってきます。そしてnext()を呼ぶたびに、次の要素が1つずつ出てきます。
4回目のnext()では、StopIterationという例外が発生します。これは、もう渡せる要素がありませんという合図です。
for文は、この流れを自動でやっている¶
ここまでくると、for文の正体が見えてきます。for文は内部でiter()を呼んでイテレータを作り、next()を繰り返し呼んでいるのです。
同じ動きをwhile文で書き直すと、次のようになります。少し長いですが、for文が裏でやっていることそのものです。
fruits = ["りんご", "みかん", "ぶどう"]
it = iter(fruits)
while True:
try:
fruit = next(it)
except StopIteration:
break # 合図が来たらループを抜ける
print(fruit)
for文はStopIterationを受け取ると、エラーとして表示せずに静かにループを終えます。だから私たちは、ふだんこの例外の存在を意識しなくて済んでいるわけです。
ちなみに、ループが最後まで回りきったときだけ動くfor-else構文も、この仕組みの上に成り立っています。気になる方はこちらもどうぞ。
【関連記事】for-else 文とは?else は if だけじゃない、ニッチで便利な使い道
next()に既定値を渡すと例外を避けられる¶
StopIterationを毎回tryで受け止めるのは少し面倒ですよね。そんなときは、next()の2つ目の引数に既定値を渡せます。
it = iter(["りんご"])
print(next(it, "おしまい")) # りんご
print(next(it, "おしまい")) # おしまい(例外にならない)
要素が残っていれば普通に返し、尽きていれば既定値を返します。条件に合う最初の1件だけ欲しいときに、この書き方がよく使われます。
2回目のループが空になる落とし穴¶
イテレータの性質がいちばん効いてくるのが、ここからの話です。次のコードを見て、結果を予想してみてください。
numbers = map(int, ["1", "2", "3"])
print(list(numbers)) # [1, 2, 3]
print(list(numbers)) # [] ← 2回目は空っぽ
1回目は期待どおりの結果が出ます。ところが2回目は、エラーも出ずに空のリストが返ってきます。
理由は、mapの戻り値がリストではなくイテレータだからです。1回目のlist()で係の人が最後まで本を渡し終えたので、2回目に頼んでも何も出てこないのです。
この現象は、mapだけでなくfilterやzipでも、ファイルオブジェクトでも同じように起きます。エラーにならずに静かに空になるので、気づくのが遅れやすいのが厄介なところです。
mapやfilterの使い方そのものは、こちらの記事でくわしく紹介しています。
【関連記事】Pythonのmap()とfilter()とは?リスト内包表記との使い分け
私も実務で、CSVファイルを読み込んだreaderを件数チェックで一度ループし、本処理で使おうとしたら0件だったことがあります。ログには何のエラーも出ておらず、原因にたどり着くまで半日かかりました。
inで調べるだけでも中身が減る¶
もう1つ、見落としやすいパターンがあります。イテレータに対してinで存在チェックをすると、見つかった位置までの要素が消費されてしまうのです。
numbers = map(int, ["1", "2", "3"])
print(3 in numbers) # True
print(list(numbers)) # [] ← 3まで読み進めたので残りがない
inは、目的の値が見つかるまでnext()を呼び続けます。リストなら何度調べても平気ですが、イテレータでは調べるたびに中身が減っていきます。
対策は、リストにしてから使うこと¶
何度も使うデータなら、最初にlist()でリストへ変換しておくのがいちばん確実です。リストはイテラブルなので、何回ループしても毎回最初から回せます。
ただし、数百万行のファイルのように大きなデータを丸ごとリストにすると、メモリを大きく使います。その場合は、ループを1回で済ませるように処理の順番を組み立て直すほうが向いています。
リストとイテレータを見分ける方法¶
では、手元のオブジェクトがどちらなのかは、どうやって見分ければよいのでしょうか。覚えやすいのは、iter()に渡したときに自分自身が返ってくるかどうかを確かめる方法です。
fruits = ["りんご", "みかん"]
print(iter(fruits) is fruits) # False(リストはイテラブル)
it = iter(fruits)
print(iter(it) is it) # True(イテレータは自分自身を返す)
z = zip([1, 2], [3, 4])
print(iter(z) is z) # True(zipはイテレータ)
r = range(3)
print(iter(r) is r) # False(rangeはイテラブル)
イテレータは、iter()を呼ばれると自分自身を返すように作られています。そのため、結果がTrueならイテレータ、Falseならイテラブルだと判断できます。
意外に感じるのはrangeではないでしょうか。rangeは何度ループしても最初から数え直せるイテラブルで、一度きりのイテレータではありません。
ふだんよく使う関数がどちらを返すのか、表にまとめておきます。
| 書き方 | 返ってくるもの | 2回ループできるか |
|---|---|---|
range(10) |
イテラブル | できる |
sorted(data) |
リスト | できる |
reversed(data) |
イテレータ | できない |
map()、filter() |
イテレータ | できない |
zip()、enumerate() |
イテレータ | できない |
open()の戻り値 |
イテレータ | できない |
dict.keys() |
ビュー(イテラブル) | できる |
zipやenumerateは、for文と組み合わせて使う場面がほとんどです。そのため、イテレータだと意識しないまま使っている方も多いはずです。
【関連記事】Pythonのzip関数の使い方を解説!複数のリストをスマートにまとめる技
自作クラスをfor文で回せるようにする¶
仕組みがわかると、自分で作ったクラスもfor文で回せるようになります。必要なのは、__iter__と__next__という2つの特殊メソッドです。
ここでは、指定した数から1まで数え下げるカウントダウンを作ってみます。本棚役とイテレータ役を、別々のクラスに分けているのがポイントです。
class Countdown:
"""何度でもループできるイテラブル(本棚役)"""
def __init__(self, start):
self.start = start
def __iter__(self):
# ループのたびに新しい係の人を用意する
return CountdownIterator(self.start)
class CountdownIterator:
"""1つずつ値を渡すイテレータ(係の人役)"""
def __init__(self, current):
self.current = current
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
countdown = Countdown(3)
print(list(countdown)) # [3, 2, 1]
print(list(countdown)) # [3, 2, 1] ← 何度でも最初から
Countdownの__iter__は、呼ばれるたびに新しいCountdownIteratorを作って返します。だから2回目のlist()でも、ちゃんと3から数え直してくれます。
一方で、CountdownIteratorを直接ループすると、2回目は空になります。今どこまで進んだかを自分の中に持っているからです。
この2つを1つのクラスにまとめて書くこともできます。ただしその場合は、mapと同じく一度きりのループになる点に注意してください。
ジェネレータを使えばもっと短く書ける¶
正直なところ、クラスを2つも書くのは少し大変ですよね。実務では、yieldを使ったジェネレータで書くほうがずっと一般的です。
def countdown(start):
while start > 0:
yield start
start -= 1
print(list(countdown(3))) # [3, 2, 1]
ジェネレータ関数を呼び出すと、__iter__と__next__を備えたイテレータが自動で作られます。StopIterationの発生も、関数の終わりに達したときにPythonが代わりにやってくれます。
yieldの動きをもっと知りたい方は、こちらの記事が参考になります。
【関連記事】Pythonのyieldとは?ジェネレータの仕組みを解説
イテレータを知るとうれしいこと¶
ここまで読んで、わざわざ一度きりの仕組みを使う理由はあるのかと思った方もいるかもしれません。じつは、この性質にははっきりとした利点があります。
いちばん大きいのは、メモリをほとんど使わないことです。イテレータは要素をすべて抱え込まず、頼まれたときに1つずつ作って渡します。
たとえば、10GBあるログファイルをfor文で1行ずつ読んでも、手元に置くのは今読んでいる1行だけです。リストに全行を読み込んでいたら、パソコンのメモリが足りなくなってしまいます。
もう1つの利点は、終わりのないデータも扱えることです。センサーから届き続ける値や、無限に続く連番も、必要な分だけ取り出せば済みます。
標準ライブラリのitertoolsには、こうしたイテレータを組み合わせるための道具がそろっています。仕組みを理解したうえで眺めると、使いどころがぐっとわかりやすくなるはずです。
【関連記事】itertoolsを使いこなせ!複雑なループ処理を1行で美しく書くテクニック
10年ほどエンジニアとして開発に関わってきて感じるのは、大きなデータでつまずく人ほど、全部をリストに入れてから処理しようとしがちだということです。イテレータのまま流すという発想を持っておくだけで、メモリ不足のトラブルはかなり減らせます。
まとめ¶
今回は、イテレータとイテラブルの違いを、iter()とnext()を使いながら見てきました。最後にポイントを振り返っておきましょう。
イテラブルは何度でも最初から回せる本棚で、イテレータは1冊ずつ渡す係の人です。for文は裏でiter()を呼び、StopIterationが来るまでnext()を繰り返しています。
map、filter、zip、enumerate、ファイルの戻り値はイテレータです。2回目のループは空になるので、何度も使うならlist()でリストにしておきましょう。
自作クラスをループさせたいときは__iter__と__next__を実装します。とはいえ、多くの場面ではyieldを使ったジェネレータのほうが短く安全に書けます。
まずは手元のPythonで、iter()とnext()を実際に呼んでみてください。for文がぐっと身近な存在に感じられるはずです。
ここまでお読みいただきありがとうございました。