Python `yield` 詳細解説:ジェネレータを使いこなそう

はじめに:`yield`ってなんだろう?

Pythonプログラミングをしていると、時折 `yield` というキーワードに出くわすことがあります。これは一見 `return` と似ているようで、実は全く異なる振る舞いをする、Pythonの強力な機能の一つです。

`yield` は、主にジェネレータ (Generator) と呼ばれる特別な関数を作成するために使われます。ジェネレータは、通常の関数とは異なり、処理の途中で値を返し、その後、必要に応じて処理を再開できるという特徴を持っています。

この特性により、特に大量のデータを扱う場合や、無限に続く可能性のあるシーケンス(数列など)を扱う場合に、メモリ効率を劇的に改善することができます。なぜなら、ジェネレータは一度にすべての値をメモリ上に保持するのではなく、必要になったタイミングで一つずつ値を生成(yield)するからです。

この記事では、`yield` の基本的な使い方から、ジェネレータの仕組み、応用例、そして `yield from` や `send()` といった高度な機能まで、深く掘り下げて解説していきます。さあ、`yield` の世界を探検しましょう!

基本的な使い方:`yield` でジェネレータ関数を作る

`yield` を使った関数は「ジェネレータ関数」と呼ばれます。通常の関数定義と同様に `def` を使いますが、値を返す箇所で `return` の代わりに `yield` を使用します。

簡単な例を見てみましょう。3つの挨拶を順番に返すジェネレータ関数です。


def simple_greeting_generator():
    print("ジェネレータ開始!")
    yield "おはようございます"
    print("一つ目のyieldの後")
    yield "こんにちは"
    print("二つ目のyieldの後")
    yield "こんばんは"
    print("ジェネレータ終了!")

# ジェネレータ関数を呼び出すと、ジェネレータオブジェクトが返される
greeting_gen = simple_greeting_generator()

print(f"ジェネレータオブジェクト: {greeting_gen}")

# next()関数で値を取り出す
print("--- 1回目のnext() ---")
message1 = next(greeting_gen)
print(f"受け取った値: {message1}")

print("--- 2回目のnext() ---")
message2 = next(greeting_gen)
print(f"受け取った値: {message2}")

print("--- 3回目のnext() ---")
message3 = next(greeting_gen)
print(f"受け取った値: {message3}")

# # これ以上yieldする値がない状態でnext()を呼ぶと StopIteration 例外が発生する
# print("--- 4回目のnext() ---")
# try:
#     next(greeting_gen)
# except StopIteration:
#     print("StopIterationが発生しました!")
            

このコードを実行すると、以下のようになります。


ジェネレータオブジェクト: <generator object simple_greeting_generator at 0x...>
--- 1回目のnext() ---
ジェネレータ開始!
受け取った値: おはようございます
--- 2回目のnext() ---
一つ目のyieldの後
受け取った値: こんにちは
--- 3回目のnext() ---
二つ目のyieldの後
受け取った値: こんばんは
            

注目すべき点は以下の通りです。

  • ジェネレータ関数 `simple_greeting_generator()` を呼び出しても、すぐには関数の中身は実行されません。代わりに「ジェネレータオブジェクト」が返されます。
  • `next()` 関数を呼び出すたびに、ジェネレータ関数内の処理が次の `yield` 文まで進み、`yield` で指定された値が返されます。
  • `yield` で処理が一時停止した後、次に `next()` が呼ばれると、停止した箇所から処理が再開されます。関数内の変数などの状態は保持されています。
  • すべての `yield` が実行された後に `next()` を呼び出すと、`StopIteration` 例外が発生します。これは、イテレーションの終了を示します。

forループでの利用

ジェネレータオブジェクトはイテレータでもあるため、`for` ループで直接使うことができます。`for` ループは内部で `next()` を呼び出し、`StopIteration` を自動的に捕捉してくれるため、より簡潔に記述できます。


