Pythonのyieldを使ったジェネレーター関数の動作と活用方法をわかりやすく解説

スポンサーリンク
スポンサーリンク
amazon
スマイルSALE
--:--:--
ad. 価格範囲を指定して商品を探せます

yieldの概要

ジェネレーター関数の中断と再開(データの遅延評価) Python予約語

yield

概要 yield(イールド)は、関数を通常の「戻り値を返して終了する関数」から、「呼び出されるたびに値を1つずつ生成し、状態を維持したまま一時停止する特殊な関数(ジェネレーター関数)」へと変貌させるためのキーワードです。

わかりやすく説明 yield は、一気にすべての仕事をして成果物をドカンと渡すのではなく、「とりあえず1個できたから渡すね。次のリクエストが来るまで、私はここで今の状態のまま昼寝して待ってるよ」という、省エネかつマイペースな職人のような仕組みです。数百万件もの巨大なデータを一度にパソコンのメモリに載せてパンクさせるのを防ぎ、必要なときに必要な分だけ(オンデマンド)1件ずつ処理することができます。

  • return とは異なり、値を外に排出しながらも関数の内部状態(ローカル変数の値や次に実行する行)を記憶したまま一時停止します。
  • yield を含む関数を呼び出すと、内部のコードは即座に実行されず、next() で呼び出せる「ジェネレーターオブジェクト」が生成されます。
  • 大量のテキストファイル処理、ビッグデータの解析、無限に続くストリーミングデータの取得といった場面で、メモリ消費をほぼゼロに抑えるために必須となります。

yieldの考え方をイメージで理解

yieldの基本的な使い方

ジェネレーター関数を定義し、next() を使うことで、関数内のループを1ステップずつ動かして値を順番に回収する例です。

# yield を使ったジェネレーター関数
def count_up():
    for i in range(3):
        yield i  # ここで値を外に送り出し、関数の実行を一時停止します

# 関数を呼び出すと、ジェネレーターオブジェクトが作られます(まだ実行はされません)
gen = count_up()

print(next(gen))  # 結果: 0 (最初のループが走り、yield 0 で一時停止)
print(next(gen))  # 結果: 1 (前回の続きから再開、yield 1 で一時停止)
print(next(gen))  # 結果: 2 (さらに続きから再開、yield 2 で一時停止)
  • yield i が実行されるたびに、その時点での i の値を next() 側へ返却し、関数はその場所でピタッと動きを止めます。

ジェネレーターの「終わり」の動作

関数内のすべての yield を出力し終えたり、関数の末尾に到達したりすると、ジェネレーターは「もう出すデータがありません」という合図として特別なエラーを発生させます。

# next() の挙動と弾切れの瞬間
def simple_generator():
    yield "A"
    yield "B"
    yield "C"

gen = simple_generator()
print(next(gen))  # 結果: A
print(next(gen))  # 結果: B
print(next(gen))  # 結果: C

# もう関数の内部には yield が残っていません
print(next(gen))  # StopIteration エラーが発生してプログラムが停止します
  • すべての要素を取り出しきった後に next() を呼ぶと、StopIteration という例外(エラー)が発生します。これが「データの終端」を示すPythonの正規の仕様です。

forループでジェネレーターをスマートに処理する

実務では next() を手動で何度も書くことは稀です。for ループにジェネレーターを放り込むと、Pythonが裏側で自動的に next() を呼び出し、最後の StopIteration を検知して安全にループを終了してくれます。

# forループによる自動化
def count_down(n):
    while n > 0:
        yield n
        n -= 1

# forループが StopIteration を自動で処理するため、エラーなく綺麗に出力されます
for num in count_down(5):
    print(num)  # 結果: 5 4 3 2 1 が順番に改行されて出力されます

yield を使った「メモリを喰わない無限ループ」

通常の関数で無限ループを作ってリストにデータを詰め込むと、あっという間にパソコンのメモリが限界を迎えますが、yield を使えば「必要と言われた瞬間にその場で数値を1個作るだけ」なので、無限に続くデータであってもメモリ消費量は常に最小限(1個分)になります。

# 無限ループのジェネレーター
def infinite_counter():
    n = 0
    while True:
        yield n
        n += 1

