順番に読み進めながら学べます

ジェネレーター関数 yield — 値を 1 つずつ生成してメモリを抑える

Python のジェネレーター関数 yield を図解で解説します。値を 1 つずつ返す書き方、next() での取り出し、メモリ削減のしくみまで一通り押さえます。

前回はクロージャで、関数の中に状態を閉じ込めて毎回違う値を返す書き方を見ました。Python にはこのような「呼ぶたびに次の値を返す」関数を専用で作るしくみがあり、それがジェネレーター関数です。return の代わりに yield を使うと、関数の途中で処理を止めて値を返し、次に呼ばれたときに続きから再開できます。

大きなデータを一気にメモリに展開せず1 件ずつ流す用途に向いており、ログ処理・大量データの処理などで活躍します。

yield の基本 — 1 つずつ値を返す関数

関数の中に yield 値 を書くと、その関数はジェネレーター関数です。普通の関数のように呼び出しても中身は実行されずgenerator という特殊なオブジェクトが返ってくるだけです。

値を実際に取り出すには next(オブジェクト) を呼びます。

最初の next() で関数の冒頭から最初の yield まで実行され、その値が返されます。

次の next() でその続きから次の yield まで進みます。

yield がもう無くなった状態でさらに next() を呼ぶと、StopIteration という例外が発生します。

def simple():
    yield 1
    yield 2

gen = simple()
print(type(gen))    # <class 'generator'>

print(next(gen))    # 1
print(next(gen))    # 2
print(next(gen))    # 3
# print(next(gen))  ← StopIteration(もう yield が無い)
yield と next() の関係
gen = simple()(まだ実行されない)generator オブジェクトが作られるnext(gen) → yield 1 で停止 → 1 が返るnext(gen) → yield 2 で停止 → 2 が返るnext() → yield 尽きてStopIteration1 回目2 回目3 回目以降

return との違い

通常の関数は return した時点で関数のスコープごと消え、次に呼んでも最初から実行されます。一方、ジェネレーター関数は yield処理を中断し、ローカル変数や進行位置をそのまま保持します。次の next() で続きから再開できるのが大きな違いです。

注文 ID を 1, 2, 3 と順番に発行するジェネレーター関数を作って、next() で 1 つずつ取り出します。

def order_ids(): を定義し、yield 1 / yield 2 / yield 3 を 3 行並べてください。

gen = order_ids() でジェネレーターオブジェクトを作成してください。

print(next(gen)) を 3 回続けて呼び出し、1 / 2 / 3 と返ってくることを確認してください。

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

for で順に取り出す — StopIteration を意識しなくていい

next() を毎回書くのは面倒で、StopIteration を自分で処理する必要もありません。代わりに for value in ジェネレーター: と書けば、Python が裏で next() を呼び続け、最後まで来たら自動でループを抜けてくれます。普段はこの書き方が圧倒的に多いです。

for ループとの組み合わせ
for v in gen:ジェネレーター側yield で値を返して停止呼び出し側ループ本体で値を使うyield 尽きたら自動終了次を要求値が届く

ループの中で print("進行中...") のような処理を入れると、yield するたびにジェネレーター側 → 呼び出し側 → ジェネレーター側 と交互に進んで実行されます。

def count_up_to(max_value):
    print("ジェネレーター開始")
    for i in range(max_value):
        print(f"  yield 直前: {i}")
        yield i
        print(f"  yield 後の続き: {i}")

for v in count_up_to(3):
    print(f"受け取った値: {v}")

# 出力の流れ:
# ジェネレーター開始
#   yield 直前: 0
# 受け取った値: 0
#   yield 後の続き: 0
#   yield 直前: 1
# ...(続く)

顧客データを 1 件ずつ流すジェネレーターを for で受け取って表示します。

def each_customer(): を定義し、for name in ["佐藤", "鈴木", "高橋"]: でループしながら yield name してください。

for name in each_customer(): で取り出し、print(f"次のお客様: {name}") を表示してください。

3 件すべてが順に表示されれば成功です。

Python エディタ

コードを実行してください

list との違い — メモリ使用量を抑える

