閉じる

Pythonジェネレータの基本と使い方: 遅延評価で省メモリ処理

データを一度に全てメモリに載せず、必要になった時だけ順次取り出して処理したい場面は多くあります。

Pythonのジェネレータは、遅延評価により省メモリで効率的な反復処理を実現する仕組みです。

本記事では、ジェネレータの基本から実用的な活用法まで、初心者の方にも分かりやすく段階的に解説します。

ジェネレータとは?遅延評価で省メモリ

ジェネレータとイテレータの基本

ジェネレータは、Pythonの「イテレータ」を作るための仕組みです。

yieldを使った関数や、ジェネレータ式で作られます。

必要な値を1つずつ生成して返すため、全体をメモリに展開しないのが特徴です。

以下の用語の関係を整理します。

用語説明代表例
イテラブル(iterable)forで回せるオブジェクト。__iter__()を実装list, str, dict, range
イテレータ(iterator)次の要素を返す__next__()を持つiter(list)の結果、ジェネレータ
ジェネレータ(generator)yieldで値を順次返すイテレータジェネレータ関数、ジェネレータ式

簡単なジェネレータ関数の例です。

Python
# 0, 1, 4, 9, ... と平方数を必要な分だけ順次生成するジェネレータ
def squares(limit):
    # limit個ぶんの平方数を遅延生成
    for n in range(limit):
        # 値を1つ返して関数の状態を一時停止
        yield n * n

# 使い方例: forループで消費
for v in squares(5):
    print(v)
実行結果
0
1
4
9
16

遅延評価の仕組みとメリット

ジェネレータ関数を呼ぶと、すぐには実行されず「ジェネレータオブジェクト」を返します。

next()で初めて実行され、yieldに到達した時点で値を1つ返し停止します。

次のnext()で続きを再開する、という動きを繰り返します。

遅延評価の主なメリットは次の通りです。

全て文章で説明します。

まず、省メモリであるため巨大なデータも扱いやすく、メモリ不足を避けられます。

また、途中で処理を打ち切れるためムダな計算を省けます。

さらに処理のパイプライン化(段階的な加工)が容易で、ストリーミング処理に適します。

大量データ処理に向くケースと注意点

ジェネレータが特に力を発揮するのは、巨大ファイルの逐次処理、ネットワークやデータベースからのストリーム処理、無限列(際限ないシーケンス)の計算などです。

一方で、ジェネレータは一度消費すると再利用できず、ランダムアクセスやlen()ができません。

デバッグ時にうっかり消費してしまうと後続の処理が空振りする点にも注意が必要です。

yieldの使い方とジェネレータ関数の書き方

yieldとreturnの違い

returnは関数を即座に終了して「最終結果」を返すのに対し、yieldは「途中結果」を1つ返して関数の実行を一時停止します。

yieldを含む関数は「ジェネレータ関数」として扱われます。

キーワード役割関数の性質
returnその場で関数終了、値を1回だけ返す通常の関数
yield値を1つ返して停止、次の呼び出しで再開ジェネレータ関数

比較コードを示します。

Python
# return版: すべての結果を一括で返す(メモリ消費が大きい)
def build_squares_list(limit):
    # 全要素をリストに格納してから返す
    return [n * n for n in range(limit)]

# yield版: 必要になったときに1つずつ返す(省メモリ)
def gen_squares(limit):
    for n in range(limit):
        # 値を1つずつ遅延生成
        yield n * n

# 使い方比較
print("return版:", build_squares_list(5))  # すぐにリストができる
print("yield版:", list(gen_squares(5)))    # 消費するときに生成される
実行結果
return版: [0, 1, 4, 9, 16]
yield版: [0, 1, 4, 9, 16]

なお、ジェネレータ関数内でreturn 値を書くとStopIteration例外にその値が格納されて終了します。

通常は明示的に使わず、yieldで値を返す設計にします。

nextで取り出す反復の流れ

next()はジェネレータから次の値を1つ取り出す関数です。

yieldで停止・再開する流れを確認します。

Python
def count_up():
    print("start")       # 最初のnext()でここから実行
    yield 1              # 値1を返して停止
    print("resume A")    # 次のnext()でここから再開
    yield 2
    print("resume B")
    yield 3
    print("end")         # 次のnext()でStopIteration

g = count_up()
print(next(g))  # 最初の値: 1
print(next(g))  # 次の値: 2
print(next(g))  # 次の値: 3

