閉じる

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

Pythonのyieldは、関数の途中で値をひとつずつ外に渡しながら処理を一時停止できる強力な仕組みです。

returnとの違いを正しく理解すると、メモリ効率が高く読みやすいコードを書けるようになります。

本記事では基礎から実例、活用テクニックまで段階的に解説します。

yieldとジェネレータの基礎

yieldキーワードの使い方を理解する

ジェネレータ関数とは

関数の本体にyieldを含む関数を呼び出すと、関数はすぐに実行されるのではなく、まずジェネレータオブジェクトを返します。

これは「準備ができた反復可能オブジェクト」であり、forループやnext()で順次値を取り出せます。

Python
# 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ループが呼ばれると直後から再開されます。

Python
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ループはこれを捕捉して自然にループを終了します。

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

必要な分だけ作る

ジェネレータは値を「必要になった時にその場で」生成します。

巨大なリストを事前に作らないため、メモリ使用量を大きく削減できます。

Python
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つ返して関数の実行を一時停止し、次の要求時に続きから再開します。

以下に主な違いをまとめます。

観点returnyield
実行制御即時終了一時停止と再開
返すもの単一の値値の流れ(イテレータ)
メモリまとめて生成しがち遅延生成で省メモリ
使い所単発の計算結果逐次処理・ストリーミング
例外終端通常のreturn最終的にStopIterationで終端
再利用関数を再呼び出しジェネレータを再生成

単一の戻り値と逐次的な出力

リストを返す関数 vs ジェネレータ関数

同じ「1からnまでの二乗」を返す処理を2通りで比較します。

Python
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つのイテレータを共有して進めるのが正しい使い方です。

Python
it = iter(squares_gen(5))
print(next(it), next(it), next(it))
実行結果
0 1 4

forループでの振る舞いの違い

ループは自動でnext()を呼ぶ

forループは内部でnext()を繰り返し呼び、StopIterationが起きたらループを終えます。

通常は明示的にnext()を書く必要はありません。

Python
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を指定して有限回だけ数える

Python
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

偶数だけを通すフィルタ

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

Python
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行分だけメモリに載せるため、安定して処理できます。

ジェネレータ活用術

ジェネレータ式と関数の違い

簡潔さと再利用性のトレードオフ

ジェネレータ式は軽量で一時的な処理に向き、関数は名前やコメントを付けて再利用したい時に向きます。

Python
# ジェネレータ式: すぐ使うワンライナーに
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は別の反復可能オブジェクトからの値をそのまま委譲して取り出させます。

ネストを平らにする時にも便利です。

Python
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経由で受け継ぎます。

Python
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を送る必要があります。

Python
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で後始末を書くと安全です。

Python
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で自然に扱い、必要に応じてnextsendcloseで細かく制御します。

まずは小さなジェネレータから書き始め、使いどころを体感してからyield fromによる委譲や双方向通信といった応用に進むと、無理なく着実に身に付きます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

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

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

URLをコピーしました!