0 から 999_999 までを扱うとき、リスト内包表記でリストを作ると100 万個の整数を一度にメモリに載せることになります。一方、ジェネレーターなら現在の 1 つだけをメモリに置き、必要になった時点で次を計算します。リストはおよそ数 MB のサイズになりますが、ジェネレーターオブジェクト自体は数百バイトしかありません。

list と generator のメモリ消費
list[0, 1, 2, ..., 999999]数 MB を一気にメモリへ全件を一覧したい用途に向くgenerator(i for i in range(...))数百バイト(現在の 1 件のみ)1 件ずつ流す用途に向く
import sys

MAX = 10 ** 6

# リスト: 全件を一気にメモリに載せる
data_list = [i for i in range(MAX)]
print(sys.getsizeof(data_list))
# 例: 8000056(およそ 8 MB)

# ジェネレーター式: 現在の 1 件だけ保持
data_gen = (i for i in range(MAX))
print(sys.getsizeof(data_gen))
# 例: 200 程度のバイト数

# 流して使う側のコードは同じ
for v in data_gen:
    if v > 2:
        break
    print(v)
# 0
# 1
# 2

ジェネレーター式という近道

リスト内包表記の角括弧 [ ... ] を丸括弧 ( ... ) に変えるだけで、ジェネレーター式になります。(i for i in range(1_000_000)) のように書くと、ジェネレーター関数を def で書かなくても 1 行で同じ効果が得られます。sum() / max() / any() などにそのまま渡せて便利です。

ジェネレーター式(リスト内包表記の角括弧を丸括弧に変えた書き方)で価格データを作り、type() で型を確認してから for で 1 件ずつ取り出します。

prices = (base * 100 for base in range(1, 6)) を作ってください([ ] ではなく ( ) を使うのがポイント)。

print(type(prices)) で型を表示し、<class 'generator'> と出ることを確認してください。

for p in prices: で取り出して、print(f"価格: {p}") を表示してください。

Python エディタ

コードを実行してください

yield from でジェネレーターを連結する

ジェネレーターの中から別のジェネレーターの値をそのまま流したいときは、for v in sub: yield v のように書く代わりに yield from サブジェネレーター と 1 行で書けます。複数のデータ源を 1 つのジェネレーターにまとめたいときに便利です。

例えば、東京支店と大阪支店、それぞれの売上を流す関数があるとき、yield from tokyo_sales()yield from osaka_sales() を続けて書けば、呼び出し側からは 1 本の流れに見えるジェネレーターになります。

def tokyo_sales():
    yield 1200
    yield 980

def osaka_sales():
    yield 850
    yield 1340

def all_sales():
    yield from tokyo_sales()
    yield from osaka_sales()

for amount in all_sales():
    print(amount)
# 1200
# 980
# 850
# 1340
yield from でサブジェネレーターに処理を委譲
呼び出し側for amountin all_sales()all_sales()メインジェネレーター受け取る順序1200 → 980→ 850 → 1340①yield fromtokyo_sales()tokyo_sales()yield 1200yield 980②yield fromosaka_sales()osaka_sales()yield 850yield 1340回す委譲尽きたら次委譲

yield from sub_gen()for v in sub_gen(): yield v の短縮です

サブが yield した値はそのまま外側の呼び出し側に届くので、

tokyo_sales1200, 980

osaka_sales850, 1340

for amount in all_sales(): に順番に届きます。

支店別の在庫リストを 1 つにまとめて流すジェネレーターを作ります。

def store_a():for item in ["りんご", "みかん"]: yield item してください。

def store_b():for item in ["バナナ", "ぶどう", "いちご"]: yield item してください。

def all_items(): を定義し、yield from store_a() のあとに yield from store_b() を続けて書いてください。

for item in all_items(): print(item) で 5 件を順に表示してください。

Python エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1次のコードを実行したとき、print(type(gen)) の出力に最も近いものはどれですか?
def f():
yield 1
yield 2
gen = f()
print(type(gen))

Q2ジェネレーター関数の next() をすべての yield を使い切ったあとにさらに呼ぶと、何が起きますか?

Q3次の 2 行のうち、メモリ使用量が圧倒的に少ないのはどちらですか?
A: data = [i for i in range(10**6)]
B: data = (i for i in range(10**6))