🔄
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のような終わりのないイテレータ