Q1次のコードを実行したとき、print(type(gen))の出力に最も近いものはどれですか?def f():
yield 1
yield 2
gen = f()
print(type(gen))
ジェネレーター関数 yield — 値を 1 つずつ生成してメモリを抑える
yieldで処理を中断し続きから再開する関数。next()とforでの取り出し、100万件でもメモリ数百バイトで済むしくみ、yield fromで支店ごとのジェネレーターを1本に連結する書き方を扱います。
前回はクロージャで、関数の中に状態を閉じ込めて毎回違う値を返す書き方を見ました。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))