Pythonのyieldは、長い処理や巨大なデータを扱うときに力を発揮する仕組みです。
関数を途中で一時停止し、必要なタイミングで少しずつ値を返すことで、メモリを節約しながら読みやすいコードを書けます。
本稿ではreturnとの違いからジェネレータの実践的な使い分けまで、初心者でも段階的に理解できるよう丁寧に解説します。
yieldとは?ジェネレータの基本
yieldの使い方と基本構文
yieldは、関数の実行を一時停止して値を1つ返し、次回の再開時に続きから処理を行うキーワードです。
関数内にyield
が1つでもあると、その関数はジェネレータ関数になり、呼び出し時にジェネレータ(イテレータ)を返します。
# 基本のジェネレータ関数: 0,1,2,...,n-1 を順番に返す
def count_up(n):
print("start") # 最初のnext()またはforでここまで進む
i = 0
while i < n:
print(f"yield: {i}") # いつ値が出るかを可視化
yield i # ここで一時停止して値を返す
i += 1
print("done") # すべてyieldしたあとに到達
# 実行例
gen = count_up(3)
print(next(gen)) # 最初の値(0)
print(next(gen)) # 次の値(1)
print(next(gen)) # 次の値(2)
try:
print(next(gen)) # これ以上ないのでStopIteration
except StopIteration:
print("StopIterationが発生")
start
yield: 0
0
yield: 1
1
yield: 2
2
done
StopIterationが発生
ここで重要なのは、関数呼び出し時には本体は実行されず、最初のnext()
またはfor
で初めて動き出すことです。
ジェネレータ関数とイテレータの仕組み
ジェネレータが返すオブジェクトは__iter__
と__next__
を実装するイテレータです。
値が尽きるとStopIteration
が送出されます。
def ones(k):
# 1をk回だけ返すイテレータ
for _ in range(k):
yield 1
gen = ones(2)
print(hasattr(gen, "__iter__"), hasattr(gen, "__next__")) # イテレータか確認
print(iter(gen) is gen) # ジェネレータは自分自身がイテレータ
print(next(gen)) # 1回目
print(next(gen)) # 2回目
try:
print(next(gen)) # 3回目は尽きる
except StopIteration as e:
print("尽きました")
True True
True
1
1
尽きました
イテレータは一方向にしか進めず、消費した要素は戻せない点を意識すると混乱が減ります。
forループとnext()での取り出し
for
は内部でnext()
を繰り返し呼び、StopIteration
を検知すると自動でループを終了します。
手動で扱う場合はnext()
とtry/except
を使います。
def squares(n):
for i in range(n):
yield i * i
# forループで取り出す
for v in squares(3):
print(v)
# next()で手動取り出し
g = squares(2)
try:
while True:
print(next(g))
except StopIteration:
print("読み切り")
0
1
4
0
1
読み切り
普段はforを使い、制御したいときのみnext()
を使うのが読みやすい書き方です。
- 関連記事:はじめてのfor文: リスト処理の書き方とコツ
- 関連記事:0から9まで回すには?range()の基本と実例10選
- 関連記事:forループでenumerateでインデックスと要素を同時取得する方法
- next()/iter() — Python 3.13.7 ドキュメント
returnとの違い
yieldは一時停止、returnは終了
returnは関数を即座に終了し値を1つ返すのに対し、yieldは関数を終了せずに値を逐次返す点が最大の違いです。
観点 | yield | return |
---|---|---|
動作 | 実行を一時停止し値を返す | 即座に関数を終了し値を返す |
複数回の値 | 可能(逐次) | 不可(1回のみ) |
状態保持 | 可能(局所変数や実行位置を保持) | 不要 |
例外終端 | 値が尽きるとStopIteration | ただちに戻る |
使いどころ | ストリーミング、巨大データ、パイプライン | 最終結果を一度に返す |
ジェネレータ内のreturn
はStopIteration
を送出し、イテレーションを終了します。
状態保持と再開の仕組み
ジェネレータはフレームのスナップショット(ローカル変数や実行位置)を保持して再開します。
def counter():
n = 0
while True:
yield n # ここで一時停止。nの状態は保持される
n += 1
g = counter()
print(next(g)) # 0
print(next(g)) # 1
print(next(g)) # 2
0
1
2
再開しても前回の局所変数が保たれるため、外部状態を持たずに連番や逐次計算を自然に実装できます。
メモリ効率と遅延評価のメリット
必要になったときにだけ計算する遅延評価により、巨大データでもメモリを節約できます。
# 大量のIDを順次処理する例(メモリ効率重視)
def generate_ids(limit):
for i in range(limit):
yield f"id-{i}"
count = 0
for _ in generate_ids(10_000_000): # 1千万件でも一括で保持しない
count += 1
print(count)
10000000
全件をリストに貯めると、そのサイズ分のメモリが必要になりますが、ジェネレータなら一定量のメモリで処理が前進します。
使い分けの判断基準
- 結果を一度に必要とする(合計値、最終結果だけでよい)なら
return
。 - 大量データやストリームを順次処理するなら
yield
。 - パイプライン処理やフィルタ、変換の中間段階には
yield
が適合。「サイズ不明・巨大・逐次処理」ならyield、「小さく確定・一度だけ」ならreturnが目安です。
ジェネレータの書き方と活用
基本例: 数列をyieldする
# フィボナッチ数列を必要な分だけ取り出す
def fib(limit):
a, b = 0, 1
for _ in range(limit):
yield a
a, b = b, a + b
print(list(fib(7)))
[0, 1, 1, 2, 3, 5, 8]
反復の内部状態(a,b)を保持し続けられるのがジェネレータの強みです。
条件付きyieldと早期終了(return)
# 偶数だけを返し、しきい値に達したら終了
def even_until(n, stop_at):
for i in range(n):
if i % 2 == 0:
yield i
if i >= stop_at:
return # ここでStopIterationとなり、forは終了する
print(list(even_until(10, stop_at=5)))
[0, 2, 4]
ジェネレータ内のreturn
は値を返さずに列を打ち切る動作になります(返す値は後述のStopIteration.value
へ格納)。
ジェネレータ式(generator expression)
(expr for x in iterable)
の形で、内包表記のジェネレータ版が書けます。
# 二乗の合計(リストを作らずに合計)
total = sum(x * x for x in range(1, 6))
print(total)
# 一度消費すると空になることに注意
g = (x * 2 for x in range(3))
print(list(g))
print(list(g)) # 2回目は空
55
[0, 2, 4]
[]
ジェネレータ式は「一度きり」なので再利用したいときは関数に切り出すのが安全です。
yield fromでジェネレータを合成
yield from
は、別のイテラブルを委譲してまとめてyieldします。
さらに、yield from
は委譲先ジェネレータのreturn
値を受け取れます。
def subgen():
yield 1
yield 2
return "done-sub" # StopIteration.valueに入る
def outer():
yield 0
result = yield from subgen() # 1,2をそのまま外へ流し、return値を受け取る
yield 3
yield f"sub-result={result}"
print(list(outer()))
[0, 1, 2, 3, 'sub-result=done-sub']
分割した処理を自然に合成でき、委譲先の終了値も受け渡せるのがyield from
の利点です。
send/throw/closeで制御する
ジェネレータはsend()
で値を送り込み、throw()
で内部に例外を投げ、close()
で終了を依頼できます。
# 受信した数値の移動平均を返し続けるジェネレータ
def moving_average():
total = 0.0
count = 0
avg = None
try:
while True:
x = yield avg # 呼び出し側から送られる値を受け取る
total += x
count += 1
avg = total / count
except GeneratorExit:
# close()が呼ばれたときに後始末できる
print("cleanup")
raise
g = moving_average()
print(next(g)) # 最初のyieldまで進める(Noneが返る)
print(g.send(10)) # 10を送り、平均10.0
print(g.send(20)) # 平均15.0
print(g.send(0)) # 平均10.0
g.close() # 終了を依頼
None
10.0
15.0
10.0
cleanup
最初のsend()
の前にはnext()
でジェネレータを起動する必要があります(最初のsend(None)
でも可)。
- 関連記事:例外(Exception)入門: 実行時エラーの正しい理解
- GeneratorExit — Python 3.13.7 ドキュメント
- ジェネレータ/sendの仕様 — Python 3.13.7 ドキュメント
非同期との違い(async/awaitの注意)
yieldは並行実行を提供しません。
非同期処理はasync/await
と別の機能です。
async def
+yield
は非同期ジェネレータになり、async for
で反復します。- 通常のジェネレータ内で
await
は使えません。
import asyncio
async def agen():
for i in range(3):
yield i # 非同期ジェネレータ
await asyncio.sleep(0.01)
async def main():
out = []
async for v in agen():
out.append(v)
print(out)
asyncio.run(main())
[0, 1, 2]
「待ち」を扱うならasync、「逐次生成」ならyieldという棲み分けを意識します。
よくある落とし穴とテスト
StopIterationとreturn値の関係
ジェネレータ内のreturn
はStopIteration(value)
を送出します。
yield from
経由ならこのvalue
を受け取れます。
def sub():
yield 1
return 99 # StopIteration.value に入る
def outer():
val = yield from sub()
yield f"got={val}"
print(list(outer()))
[1, 'got=99']
直接next()
で受け取るには例外のvalue
を明示的に参照する必要がありますが、yield from
なら自然に値を受け取れます。
関数内にyieldが1つでもあればジェネレータ
関数内にyield
があると、常に「ジェネレータ関数」扱いになります。
普通のreturn
で値を返したい用途とは両立しません。
def maybe_list(flag):
if flag:
yield 1 # ← これがあるだけでジェネレータ関数になる
else:
return [1] # 呼び出し側はリストだと思って受けると破綻する
print(maybe_list(True)) # <generator object ...>
print(maybe_list(False)) # None (returnで終了、値は返らない)
<generator object maybe_list at ...>
None
関数の戻り契約は一貫させるため、用途ごとに関数を分けるのが安全です。
使い捨てのイテレータに注意(再利用不可)
ジェネレータは一度消費すると空になります。
g = (i for i in range(3))
print(list(g)) # すべて消費
print(list(g)) # 2回目は空
[0, 1, 2]
[]
再利用したいなら新しいジェネレータを都度作るか、必要ならリスト化して保持します。
list()で中身を確認するデバッグ
中身を確認したいだけで全消費してしまうのは危険です。
itertools.islice
で一部だけを見ると安全です。
from itertools import islice
def numbers():
for i in range(100):
yield i
g = numbers()
print(list(islice(g, 5))) # 先頭5件だけ確認
print(next(g)) # 続きから6番目を取得
[0, 1, 2, 3, 4]
5
デバッグでも「全部取り尽くす」操作は慎重に扱いましょう。
型ヒントで明示する(typing.Generator)
型ヒントで返り値の性質を明示できます。
Generator[Y, S, R]
はyield
する型Y
、send()
で受け取る型S
、return
で返す型R
です。
単に反復するだけならIterator[T]
でも十分です。
from typing import Generator, Iterator
def gen_ints(n: int) -> Iterator[int]:
for i in range(n):
yield i
def avg() -> Generator[float, float, None]:
# yield: float(平均値)、send: float(新しい値)、return: None
total = 0.0
count = 0
avg_value = 0.0
while True:
x = yield avg_value
total += x
count += 1
avg_value = total / count
型ヒントはジェネレータの使い方(送受信や終了値)をドキュメント化し、レビューや静的解析を助けます。
まとめ
yieldは「途中で止まりつつ値を流す」ための仕組みで、状態保持と遅延評価によりメモリ効率と可読性を両立します。
returnは即時終了、yieldは一時停止という根本の違いを理解し、巨大データの逐次処理、パイプラインの構築、yield from
による合成、send/throw/close
による制御まで押さえると実務でも強力に活用できます。
落とし穴としては再利用不可・StopIteration・関数契約の一貫性に注意し、必要に応じてtyping.Generator
で意図を明示してください。
最終的には、「一括で欲しいか、少しずつ欲しいか」でreturn
とyield
を使い分けるのがコツです。