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 は「__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)は再開の間も保持されます。

2本のライフライン(呼び出し側とジェネレータ本体)を持つ シーケンス図。呼び出し側が count_up_to(3) を呼び出すと、 本体を実行せずにジェネレータが作成されます。 その後の各 next() は、次の yield まで本体を実行し、 yield された値を返して一時停止します。4回目の next() は 末尾を抜けて StopIteration を送出します。変数 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)がいつ停止するかを決めます。

左から右に3つのボックス: numbers()、squares(source)、 そして for-v-in-pipeline の消費側。その下に3つの サイクルが描かれています。各サイクルで、消費側は プル要求を左方向の squares に送り、squares はさらに 左方向の numbers にプルを送ります。numbers は値を 右方向の squares に yield し、squares は二乗した値を 右方向の消費側に yield します。

消費側での各 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 地点を持つループとして自然に書ける場合はジェネレータを使い、シーケンス全体を本当にメモリ内に必要とする場合は単純なリスト内包表記を使ってください。