# 次を取り出そうとすると要素が尽きてStopIteration
try:
    print(next(g))
except StopIteration:
    print("StopIteration発生")
実行結果
start
1
resume A
2
resume B
3
end
StopIteration発生

StopIterationとforループの関係

forループは内部でnext()を呼び、尽きたらStopIterationを捕まえて自動的にループを終了します。

つまり、通常はtry/exceptを書かずにforを使うだけで安全に消費できます。

Python
def gen123():
    yield 1
    yield 2
    yield 3

# forは内部でStopIterationを処理する
for x in gen123():
    print(x)
print("forループ終了")  # 例外処理は不要
実行結果
1
2
3
forループ終了

ジェネレータ式の書き方と使い分け

ジェネレータ式の基本構文

ジェネレータ式は、内包表記に似た書き方でイテレータを作る構文です。

丸括弧で囲みます。

Python
# 0〜4の平方を遅延生成するジェネレータ式
g = (x * x for x in range(5))

# 1つずつ取り出す
print(next(g))
print(next(g))

# 残りを合計(消費しながら計算)
print(sum(g))
実行結果
0
1
29

sum(g)の処理のタイミングでは1 * 1の結果の1は既に消費済みなため、2 * 24 * 4の合計がsum(g)で求められます。

そのため、1~5の平方の合計値から1引いた値が合計値29となります。

リスト内包表記との違いとメモリ比較

リスト内包表記は結果を即時にリスト化するため、要素数に比例してメモリを消費します。

一方、ジェネレータ式は要素を1つずつ計算して返すので一定のメモリで済みます。

sys.getsizeof()は概算であり、リスト要素分のメモリを完全には反映しませんが、相対的な違いを体感できます。

Python
import sys

N = 100_000

# リスト内包表記: すぐに10万要素のリストを作る
lst = [i * 2 for i in range(N)]
# ジェネレータ式: 必要時に1つずつ取り出す
gen = (i * 2 for i in range(N))

print("listの概算サイズ(bytes):", sys.getsizeof(lst))
print("generatorの概算サイズ(bytes):", sys.getsizeof(gen))

# 両者の結果は同じだが、作り方が異なる
print("listの最初の3要素:", lst[:3])

