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
A for loop calls __iter__ once to get an iterator, then calls __next__ repeatedly until StopIteration ends the loop.

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

Sequence diagram with two lifelines (caller and generator body). The caller calls count_up_to(3), which creates a generator without running the body. Each subsequent next() runs the body until the next yield, returns the yielded value, and pauses. The fourth next() falls off the end and raises StopIteration. The variable i is preserved across pauses.

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)がいつ停止するかを決めます。

Three boxes left-to-right: numbers(), squares(source), and the for-v-in-pipeline consumer. Three cycles are drawn beneath. In each cycle, the consumer sends a pull request leftward to squares, which sends a pull leftward to numbers; numbers yields a value rightward to squares, which yields its squared value rightward to the consumer.

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