def count_up_to(n):
    print(f"1から{n}まで数えます。")
    i = 1
    while i <= n:
        yield i
        i += 1
    print("カウント終了。")

# forループでジェネレータを使う
print("--- forループ開始 ---")
for number in count_up_to(5):
    print(f"受け取った数字: {number}")
print("--- forループ終了 ---")
            

実行結果:


--- forループ開始 ---
1から5まで数えます。
受け取った数字: 1
受け取った数字: 2
受け取った数字: 3
受け取った数字: 4
受け取った数字: 5
カウント終了。
--- forループ終了 ---
            

このように、`for` ループを使うと `StopIteration` を意識することなく、ジェネレータが生成する値を順番に処理できます。これがジェネレータの最も一般的な使い方です。

ジェネレータの仕組み:なぜメモリ効率が良いのか?

ジェネレータがメモリ効率が良い理由は、その「遅延評価 (Lazy Evaluation)」の性質にあります。

通常の関数がリストなどのシーケンスを返す場合、関数はシーケンスのすべての要素を計算し、メモリ上に格納してから、そのシーケンス全体を返します。もしシーケンスが非常に大きい場合、大量のメモリが必要になります。


import sys

# リストを返す関数
def create_large_list(n):
    print("リストを作成中...")
    result = []
    for i in range(n):
        result.append(i)
    print("リスト作成完了!")
    return result

# ジェネレータ関数
def create_large_generator(n):
    print("ジェネレータが呼ばれました。")
    for i in range(n):
        # print(f"{i} をyieldします。") # コメントアウト解除すると、yieldのタイミングがわかる
        yield i
    print("ジェネレータ終了。")

num_elements = 1_000_000 # 100万要素

# リストの場合
print("--- リストのアプローチ ---")
large_list = create_large_list(num_elements)
print(f"リストのメモリサイズ: {sys.getsizeof(large_list) / 1024 / 1024:.2f} MB")
# # リストの最初の5要素にアクセス
# print("最初の5要素:", large_list[:5])

print("\\n--- ジェネレータのアプローチ ---")
large_gen = create_large_generator(num_elements)
print(f"ジェネレータオブジェクトのメモリサイズ: {sys.getsizeof(large_gen) / 1024:.2f} KB")
# # ジェネレータの最初の5要素にアクセス
# print("最初の5要素:")
# for _ in range(5):
#     print(next(large_gen), end=" ")
# print()

# メモリ使用量の比較(おおよその目安)
# 注意: 実際のメモリ使用量は環境によって異なります
list_memory = sys.getsizeof(create_large_list(num_elements))
gen_memory = sys.getsizeof(create_large_generator(num_elements)) # ジェネレータオブジェクト自体のサイズ

print(f"\\n比較:")
print(f"リスト ({num_elements}要素): {list_memory / 1024 / 1024:.2f} MB")
print(f"ジェネレータオブジェクト: {gen_memory / 1024:.2f} KB")
            

上記のコードを実行すると(環境によりますが)、リストは数MBのメモリを消費するのに対し、ジェネレータオブジェクト自体のサイズは非常に小さい(数KB程度)ことがわかります。

ジェネレータは、`next()` が呼ばれるまで次の値を計算しません。そして、値を `yield` すると、その時点での関数の状態(ローカル変数など)を保持したまま処理を一時停止します。次に `next()` が呼ばれると、保存された状態から処理を再開します。これにより、一度に一つの要素だけをメモリ上に保持すればよいため、巨大なデータセットや無限シーケンスを扱う際に、メモリ使用量を大幅に削減できるのです。

この仕組みは、Pythonのイテレータプロトコルに基づいています。ジェネレータオブジェクトは `__iter__()` メソッドと `__next__()` メソッドを持っており、イテレータとして振る舞います。`for` ループは、このプロトコルを利用してジェネレータから値を取り出しています。

`yield` の応用例:こんな時に便利!

1. 大規模データの処理

