閉じる

returnとの違いは?Pythonのyieldとジェネレータ活用術

Pythonのyieldは、長い処理や巨大なデータを扱うときに力を発揮する仕組みです。

関数を途中で一時停止し、必要なタイミングで少しずつ値を返すことで、メモリを節約しながら読みやすいコードを書けます。

本稿ではreturnとの違いからジェネレータの実践的な使い分けまで、初心者でも段階的に理解できるよう丁寧に解説します。

yieldとは?ジェネレータの基本

yieldの使い方と基本構文

yieldは、関数の実行を一時停止して値を1つ返し、次回の再開時に続きから処理を行うキーワードです。

関数内にyieldが1つでもあると、その関数はジェネレータ関数になり、呼び出し時にジェネレータ(イテレータ)を返します。

Python
# 基本のジェネレータ関数: 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したあとに到達
Python
# 実行例
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が送出されます。

Python
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を使います。

Python
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()を使うのが読みやすい書き方です。

returnとの違い

yieldは一時停止、returnは終了

returnは関数を即座に終了し値を1つ返すのに対し、yieldは関数を終了せずに値を逐次返す点が最大の違いです。

観点yieldreturn
動作実行を一時停止し値を返す即座に関数を終了し値を返す
複数回の値可能(逐次)不可(1回のみ)
状態保持可能(局所変数や実行位置を保持)不要
例外終端値が尽きるとStopIterationただちに戻る
使いどころストリーミング、巨大データ、パイプライン最終結果を一度に返す

ジェネレータ内のreturnStopIterationを送出し、イテレーションを終了します

状態保持と再開の仕組み

ジェネレータはフレームのスナップショット(ローカル変数や実行位置)を保持して再開します。

Python
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

再開しても前回の局所変数が保たれるため、外部状態を持たずに連番や逐次計算を自然に実装できます

メモリ効率と遅延評価のメリット

必要になったときにだけ計算する遅延評価により、巨大データでもメモリを節約できます。

Python
# 大量の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する

Python
# フィボナッチ数列を必要な分だけ取り出す
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)

Python
# 偶数だけを返し、しきい値に達したら終了
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)の形で、内包表記のジェネレータ版が書けます。

Python
# 二乗の合計(リストを作らずに合計)
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値を受け取れます。

Python
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()で終了を依頼できます。

Python
# 受信した数値の移動平均を返し続けるジェネレータ
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)でも可)。

非同期との違い(async/awaitの注意)

yieldは並行実行を提供しません

非同期処理はasync/await別の機能です。

  • async def + yield非同期ジェネレータになり、async forで反復します。
  • 通常のジェネレータ内でawaitは使えません。
Python
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値の関係

ジェネレータ内のreturnStopIteration(value)を送出します。

yield from経由ならこのvalueを受け取れます。

Python
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で値を返したい用途とは両立しません。

Python
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

関数の戻り契約は一貫させるため、用途ごとに関数を分けるのが安全です。

使い捨てのイテレータに注意(再利用不可)

ジェネレータは一度消費すると空になります。

Python
g = (i for i in range(3))
print(list(g))  # すべて消費
print(list(g))  # 2回目は空
実行結果
[0, 1, 2]
[]

再利用したいなら新しいジェネレータを都度作るか、必要ならリスト化して保持します。

list()で中身を確認するデバッグ

中身を確認したいだけで全消費してしまうのは危険です。

itertools.isliceで一部だけを見ると安全です。

Python
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する型Ysend()で受け取る型Sreturnで返す型Rです。

単に反復するだけならIterator[T]でも十分です。

Python
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で意図を明示してください。

最終的には、「一括で欲しいか、少しずつ欲しいか」returnyieldを使い分けるのがコツです。

Python 実践TIPS - コーディング効率化・Pythonic
この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!