Q1次のコードを実行したとき、print(type(gen)) の出力に最も近いものはどれですか?def f():
yield 1
yield 2
gen = f()
print(type(gen))
ジェネレーター関数 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 が無い)
return との違い
通常の関数は return した時点で関数のスコープごと消え、次に呼んでも最初から実行されます。一方、ジェネレーター関数は yield で処理を中断し、ローカル変数や進行位置をそのまま保持します。次の next() で続きから再開できるのが大きな違いです。
for で順に取り出す — StopIteration を意識しなくていい
next() を毎回書くのは面倒で、StopIteration を自分で処理する必要もありません。代わりに for value in ジェネレーター: と書けば、Python が裏で next() を呼び続け、最後まで来たら自動でループを抜けてくれます。普段はこの書き方が圧倒的に多いです。
ループの中で 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
# ...(続く)
list との違い — メモリ使用量を抑える
0 から 999_999 までを扱うとき、リスト内包表記でリストを作ると100 万個の整数を一度にメモリに載せることになります。一方、ジェネレーターなら現在の 1 つだけをメモリに置き、必要になった時点で次を計算します。リストはおよそ数 MB のサイズになりますが、ジェネレーターオブジェクト自体は数百バイトしかありません。
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() などにそのまま渡せて便利です。
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 sub_gen() は for v in sub_gen(): yield v の短縮です。
サブが yield した値はそのまま外側の呼び出し側に届くので、
tokyo_sales の 1200, 980、
osaka_sales の 850, 1340 が
for amount in all_sales(): に順番に届きます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2ジェネレーター関数の next() をすべての yield を使い切ったあとにさらに呼ぶと、何が起きますか?
Q3次の 2 行のうち、メモリ使用量が圧倒的に少ないのはどちらですか?
A: data = [i for i in range(10**6)]
B: data = (i for i in range(10**6))