2.31. イテレータとジェネレータ¶
for ループは見た目以上の仕事をしてきました。このページでは、それが動作の基盤としている イテレータプロトコル と、自分でイテレータを構築できるようにする yield キーワードを扱います。
2.31.1. イテレータプロトコル¶
ループ処理できるすべてのオブジェクトは、2つのメソッドを実装しています。
__iter__()-- オブジェクトの要素に対する イテレータ を返します。__next__()-- イテレータ上で、次の要素を返すか、もう要素がない場合はStopIterationを送出します。
組み込みの iter() は __iter__ を呼び出し、next() は __next__ を呼び出します。リストを手作業でたどってみましょう。
it = iter([10, 20, 30])
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
print(next(it)) # raises StopIteration
for は「__iter__ を一度呼び出し、その後 StopIteration まで __next__ をループする」ことの糖衣構文です。¶
for x in items: が実際に行っていること:
_it = iter(items)
while True:
try:
x = next(_it)
except StopIteration:
break
# ... loop body ...
すべてのリスト、タプル、文字列、辞書、集合、ファイルオブジェクト、ジェネレータはすでに __iter__ と __next__ を実装しています。だからこそ、それらはすべて for で動作するのです。
2.31.2. yield とジェネレータ関数¶
yield 文を含む関数は ジェネレータ関数 です。これを呼び出しても本体は実行されず、本体を一度に一つの yield ずつ実行する ジェネレータオブジェクト(イテレータ)が返されます。
def count_up_to(n):
i = 0
while i < n:
yield i
i += 1
for value in count_up_to(3):
print(value)
出力:
0
1
2
next() を呼び出すたびに、関数は次の yield まで再開し、その値を呼び出し側に渡してそこで一時停止します。ローカルな状態(この場合は i)は再開の間も保持されます。
next() は次の yield まで本体を実行し、値を返して一時停止します。ローカルな状態は一時停止後も保持されます。¶
ジェネレータはシーケンスを遅延生成する最も簡単な方法です。リストは構築されず、要素は消費側が要求したときにのみ計算され、関数は望めば永遠に要素を生成し続けることもできます。
2.31.3. 遅延パイプライン¶
ジェネレータはうまく合成できます。あるジェネレータの出力を別のジェネレータに供給できます。
def numbers():
i = 0
while True:
yield i
i += 1
def squares(source):
for x in source:
yield x * x
pipeline = squares(numbers())
for v in pipeline:
if v > 100:
break
print(v)
値はパイプラインを一度に一つずつ流れていきます。中間のリストはなく、numbers に組み込まれた上限もなく、消費側(for v in pipeline)がいつ停止するかを決めます。
消費側での各 next() は、チェーンを通じて1回のプルを引き起こします。値は何かが要求したときにのみ存在します。¶
2.31.3.1. yield from¶
別のイテラブルから要素を引き出して各要素を yield するループは十分によくあるので、Pythonはそのためのショートカットを提供しています。yield from iter 式は、イテラブルが生成する各値を順番に yield します。あたかもジェネレータがインラインで for x in iter: yield x ループを持っているかのようです。
def chain(*sources):
for source in sources:
yield from source
for v in chain([1, 2, 3], (4, 5), "abc"):
print(v)
出力:
1
2
3
4
5
a
b
c
yield from は明示的な for ループとまったく等価で、ただ短くなるだけです。さらに内側のイテラブルからの StopIteration を外側のジェネレータへきれいに伝播させます。これは複数のジェネレータを端から端まで連結するときに便利です。
2.31.3.2. yield が尽きたとき¶
ジェネレータ関数の末尾を抜ける(または明示的な return に到達する)と、自動的に StopIteration が送出されます。手作業で送出する必要はありません。周囲の for ループがそれを検知して終了します。
生成側のコードがいくつかの yield 地点を持つループとして自然に書ける場合はジェネレータを使い、シーケンス全体を本当にメモリ内に必要とする場合は単純なリスト内包表記を使ってください。