Pythonのyield
は、関数の途中で値をひとつずつ外に渡しながら処理を一時停止できる強力な仕組みです。
return
との違いを正しく理解すると、メモリ効率が高く読みやすいコードを書けるようになります。
本記事では基礎から実例、活用テクニックまで段階的に解説します。
yieldとジェネレータの基礎
yieldキーワードの使い方を理解する
ジェネレータ関数とは
関数の本体にyield
を含む関数を呼び出すと、関数はすぐに実行されるのではなく、まずジェネレータオブジェクトを返します。
これは「準備ができた反復可能オブジェクト」であり、for
ループやnext()
で順次値を取り出せます。
# PEP8に沿ってスネークケースで命名します
# 型ヒントは理解を助けるために添えますが、必須ではありません
from typing import Iterator
def simple_gen() -> Iterator[int]:
# 最初の値を渡して一時停止
yield 1
# 再開後、次の値
yield 2
# 再開後、最後の値
yield 3
# ジェネレータは呼び出した瞬間には実行されない
g = simple_gen()
print(g) # <generator object ...>
# 値を順に取り出す
for x in g:
print(x)
<generator object simple_gen at 0x...>
1
2
3
ジェネレータは一度きり
ジェネレータは一度消費すると再利用できません。
再度値が必要な場合は新しく生成してください。
ジェネレータの仕組みと実行の流れ
一時停止と再開のイメージ
yield
に到達すると、その時点のローカル状態(ローカル変数や実行位置)を保持したまま値を外へ渡し、以後の実行は停止します。
次にnext()
やfor
ループが呼ばれると直後から再開されます。
def trace_gen():
print("start")
yield "A" # ここで一時停止
print("after A") # 再開地点
yield "B" # 再び一時停止
print("end")
g = trace_gen()
print(next(g)) # 1回目の再開
print(next(g)) # 2回目の再開
# 3回目のnextでStopIteration例外が内部的に発生する
try:
print(next(g))
except StopIteration:
print("done")
start
A
after A
B
end
done
StopIteration
は「もう値がない」ことを示す内部的な仕組みで、for
ループはこれを捕捉して自然にループを終了します。
遅延評価とメモリ効率のメリット
必要な分だけ作る
ジェネレータは値を「必要になった時にその場で」生成します。
巨大なリストを事前に作らないため、メモリ使用量を大きく削減できます。
import sys
# リスト内包表記は全要素を保持
lst = [n for n in range(1_000)]
# ジェネレータ式は要素を保持しない (イテレータだけ)
gen = (n for n in range(1_000))
print("list size:", sys.getsizeof(lst))
print("gen size :", sys.getsizeof(gen))
list size: 9016
gen size : 104
この差は要素数が増えるほど顕著になります。
IOやパイプライン処理で特に威力を発揮します。
returnとの違いと使い分け
関数の終了と一時停止の違い
それぞれのキーワードの本質
return
は関数を即座に終了して値を1つ返します。
yield
は値を1つ返して関数の実行を一時停止し、次の要求時に続きから再開します。
以下に主な違いをまとめます。
観点 | return | yield |
---|---|---|
実行制御 | 即時終了 | 一時停止と再開 |
返すもの | 単一の値 | 値の流れ(イテレータ) |
メモリ | まとめて生成しがち | 遅延生成で省メモリ |
使い所 | 単発の計算結果 | 逐次処理・ストリーミング |
例外終端 | 通常のreturn | 最終的にStopIterationで終端 |
再利用 | 関数を再呼び出し | ジェネレータを再生成 |
単一の戻り値と逐次的な出力
リストを返す関数 vs ジェネレータ関数
同じ「1からnまでの二乗」を返す処理を2通りで比較します。
from typing import List, Iterator
def squares_list(n: int) -> List[int]:
# すべて計算してリストに格納してから返す
return [i * i for i in range(n)]
def squares_gen(n: int) -> Iterator[int]:
# 値を1つずつ計算しながら返す
for i in range(n):
yield i * i
print("list:", squares_list(5))
print("gen first three:", [next(iter(squares_gen(5))) for _ in range(3)])
list: [0, 1, 4, 9, 16]
gen first three: [0, 0, 0]
上の最後の行はあえて落とし穴の例です。
同じiter(squares_gen(5))
を3回作ると毎回最初の要素からになります。
ジェネレータは一度きりなので、1つのイテレータを共有して進めるのが正しい使い方です。
it = iter(squares_gen(5))
print(next(it), next(it), next(it))
0 1 4
forループでの振る舞いの違い
ループは自動でnext()を呼ぶ
for
ループは内部でnext()
を繰り返し呼び、StopIteration
が起きたらループを終えます。
通常は明示的にnext()
を書く必要はありません。
def countdown(n: int):
print("ready")
while n > 0:
yield n
n -= 1
print("blastoff")
for x in countdown(3):
print(x)
ready
3
2
1
blastoff
return
をジェネレータ関数の中で書くと、以降のyield
は実行されず、for
ループは終了します。
初心者が最初に書くyieldの例
連番を生成するジェネレータ
startとstepを指定して有限回だけ数える
from typing import Iterator
def count(start: int = 0, step: int = 1, times: int = 5) -> Iterator[int]:
current = start
for _ in range(times):
yield current
current += step
for n in count(start=10, step=2, times=4):
print(n)
10
12
14
16
条件でフィルタしてyield
偶数だけを通すフィルタ
from typing import Iterable, Iterator
def only_even(numbers: Iterable[int]) -> Iterator[int]:
for n in numbers:
if n % 2 == 0:
yield n
print(list(only_even(range(10))))
[0, 2, 4, 6, 8]
大きなファイルを行ごとに処理
行を遅延評価で読む
実ファイルを扱う代わりに、ここではio.StringIO
でシミュレーションします。
実運用ではopen("path")
を使います。
import io
from typing import Iterator
def iter_lines(stream: io.TextIOBase) -> Iterator[str]:
# 各行をstripして逐次返す
for line in stream:
yield line.rstrip("\n")
fake_file = io.StringIO("a\nb\nc\n")
for line in iter_lines(fake_file):
print(f"line={line}")
line=a
line=b
line=c
巨大ファイルでも常に1行分だけメモリに載せるため、安定して処理できます。
ジェネレータ活用術
ジェネレータ式と関数の違い
簡潔さと再利用性のトレードオフ
ジェネレータ式は軽量で一時的な処理に向き、関数は名前やコメントを付けて再利用したい時に向きます。
# ジェネレータ式: すぐ使うワンライナーに
squares_expr = (x * x for x in range(5))
print(list(squares_expr))
# ジェネレータ関数: ロジックを分けて説明・テストしやすい
def squares_func(n: int):
for x in range(n):
# 複雑な前処理やログなども書ける
yield x * x
print(list(squares_func(5)))
[0, 1, 4, 9, 16]
[0, 1, 4, 9, 16]
特性 | ジェネレータ式 | ジェネレータ関数 |
---|---|---|
記述量 | 少ない | 多いが柔軟 |
再利用性 | 低い | 高い |
例外処理・分岐 | 書きにくい | 書きやすい |
デバッグ容易性 | 低い | 高い |
yield fromで処理を委譲
サブイテレータに丸投げする
yield from
は別の反復可能オブジェクトからの値をそのまま委譲して取り出させます。
ネストを平らにする時にも便利です。
def flatten(list_of_lists):
for sub in list_of_lists:
# subがイテラブルであれば、その中身をそのまま流す
yield from sub
print(list(flatten([[1, 2], (3, 4), range(5, 7)])))
[1, 2, 3, 4, 5, 6]
return値の委譲(中級)
ジェネレータはreturn
で終了時の値を返せます。
yield from
はその値を受け取り、StopIteration.value
経由で受け継ぎます。
def sub():
yield 1
yield 2
return "done" # 最終結果
def outer():
result = yield from sub()
# subのreturn値がここに入る
yield f"sub result = {result}"
print(list(outer()))
[1, 2, 'sub result = done']
next send closeの基本
nextで進める
next(gen)
は次のyield
まで実行を進めて値を受け取ります。
sendで値を送り込む
send(value)
はyield
式の評価値としてジェネレータに値を渡します。
最初にsend
する時はNone
を送る必要があります。
def accumulator():
total = 0
while True:
x = yield total # 直前の合計を返し、次の入力を受け取る
if x is None:
# Noneが来たらスキップ
continue
total += x
g = accumulator()
print(next(g)) # 初期化して0を受け取る
print(g.send(5)) # 0 + 5 = 5 を返す
print(g.send(3)) # 5 + 3 = 8 を返す
print(g.send(None)) # そのまま8
g.close() # 以降は使用できない
0
5
8
8
closeで明示終了
close()
は内部でGeneratorExit
を送出してジェネレータを終了させます。
リソース解放が必要なジェネレータではtry/finally
で後始末を書くと安全です。
def managed():
print("open")
try:
while True:
yield "working"
finally:
print("close")
g = managed()
print(next(g))
g.close()
open
working
close
まとめ
yield
は「値を返して一時停止できる」仕組みであり、return
のように一度で終わるものではありません。
遅延評価によりメモリ効率が良く、IO処理やデータパイプライン、無限列の生成などで特に有効です。
基本はfor
で自然に扱い、必要に応じてnext
やsend
、close
で細かく制御します。
まずは小さなジェネレータから書き始め、使いどころを体感してからyield from
による委譲や双方向通信といった応用に進むと、無理なく着実に身に付きます。