counter = infinite_counter()
# どれだけ next() を呼び出しても、メモリを一切圧迫しません
print(next(counter))  # 結果: 0
print(next(counter))  # 結果: 1
print(next(counter))  # 結果: 2

yield を使って「外部から値を受け取る」応用(コルーチン)

yield はただ値を外へ出すだけでなく、received = yield のように記述することで、外部から send() メソッドを使って関数内部へデータを注入(逆流)させ、双方向のやり取りを行うことができます。

# send() を使ったデータの注入
def echo():
    while True:
        # 外部から send() されたデータが received に代入されます
        received = yield
        print(f"ジェネレーター内部で受信: {received}")

gen = echo()
next(gen)  # 【重要】最初の1回は next() を呼んで、最初の yield の場所まで関数を進めておく(プライミング)

gen.send("Hello")   # 結果: ジェネレーター内部で受信: Hello
gen.send("Python")  # 結果: ジェネレーター内部で受信: Python

yield の注意点

  • ジェネレーターは「一度きりの使い捨て」である(最重要):リストとは異なり、ジェネレーターは一度最後までデータを取り尽くすと、空っぽの抜け殻になります。もう一度最初からデータを回したい場合は、再度関数を呼び出して新しいジェネレーターオブジェクトを作り直さなければなりません。
    # 【失敗例】一度使い切ったジェネレーターを再利用しようとする
    def data_gen():
        yield 1
        yield 2
    
    g = data_gen()
    for x in g: print(x) # 1, 2 と出力される
    
    # 2回目のループを回そうとしても、g はすでに空っぽなので何も起きません
    for x in g: print(x) # 完全に無視されます(エラーも出ないため気づきにくいバグになります)
  • 中身の要素をインデックス([0]など)で直接指定して取得することはできない:ジェネレーターは「次のデータが何になるか」をその都度計算しているため、gen[2] のように途中の要素をピンポイントで狙い撃ちして取り出すことは不可能です。
  • デバッグが少し複雑になる:コードが1行ずつ上から下に流れるのではなく、呼び出し元と yield の間を何度も行ったり来たり(ジャンプ)するため、通常のステップ実行デバッグを行う際に頭が混乱しやすくなります。

yield のよくある質問

Q: `yield` と `return` の最大の違いを教えてください。
A: 関数にとっての「終わりの重さ」が違います。 return に出会うと、関数は自分の仕事部屋(メモリ)を完全に解体し、すべてのローカル変数を綺麗さっぱり消去して現世に帰還します。 yield は、値を1つ外の窓口に提出しますが、仕事部屋や書きかけのメモ(変数の状態)はそのまま残し、ドアに「ただいま一時停止中」の札をかけて待機します。次回 next() が呼ばれたら、その机の前の続きからペンを握り直すことができます。
Q: ジェネレーターを `list()` で囲って一瞬でリストに変換できると聞きましたが、やってもいいですか?
A: データの規模によります。list(gen) と書けば、すべての yield を限界まで自動で絞り出して一般的なリストに変換してくれます。しかし、これを数億件のデータや無限ループに対して実行してしまうと、yield が本来持っていた「省メモリ」という最強のメリットが完全に打ち消され、一気にメモリが限界を迎えてシステムがフリーズ(OOMクラッシュ)します。データサイズが確実に小さいと分かっている時以外は、そのまま for ループで回すのが鉄則です。

まとめ

yield は、関数の実行を細切れにし、必要になったタイミングで必要なデータだけを生成する「遅延評価(Lazy Evaluation)」を可能にするための超強力なキーワードです。

  • 関数の内部データを保存したまま処理を「一時停止・再開」させる。
  • 全データを一気にメモリに展開しないため、ギガバイトクラスの巨大なファイルや、無限に続くログデータの解析を低スペックな環境でも安全に行える。
  • for ループと組み合わせることで、エラーハンドリングを意識せずスマートに値を走査できる。

大量のインフラログを一行ずつ監視するプログラムや、スクレイピングで大量のWebページを順番に解析するクローラーなど、実務のバックエンド開発において yield はパフォーマンスを劇的に改善する武器となります。通常の return との挙動の違いを完璧に脳内でシミュレートできるようになりましょう。