前述の通り、メモリに収まりきらないような巨大なファイルを処理する場合にジェネレータは非常に有効です。例えば、巨大なCSVファイルを1行ずつ読み込んで処理するケースです。


import csv
import time # 時間計測用
import os # ファイル作成用

# ダミーの巨大CSVファイルを作成する関数 (デモ用)
def create_dummy_csv(filename, rows):
    print(f"'{filename}' を作成中 ({rows}行)...")
    with open(filename, 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(['ID', 'Value', 'Timestamp']) # ヘッダー
        for i in range(rows):
            writer.writerow([i, f'Data-{i}', time.time()])
    print(f"'{filename}' 作成完了。")

# ジェネレータを使ってファイルを処理する関数
def process_csv_generator(filename):
    print(f"ジェネレータで '{filename}' を処理開始...")
    with open(filename, 'r', newline='') as f:
        reader = csv.reader(f)
        header = next(reader) # ヘッダー行をスキップ
        print(f"ヘッダー: {header}")
        for row in reader:
            # ここで各行に対する処理を行う (例: 特定の条件でフィルタリング、集計など)
            # 例としてIDが偶数の行だけをyieldする
            if int(row[0]) % 2 == 0:
                yield row
    print("ジェネレータ処理完了。")

# --- 実行 ---
dummy_file = 'large_data.csv'
num_rows = 500_000 # 50万行のダミーデータ

# # 初回実行時やファイルがない場合にダミーファイルを作成
# if not os.path.exists(dummy_file):
#     create_dummy_csv(dummy_file, num_rows)
# else:
#     print(f"'{dummy_file}' は既に存在します。")

# # ジェネレータを使って処理 (メモリ効率が良い)
# print("\\n--- ジェネレータによる処理 ---")
# start_time = time.time()
# processed_count = 0
# for processed_row in process_csv_generator(dummy_file):
#     # 処理された行に対する操作 (ここではカウントのみ)
#     processed_count += 1
#     if processed_count % 50000 == 0: # 5万行ごとに進捗表示
#         print(f"  {processed_count}行処理済み...")

# end_time = time.time()
# print(f"処理された行数: {processed_count}")
# print(f"処理時間: {end_time - start_time:.2f} 秒")

# # 参考:リストとして全行読み込む場合 (メモリ消費大)
# def process_csv_list(filename):
#     print(f"リストで '{filename}' を処理開始...")
#     with open(filename, 'r', newline='') as f:
#         reader = csv.reader(f)
#         header = next(reader)
#         all_rows = list(reader) # ここで全行をメモリに読み込む
#     print(f"全 {len(all_rows)} 行をメモリに読み込み完了。")
#     processed_rows = []
#     for row in all_rows:
#         if int(row[0]) % 2 == 0:
#             processed_rows.append(row)
#     print("リスト処理完了。")
#     return processed_rows

# print("\\n--- リストによる処理(比較用・実行注意) ---")
# # 大量データの場合、メモリ不足になる可能性あり
# try:
#     start_time_list = time.time()
#     processed_list = process_csv_list(dummy_file)
#     end_time_list = time.time()
#     print(f"処理された行数: {len(processed_list)}")
#     print(f"処理時間: {end_time_list - start_time_list:.2f} 秒")
# except MemoryError:
#      print("メモリ不足エラーが発生しました!")

# # 後片付け
# if os.path.exists(dummy_file):
#     print(f"\\n'{dummy_file}' を削除します。")
#     os.remove(dummy_file)
            

この例では、`process_csv_generator` 関数はファイル全体をメモリに読み込むことなく、一行ずつ読み込み、条件に合う行だけを `yield` します。これにより、ファイルサイズに関わらず一定の少ないメモリ使用量で処理を実行できます。

2. 無限シーケンスの生成

ジェネレータは、終わりのない(あるいは非常に長い)シーケンスを表現するのにも適しています。例えば、無限に続く自然数やフィボナッチ数列などです。


# 無限に自然数を生成するジェネレータ
def natural_numbers():
    n = 1
    while True:
        yield n
        n += 1

# フィボナッチ数列を生成するジェネレータ
def fibonacci_sequence():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

print("--- 自然数 (最初の10個) ---")
nat_gen = natural_numbers()
for _ in range(10):
    print(next(nat_gen), end=" ")
print("\\n")

print("--- フィボナッチ数列 (最初の15個) ---")
fib_gen = fibonacci_sequence()
for _ in range(15):
    print(next(fib_gen), end=" ")
print("\\n")
            

これらのジェネレータは理論上無限に値を生成し続けますが、`next()` が呼ばれたときに次の値を計算するだけなので、メモリを圧迫することはありません。

3. コルーチン (Coroutine) としての利用

`yield` は、単に値を生成するだけでなく、コルーチンとしての機能も持っています。コルーチンは、処理を中断・再開できるだけでなく、外部からデータを受け取ることも可能な、より汎用的なサブルーチンです。

Python 3.5 以降では `async`/`await` 構文が導入され、非同期処理は主にそちらで記述されますが、`yield` を使ったジェネレータベースのコルーチンも歴史的に重要であり、その概念は `async`/`await` の基礎にもなっています。

ジェネレータの `send()` メソッドを使うと、中断しているジェネレータに値を送り込むことができます。これは双方向の通信を可能にします。


def simple_coroutine():
    print("コルーチン開始")
    received_value = yield # ここで値を受け取るのを待つ
    print(f"値 '{received_value}' を受け取りました")
    received_value2 = yield received_value * 2 # 受け取った値を2倍してyieldし、次の値を受け取るのを待つ
    print(f"値 '{received_value2}' を受け取りました")
    print("コルーチン終了")

# コルーチン(ジェネレータ)を作成
coro = simple_coroutine()

# 最初にnext()を呼び出して、最初のyield式まで進める(重要!)
print("--- 最初のnext() ---")
next(coro) # 出力: コルーチン開始

# send()を使って値を送り込む
print("\\n--- 1回目のsend() ---")
returned_value = coro.send("Hello") # "Hello" を送り込む
print(f"yieldされた値: {returned_value}") # 出力: yieldされた値: None (最初のyieldは値を返さないため) -> 訂正:最初のyieldからの再開時にNoneが渡され、2番目のyieldで `received_value * 2` が返る

print("\\n--- 2回目のsend() ---")
try:
    coro.send(100) # 100 を送り込む
except StopIteration:
    print("StopIteration: コルーチンが正常終了しました。")

# 実行結果の訂正・詳細解説
# 1. coro = simple_coroutine() : ジェネレータオブジェクト作成
# 2. next(coro) : 実行開始 -> "コルーチン開始" -> 最初の yield で停止。この時点では何も yield されていない。
# 3. coro.send("Hello") :
#    - "Hello"が最初の yield 式の結果として received_value に代入される。
#    - 処理再開 -> "値 'Hello' を受け取りました"
#    - 次の yield received_value * 2 (つまり yield "HelloHello") で停止。
#    - "HelloHello" が send() の戻り値となる。 -> 訂正:上記コード例では数値の乗算なので `yield 100 * 2` などを想定すべき。最初のsendで"Hello"を送るとエラーになる。
#    -> 再訂正:最初の next() の後、最初の send() で送った値は最初の yield 式の結果になる。その後の処理が進み、次の yield があればその値が send() の戻り値になる。
#    -> 上記コードで send("Hello") をすると、文字列を受け取った後の `received_value * 2` でエラーになる。数値を送るべき。
# 4. coro.send(100) :
#    - 100 が2番目の yield 式の結果として received_value2 に代入される。
#    - 処理再開 -> "値 '100' を受け取りました"
#    - "コルーチン終了"
#    - 関数の終わりに到達したので StopIteration が発生。

# 正しい実行例
def numeric_coroutine():
    print("コルーチン開始 (数値)")
    received = yield # 最初の値受け取り待ち
    print(f"数値 {received} を受け取りました")
    doubled = yield received * 2 # 2倍した値をyieldし、次の値待ち
    print(f"数値 {doubled} を受け取りました")
    print("コルーチン終了 (数値)")

coro_num = numeric_coroutine()
print("--- 最初のnext() [数値] ---")
next(coro_num) # 開始して最初のyieldで待機

print("\\n--- 1回目のsend() [数値] ---")
result1 = coro_num.send(10) # 10 を送る
print(f"yieldされた値: {result1}") # 10 * 2 = 20 が yield される

print("\\n--- 2回目のsend() [数値] ---")
try:
    coro_num.send(50) # 50 を送る
except StopIteration:
    print("StopIteration: 数値コルーチンが正常終了しました。")

            

`send()` を使う場合、最初に `next()` を呼び出してジェネレータを最初の `yield` 式まで進める必要がある点に注意してください。これは、`yield` 式が値を受け取る準備をするためです。

コルーチンは、非同期I/O処理(ネットワーク通信やファイルアクセスなど)を効率的に行うための基盤技術として発展しました。

4. データパイプラインの構築

複数のジェネレータを連結して、データ処理のパイプラインを構築することができます。一つのジェネレータの出力を、次のジェネレータの入力として使うことで、段階的なデータ変換やフィルタリングをメモリ効率よく行えます。


# データのソース (例: 数値のストリーム)
def number_source(limit):
    print(f"[Source] 0から{limit-1}までの数値を生成")
    for i in range(limit):
        yield i

# フィルタリングするジェネレータ (例: 偶数のみ)
def even_filter(upstream_generator):
    print("[Filter] 偶数のみをフィルタリング")
    for value in upstream_generator:
        if value % 2 == 0:
            yield value

# 変換するジェネレータ (例: 2乗する)
def square_mapper(upstream_generator):
    print("[Mapper] 値を2乗に変換")
    for value in upstream_generator:
        yield value * value

# パイプラインを構築して実行
limit = 10
pipeline = square_mapper(even_filter(number_source(limit)))

print("\\n--- パイプライン実行 ---")
for result in pipeline:
    print(f"最終結果: {result}")

            

実行結果:


--- パイプライン実行 ---
[Mapper] 値を2乗に変換
[Filter] 偶数のみをフィルタリング
[Source] 0から9までの数値を生成
最終結果: 0
最終結果: 4
最終結果: 16
最終結果: 36
最終結果: 64
            

このパイプラインでは、データは `number_source` -> `even_filter` -> `square_mapper` の順に流れ、各ステップで必要な処理が施されます。データ全体を中間段階でメモリに保持する必要がないため、効率的です。

高度な機能:`yield from`, `send()`, `throw()`, `close()`

`yield from`:ジェネレータの委譲

Python 3.3 で導入された `yield from` 式は、あるジェネレータから別のジェネレータへ処理を簡単に委譲するための構文です。これにより、ジェネレータを入れ子にする際のコードが非常に簡潔になります。

`yield from` は、サブジェネレータ(委譲先のジェネレータ)が生成するすべての値を透過的に `yield` するだけでなく、`send()`, `throw()`, `close()` といった操作もサブジェネレータに適切に伝達します。


# サブジェネレータ
def sub_generator(name):
    print(f"  [Sub] '{name}' 開始")
    yield f"{name}: Part 1"
    yield f"{name}: Part 2"
    print(f"  [Sub] '{name}' 終了")
    return f"{name} 完了" # Python 3.3以降、ジェネレータはreturnで値を返せる

# メインジェネレータ (yield from を使用)
def main_generator_with_yield_from():
    print("[Main] 開始")
    result1 = yield from sub_generator("Sub A")
    print(f"[Main] '{result1}' を受け取りました")
    yield "--- Main बीच में ---" # बीच में = in between (Hindi)
    result2 = yield from sub_generator("Sub B")
    print(f"[Main] '{result2}' を受け取りました")
    print("[Main] 終了")

# 比較用: yield from を使わない場合
def main_generator_without_yield_from():
    print("[Main (no yield from)] 開始")
    # sub_generator("Sub A") の処理を手動で模倣
    sub_a = sub_generator("Sub A")
    try:
        while True:
            yield next(sub_a)
    except StopIteration as e:
        result1 = e.value # returnされた値を取得
        print(f"[Main (no yield from)] '{result1}' を受け取りました")

    yield "--- Main बीच में ---"

    # sub_generator("Sub B") の処理を手動で模倣
    sub_b = sub_generator("Sub B")
    try:
        while True:
            yield next(sub_b)
    except StopIteration as e:
        result2 = e.value
        print(f"[Main (no yield from)] '{result2}' を受け取りました")

    print("[Main (no yield from)] 終了")


print("--- yield from を使用 ---")
gen_yf = main_generator_with_yield_from()
for value in gen_yf:
    print(f"受け取った値: {value}")

print("\\n" + "="*30 + "\\n")

print("--- yield from を使用しない ---")
gen_noyf = main_generator_without_yield_from()
for value in gen_noyf:
    print(f"受け取った値: {value}")
            

実行結果を見ると、`yield from` を使った方がコードがシンプルで、サブジェネレータの `return` 値も自然に受け取れることがわかります。ネストされたジェネレータや、再帰的なジェネレータを扱う際に特に強力です。

`send()`:ジェネレータへの値の送信

既にコルーチンのセクションで触れましたが、`send(value)` メソッドを使うと、一時停止しているジェネレータの `yield` 式の位置に `value` を送り込むことができます。これにより、ジェネレータと呼び出し元との間で双方向の通信が可能になります。


def interactive_coroutine():
    print("インタラクティブコルーチン開始!名前を教えてください。")
    name = yield "名前待ち" # 最初のyield (値を返す & 受け取り準備)
    print(f"こんにちは、{name}さん!年齢は?")
    age = yield f"{name}さんの年齢待ち" # 2番目のyield
    print(f"{name}さんは{age}歳なんですね。")
    return f"{name}({age})さん、さようなら!"

coro_interactive = interactive_coroutine()

# 1. 最初のプロンプト("名前待ち")を受け取る
prompt1 = next(coro_interactive)
print(f"プロンプト1: {prompt1}")

# 2. 名前 ("Alice") を送信し、次のプロンプトを受け取る
prompt2 = coro_interactive.send("Alice")
print(f"プロンプト2: {prompt2}")

# 3. 年齢 (30) を送信し、終了メッセージを受け取る (StopIteration)
try:
    coro_interactive.send(30)
except StopIteration as e:
    final_message = e.value
    print(f"最終メッセージ: {final_message}")

            

`send()` は `next()` と同様にジェネレータを再開させますが、`yield` 式自体の値として `value` を渡す点が異なります。`next()` は内部的には `send(None)` を呼び出しているのと同じです。

`throw()`:ジェネレータへの例外送出

`throw(type, value=None, traceback=None)` メソッドを使うと、ジェネレータが一時停止している `yield` 式の箇所で、指定した例外を発生させることができます。これにより、外部からジェネレータの処理に介入し、エラーハンドリングを行わせることが可能になります。


def exception_handling_generator():
    print("[Gen] 開始")
    try:
        value = yield "初回yield"
        print(f"[Gen] '{value}' を受け取りました")
        value = yield "2回目yield"
        print(f"[Gen] '{value}' を受け取りました (ここは実行されないはず)")
    except ValueError as e:
        print(f"[Gen] ValueErrorをキャッチしました: {e}")
        yield "ValueError処理後"
    except TypeError:
        print(f"[Gen] TypeErrorをキャッチしました (処理せず再送出)")
        raise # キャッチした例外をそのまま再送出
    finally:
        print("[Gen] finallyブロック実行 (クリーンアップ)")
    print("[Gen] 正常終了") # 例外が発生しなければここに来る

gen_ex = exception_handling_generator()

# 1. 開始して最初のyieldまで進める
val1 = next(gen_ex)
print(f"[Main] 受け取り: {val1}")

# 2. ValueErrorを送出する
try:
    val2 = gen_ex.throw(ValueError, "テスト用のValueErrorです!")
    print(f"[Main] ValueError処理後の受け取り: {val2}")
except Exception as e:
    print(f"[Main] 予期せぬ例外をキャッチ: {type(e).__name__}: {e}")


# 3. 新しいジェネレータでTypeErrorを送出する (finallyも確認)
gen_ex2 = exception_handling_generator()
next(gen_ex2) # 開始
try:
    gen_ex2.throw(TypeError)
except TypeError:
    print("[Main] TypeErrorがジェネレータから再送出されたのをキャッチしました。")
except Exception as e:
    print(f"[Main] 予期せぬ例外をキャッチ: {type(e).__name__}: {e}")

            

`throw()` で送出された例外は、ジェネレータ内部の `try…except` ブロックで捕捉・処理できます。もし捕捉されなかったり、再送出されたりすると、`throw()` を呼び出した側に例外が伝播します。

`close()`:ジェネレータの終了処理

`close()` メソッドは、ジェネレータを終了させるために使います。`close()` が呼び出されると、ジェネレータが一時停止している `yield` 式の箇所で `GeneratorExit` 例外が発生します。

`GeneratorExit` は通常、`finally` ブロックでのリソース解放などのクリーンアップ処理を実行させるために使われます。`GeneratorExit` を捕捉して処理を続行しようとすると(例えば、さらに `yield` しようとすると)、`RuntimeError` が発生します。


def cleanup_generator():
    print("[Cleanup Gen] 開始")
    resource = "[リソース確保]" # 例: ファイルを開く、接続を確立するなど
    print(f"[Cleanup Gen] {resource}")
    try:
        i = 0
        while True:
             yield f"データ {i}"
             i += 1
    except GeneratorExit:
        print("[Cleanup Gen] GeneratorExitをキャッチしました。終了処理を行います。")
        # ここでクリーンアップ処理
        resource = "[リソース解放]"
        print(f"[Cleanup Gen] {resource}")
        # ここでさらに yield しようとすると RuntimeError になる
        # yield "終了後にyieldはできません"
    finally:
        # GeneratorExitが発生しなくても、正常終了や他の例外時にも呼ばれる
        print("[Cleanup Gen] finallyブロックが常に実行されます。")

gen_cleanup = cleanup_generator()

# いくつか値を取得
print(next(gen_cleanup))
print(next(gen_cleanup))

# ジェネレータを閉じる
print("[Main] close() を呼び出します...")
gen_cleanup.close()
print("[Main] close() の呼び出し後")

# close()後にnext()を呼ぶとStopIterationになる
try:
    next(gen_cleanup)
except StopIteration:
    print("[Main] close()後のnext()はStopIterationになります。")

            

`close()` は、`with` 文と組み合わせてコンテキストマネージャとしてジェネレータを利用する際などに、暗黙的に呼び出されることもあります (例: `@contextlib.contextmanager` デコレータ)。

`yield` vs `return`:何が違うの?

`yield` と `return` は関数から値を返すという点で似ていますが、その動作と目的は大きく異なります。

特徴 yield return
目的 ジェネレータ関数を作成し、値のシーケンスを生成する 関数の実行を終了し、単一の値(またはNone)を返す
関数の実行 値を返した後、関数の状態を保持し一時停止する。次回呼び出し時に再開。 値を返すと、関数は完全に終了し、ローカル状態は破棄される。
返り値 呼び出すとジェネレータオブジェクト (イテレータ) を返す。`next()` や `for` で値を取得。 指定された単一の値を直接返す。
呼び出し回数 関数内で複数回 `yield` 可能。それぞれの `yield` で値が返される。 関数内で `return` が実行されるのは一度だけ。
主な用途 メモリ効率の良いイテレーション、大規模データ処理、無限シーケンス、コルーチン 計算結果の返却、関数の終了
状態保持 する (ローカル変数など) しない (実行終了時に破棄)

簡単に言えば、

  • `return` は「関数の終わり」を意味し、最終結果を一つだけ返す。
  • `yield` は「一時停止と値の提供」を意味し、一連の値を順次生成するためのメカニズムを提供する。

どちらを使うかは、関数に何をさせたいかによって決まります。単純な結果を返すだけなら `return`、一連の値を効率的に生成したいなら `yield` を選びましょう。

注意点とベストプラクティス

ジェネレータは非常に強力ですが、使う上でいくつか注意点と推奨される使い方があります。

  • メモリ効率を活かす: ジェネレータの最大の利点はメモリ効率です。巨大なデータセットやストリーム処理など、メモリ使用量が問題になる場合に積極的に活用しましょう。
  • ジェネレータは使い切り: 通常、ジェネレータオブジェクトは一度最後までイテレートすると空になります。同じシーケンスを再度使いたい場合は、ジェネレータ関数をもう一度呼び出して新しいジェネレータオブジェクトを作成する必要があります。
  • 状態を持つことの意識: ジェネレータは内部状態を保持します。これが便利な反面、複雑な状態を持つジェネレータはデバッグが難しくなる可能性があります。できるだけシンプルに保つか、状態遷移を明確にすることが望ましいです。
  • `yield from` の活用: ジェネレータをネストする場合は、コードの可読性と保守性のために `yield from` を積極的に使いましょう。
  • ジェネレータ式: 単純なジェネレータであれば、リスト内包表記に似た「ジェネレータ式」を使うと、より簡潔に書けます。
    
    # リスト内包表記: すぐにリストが作られる
    squares_list = [x * x for x in range(10)]
    print(type(squares_list), squares_list)
    
    # ジェネレータ式: ジェネレータオブジェクトが作られる
    squares_gen = (x * x for x in range(10))
    print(type(squares_gen), squares_gen)
    print(list(squares_gen)) # イテレートしてリストに変換
                        
  • `return` との混同注意: ジェネレータ関数内で `return value` を使うと (Python 3.3以降)、ジェネレータはその時点で終了し、`StopIteration` 例外の `value` 属性として `value` が渡されます。`yield` と `return` の挙動の違いを正確に理解しておくことが重要です。
  • コルーチンと非同期処理: `send()`, `throw()`, `close()` を使った高度なコルーチンパターンは、`async`/`await` 構文の登場により、現代的な非同期プログラミングでは直接使われる機会が減っています。しかし、その概念を理解することは `asyncio` などのライブラリを深く理解する助けになります。

まとめ

`yield` キーワードとジェネレータは、Pythonにおける強力でエレガントな機能です。その主な利点は以下の通りです。

  • 圧倒的なメモリ効率: 大規模データや無限シーケンスを扱う際のメモリ消費を劇的に削減。
  • 遅延評価: 必要な時に必要な分だけ値を計算するため、パフォーマンス向上に貢献。
  • コードの簡潔化: イテレータを手動で実装するよりもシンプルに記述可能。
  • パイプライン処理: 複数の処理ステップを効率的に連結。
  • コルーチンの基盤: 非同期処理や協調的マルチタスクの基礎概念を提供。

`yield` を理解し、適切に使いこなすことで、より効率的で洗練されたPythonコードを書くことができるようになります。最初は少し戸惑うかもしれませんが、基本的な使い方から試していけば、その便利さを実感できるはずです。

ぜひ、あなたのプロジェクトでも `yield` とジェネレータを活用してみてください!Happy Coding!