Menu
日本語

Pythonジェネレータ入門|yieldと遅延評価を理解する

Pythonのジェネレータで値を遅延生成する仕組みを解説。yieldの使い方、ジェネレータ式、そしてリストより得する場面まで一通り押さえます。

一時停止できる関数

python のジェネレータは一見ふつうの関数に見えますが、結果を一気に計算して return するのではなく、値を1つずつ yield しては、次の値を求められるまでその場で処理を止めて待つ、という動きをします。

いちばんシンプルな例を見てみましょう。

main.py
Output
Click Run to see the output here.

ここで注目してほしいのが、return ではなく yield を使っている点です。for が最初に値を要求すると、Python は関数の中を yield 1 にぶつかるところまで実行します。そして関数は その場でいったん停止 し、1 をループに返し、どこで止まったかを変数ごと丸ごと覚えておくんです。次のイテレーションでは止まった続きから再開し、current += 1 を実行して while に戻り、yield 2 を返す。これをループ条件が成立しなくなるまで繰り返し、条件が満たされなくなった時点でジェネレータはそのまま終わります。

この「一時停止して、また再開する」という動きこそがジェネレータの核心です。

リストで作ればいいのでは?

結論から言うと、リスト版は値を最初に全部メモリに確保してしまうからです。

main.py
Output
Click Run to see the output here.

5個くらいなら、まあどっちでもいい話です。でも、5000万個の整数が欲しくて、しかも条件に合う最初の1個だけあればいい、というケースを想像してみてください。リスト版だと5000万個のintをまるごと確保してから、そのほとんどを捨てることになります。一方、ジェネレータ版は呼び出し側が消費した分だけしか生成しません。forループが目当ての値を見つけてbreakすれば、ジェネレータはそこでピタッと止まります。

ここで身につけておきたいのが次のパターンです。ジェネレータを使うと、結果をどれだけ使うかを事前に決めずにイテレーション処理を書ける、ということ。

ジェネレータ式

リスト内包表記を書いたことがあるなら、もう書き方はわかったも同然です。角括弧を丸括弧に変えるだけ。

main.py
Output
Click Run to see the output here.

squares_gen はまだ何も計算していません。あくまで「レシピ」を用意しただけで、イテレートするたびにレシピを1ステップずつ実行していく、というイメージです。

ジェネレータ式は、イテラブルを受け取る関数の引数として渡すときに特に真価を発揮します。

main.py
Output
Click Run to see the output here.

途中でリストを作る必要はありません。summaxany は値を1つずつ読み取っていくので、まさにジェネレータと相性ピッタリです。

巨大ファイルを1行ずつ読み込む

ここが python ジェネレータの王道ユースケースです。メモリに載り切らないほど大きなファイルでも、遅延評価で1行ずつ処理できます。

def parse_log_lines(path):
    with open(path) as f:
        for line in f:
            if line.startswith("ERROR"):
                yield line.rstrip()

for error in parse_log_lines("app.log"):
    print(error)

ファイルは遅延評価で読み込まれます。ジェネレータを呼び出すたびにディスクから1行だけ取り出し、フィルタして yield する流れです。ファイルサイズがどれだけ大きくなっても、メモリ使用量はほぼ一定のままです。

一度きりの走査

ジェネレータを走査できるのは一度だけです。最後までイテレートし終えたら、その時点で使い切られ、再利用はできません。

main.py
Output
Click Run to see the output here.

2回目のループでは何も出力されません。ジェネレータにはもう値が残っていないからです。

複数回イテレートしたいときは、ジェネレータ関数をもう一度呼んで新しいジェネレータを作り直すか、list(...) でシーケンスをまるごと展開してそのリストを何度も回すか、どちらかにします。使い分けはコスト次第です。処理が軽いなら作り直しで十分ですし、要素数が少ないならリスト化してしまっても問題ありません。

next() で手動イテレーションする