# generatorはスライスできないため、nextで取り出す
g_copy = (i * 2 for i in range(3))
print("generatorの最初の3要素:", [next(g_copy) for _ in range(3)])
実行結果
listの概算サイズ(bytes): 800984
generatorの概算サイズ(bytes): 200
listの最初の3要素: [0, 2, 4]
generatorの最初の3要素: [0, 2, 4

上の数値は環境で異なりますが、ジェネレータ式が極めて小さいメモリで表現されることが分かります。

フィルタやマップとの組み合わせ

ジェネレータ式はif節でフィルタしたり、入れ子にして段階的に加工するのが得意です。

mapfilterもイテレータを返すため、相性が良いです。

Python
# 1〜20のうち偶数だけ2乗し、そのうち100以上のものを合計
nums = range(1, 21)
evens = (n for n in nums if n % 2 == 0)              # フィルタ
squares = (n * n for n in evens)                     # マップ
large = (x for x in squares if x >= 100)             # さらにフィルタ
print(sum(large))  # 全体を展開せず合計

# map/filterを使う場合(どちらも遅延)
from math import sqrt
roots = map(sqrt, filter(lambda x: x % 3 == 0, range(1, 50)))
# 最初の5個だけ確認
import itertools
print(list(itertools.islice(roots, 5)))
実行結果
1420
[1.7320508075688772, 2.449489742783178, 3.0, 3.4641016151377544, 3.872983346207417]

実用例 大量データを省メモリで処理する

大きなファイルを行ごとに読み込む

巨大ファイルでも、1行ずつ遅延処理すればメモリ使用量は一定です。

ここではio.StringIOでファイル相当のオブジェクトを作り、動作を確認します。

実ファイルならopen("path")に置き換えてください。

Python
import io

def read_lines(stream):
    # 行末の改行を取り除きながら1行ずつ返す
    for line in stream:
        yield line.rstrip("\n")

# デモ用の「大きな」テキスト(実際は小さい)
data = "\n".join(f"line {i}" for i in range(1, 8))
fake_file = io.StringIO(data)

# 最初の3行だけ処理して途中で打ち切る例
for i, line in enumerate(read_lines(fake_file), start=1):
    print(f"{i}: {line}")
    if i == 3:
        break
実行結果
1: line 1
2: line 2
3: line 3

ジェネレータで処理をパイプライン化する

複数のジェネレータをつなげると、読み取り→パース→フィルタ→集計のような流れを省メモリで実現できます。

Python
import io

def read_lines(stream):
    # 入力を1行ずつ読み出す
    for line in stream:
        yield line.rstrip("\n")

def parse_csv(lines, sep=","):
    # "a,b,c" -> ["a","b","c"] のように分割
    for line in lines:
        yield [part.strip() for part in line.split(sep)]

def filter_status(rows, status="ERROR"):
    # 2列目(インデックス1)がERRORの行のみ通す
    for row in rows:
        if len(row) >= 2 and row[1] == status:
            yield row

def extract_message(rows):
    # 3列目(インデックス2)のメッセージだけ取り出す
    for row in rows:
        if len(row) >= 3:
            yield row[2]

# デモ用CSV風データ: "timestamp,level,message"
raw = "\n".join([
    "2024-01-01,INFO,Started",
    "2024-01-01,ERROR,Dropped connection",
    "2024-01-01,INFO,Heartbeat",
    "2024-01-01,ERROR,Timeout",
])
fake_csv = io.StringIO(raw)

# パイプライン: read -> parse -> filter(ERROR) -> extract message
messages = extract_message(filter_status(parse_csv(read_lines(fake_csv))))
for m in messages:
    print(m)
実行結果
Dropped connection
Timeout

すべてが遅延でつながっているため、ファイル全体を展開せずに必要な行だけ処理されます。

無限シーケンスと遅延計算の注意点

ジェネレータは無限列の生成にも使えますが、そのままlist()sum()に渡すと処理が終わりません。

itertools.islice()などで上限を決めて扱います。

Python
import itertools

def naturals(start=1):
    # 無限に自然数を生成
    n = start
    while True:
        yield n
        n += 1

# 最初の10個だけ取り出す
first_ten = list(itertools.islice(naturals(), 10))
print(first_ten)

# フィボナッチの無限列を例示
def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

print(list(itertools.islice(fib(), 8)))
実行結果
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[0, 1, 1, 2, 3, 5, 8, 13]

closeと例外処理で安全に終了する

長いパイプラインやリソースを持つ処理では、早期終了時に後始末が必要です。

ジェネレータはg.close()で終了させるとGeneratorExitが投げられ、finallyでクリーンアップが実行されます。

Python
def resourceful():
    print("open resource")
    try:
        for i in range(5):
            yield i
    finally:
        # g.close()やガベージコレクション時に呼ばれる
        print("cleanup resource")

g = resourceful()
print(next(g))   # 0を取得
print(next(g))   # 1を取得
g.close()        # ここで後始末が走る

# close後にnextするとStopIteration
try:
    print(next(g))
except StopIteration:
    print("closed")
実行結果
open resource
0
1
cleanup resource
closed

ファイルやソケットのような外部リソースは、原則としてwith open(...) as f:のようにコンテキストマネージャで管理してください。

ジェネレータ内でもwithを用いておけば、例外が発生しても確実にクローズされます。

Python
# コンテキストマネージャと併用する安全なパターン
def read_numbers(path):
    # ファイルを安全に開いて1行ずつ整数化して返す
    with open(path, "rt", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                # 整数に変換できない場合はスキップ
                try:
                    yield int(line)
                except ValueError:
                    # ログ出力などに切り替え可
                    continue

# デモ環境ではファイルを用意できないため、この関数の使い方だけ示します。
print("read_numbersは遅延で整数を返し、ファイルは自動クローズされます。")
実行結果
read_numbersは遅延で整数を返し、ファイルは自動クローズされます。

まとめ

ジェネレータは、yieldを用いて値を遅延生成するイテレータです。

メモリに全展開しないため、大量データやストリームの処理で特に有効です。

forループは内部でStopIterationを扱うので、通常はforで自然に消費できます。

ジェネレータ式は簡潔に書け、フィルタやマップを重ねてパイプライン化しやすいのが強みです。

一方で、一度消費すると再利用できない、ランダムアクセスができないといった制約があるため、使い所を見極めることが重要です。

無限列はisliceなどで上限を設け、リソースを扱う場合はwithclose()try/finallyで安全に終了させてください。

これらのポイントを押さえれば、ジェネレータはPythonの省メモリ・高効率なデータ処理を支える強力な道具になります。

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

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

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

URLをコピーしました!