🔄

Generatorとyield — メモリを食わないイテレーションの原理

yieldが関数実行を「一時停止」させるメカニズム

リストを作ると全体をメモリに載せる。1億個なら1億個分のメモリを使う。

# メモリを全部食う
numbers = [x * 2 for x in range(100_000_000)]

# メモリをほぼ使わない
numbers = (x * 2 for x in range(100_000_000))

2つ目はgenerator expression。値を事前に作らず、next()が呼ばれるたびに1つずつ計算する。

yieldが何をするか

通常の関数はreturnでスタックフレームが消える。ローカル変数、実行位置 — 全て消失。

yieldは違う。値を返しながらスタックフレームを保存する。次のnext()呼び出しでyieldの次の行から再開。ローカル変数もそのまま。

CPython内部

CPythonでgeneratorはPyGenObjectで実装。gi_frameにフレームオブジェクトが保存され、gi_codeにバイトコードが格納。next()_PyEval_EvalFrameDefaultが保存フレームで実行を再開。

通常の関数呼び出しと違い新しいフレームを作らない — 既存フレームをそのまま使う。generatorが軽量な理由。

キーポイント

1

yieldは値を返しながら関数のスタックフレームを保存する

2

next()呼び出しでyieldの次の行から再開 — ローカル変数維持

3

CPythonでPyGenObject.gi_frameにフレームが保存

4

send()でyield地点に値を注入可能 — coroutineの基盤

ユースケース

大容量ファイル処理 — 1行ずつ読みながらメモリ使用量を一定に維持 無限シーケンス — counter、fibonacciのような終わりのないイテレータ