イテレートするのに for ループを使う必要はありません。next() を使えば、値を1つずつ取り出せます。

main.py
Output
Click Run to see the output here.

StopIteration は、ジェネレータが「もう値はないよ」と知らせるためのシグナルです。for ループはこの例外を内部で拾ってくれるので、特に意識する必要はありません。自分で手書きするときは、next(gen, default) のように第2引数にデフォルト値を渡しておけば、例外を出さずに済みます。

無限ジェネレータを作る

値は必要になったときに初めて生成されるので、終わりのないシーケンスでもジェネレータで表現できます。受け取る側が途中で止めさえすれば、問題なく動きます。

main.py
Output
Click Run to see the output here.

while True の中に yield を書いても、プログラムがフリーズすることはありません。これは「呼ばれ続ける限り、値を作り続けるよ」という意味になるだけで、いつ止めるかを決めるのはあくまで消費側(コンシューマ)です。

このパターンは、ストリーミングデータやイベントループなど、あらかじめ長さが決まっていないソースから値を取り出す場面でよく登場します。

yield from:別のイテラブルに処理を委譲する

ジェネレータの中で「別のイテラブルの値をそのまま全部 yield したい」というときは、yield from を使えば一行で書けます。

main.py
Output
Click Run to see the output here.

yield from を使わない場合は、for ループを入れ子にして中で yield x する書き方になります。ちなみに yield fromsend()throw() の転送もきちんとやってくれますが、普段のコードでは「この対象の値を全部そのまま yield する」くらいの理解で十分です。

ジェネレータを使うべき場面

ジェネレータが最適解になるサインは、だいたい次の3つです。

  1. シーケンスが巨大、あるいは無限、または全部生成するのにコストが高い。
  2. 消費側が途中で止まる可能性がある(最初のマッチで break するようなケース)。
  3. filter・map・take などの変換を、中間リストを作らずにチェーンしたい。

逆に、使わないほうがいいのはこんなときです。

  • ランダムアクセスが必要(seq[42] など)。ジェネレータは前にしか進めません。
  • 同じシーケンスを何度もイテレートしたい。これはリストの出番です。
  • シーケンスが小さくて、すでに手元にある。素直にリスト内包表記を書くほうがシンプルです。

ジェネレータ、リスト内包表記、ただのリストは、それぞれ適した場面が違います。大事なのは、あまり迷わずにサッと選べるようになること。そのセンスを磨く近道は、ループを書くたびに「全部まとめて作るほうが合うか、1つずつ作るほうが合うか」を意識してみることです。

次回:コンテキストマネージャを深掘り

これで Python のイテレーションまわりの主要なイディオムはほぼ押さえられました。次は with 文、つまりコンテキストマネージャです。ファイルやネットワーク接続からデータをストリームで流すような場面で、ジェネレータと組み合わせると相性が抜群です。

よくある質問

Pythonのジェネレータとは何ですか?

ジェネレータは、値を1つずつ生成して途中で処理を一時停止できる関数です。書き方は普通の関数と同じくdefを使いますが、returnの代わりにyieldを使うのがポイント。呼び出すとジェネレータオブジェクトが返り、forループやnext()を呼ぶたびに次のyieldまでコードが実行されます。

リストとジェネレータの違いは?

リストは全要素をメモリに載せるのに対し、ジェネレータは必要になったタイミングで値を計算し、消費したら捨てていきます。巨大なデータや無限列を扱うならジェネレータがメモリをほとんど食わずに済みますが、結果を何度も使い回すような場面ではリストの方が扱いやすいです。

ジェネレータは2回ループできますか?

できません。ジェネレータは一度最後まで回すと使い切った状態になるので、2回目のforループでは何も出てきません。繰り返し使いたい場合は、ジェネレータ関数をもう一度呼び直して新しいジェネレータを作るか、list()でリスト化しておく必要があります。

Coddyでコードを学ぼう

始める