Pythonを学んでいると「ジェネレータ」や「yield」という言葉に出会うことがあります。大量のデータを効率よく処理したい、メモリを節約したい、無限数列を扱いたいなど、ジェネレータはPythonの中級以上のスキルとして実務で非常に役立つ機能のひとつです。
ジェネレータはyieldを使った関数で、値を一度にすべて生成してメモリに保持するのではなく、必要なときに1つずつ値を生成する「遅延評価」を実現します。リストに比べてメモリ使用量を大幅に削減でき、大容量データの処理や無限シーケンスの生成に非常に効果的です。
この記事では、Pythonのジェネレータの仕組みとyieldの使い方を、基本的な書き方・ジェネレータ式・yield from・実践的な応用パターンまで、サンプルコードとともにわかりやすく解説していきます。
Pythonのジェネレータとはyieldを使って値を1つずつ生成する関数のこと
それではまず、ジェネレータの基本的な仕組みとyieldの使い方について解説していきます。
通常の関数はreturnで結果を返してそこで処理が終わりますが、yieldを使ったジェネレータ関数は値を1つ返した後で実行を一時停止し、次にnext()が呼ばれると続きから再開します。
# ジェネレータ関数の基本
def simple_generator():
print("1つ目を生成")
yield "アボカド"
print("2つ目を生成")
yield "サーモン"
print("3つ目を生成")
yield "ドラゴンフルーツ"
print("終了")
# ジェネレータオブジェクトを作成
gen = simple_generator()
print(type(gen))
# next()で1つずつ値を取り出す
print(next(gen))
print(next(gen))
print(next(gen))
# 出力結果:
# 出力結果:1つ目を生成
# 出力結果:アボカド
# 出力結果:2つ目を生成
# 出力結果:サーモン
# 出力結果:3つ目を生成
# 出力結果:ドラゴンフルーツ
yieldを含む関数はジェネレータ関数となり、呼び出すとジェネレータオブジェクトが返ります。next()を呼ぶたびにyieldの箇所まで実行が進み、値を返して一時停止します。
forループでジェネレータを使う
# forループでジェネレータを使う(最も一般的なパターン)
def count_up(start, end):
current = start
while current <= end:
yield current
current += 1
# forループで1つずつ取り出す
for num in count_up(1, 5):
print(f"数値:{num}")
# 出力結果:数値:1
# 出力結果:数値:2
# 出力結果:数値:3
# 出力結果:数値:4
# 出力結果:数値:5
forループはジェネレータのStopIterationを自動で処理してループを終了させます。ジェネレータとforループの組み合わせが最も基本的な使い方です。
リストとジェネレータのメモリ使用量の違い
import sys
# リストで100万件のデータを生成
list_data = [i * 2 for i in range(1_000_000)]
list_size = sys.getsizeof(list_data)
print(f"リストのメモリ:{list_size:,}バイト(約{list_size // 1024 // 1024}MB)")
# ジェネレータで同じデータを生成
def gen_data(n):
for i in range(n):
yield i * 2
gen = gen_data(1_000_000)
gen_size = sys.getsizeof(gen)
print(f"ジェネレータのメモリ:{gen_size:,}バイト")
# 出力結果:リストのメモリ:8,448,728バイト(約8MB)
# 出力結果:ジェネレータのメモリ:200バイト
100万件のデータに対してリストは約8MB消費しますが、ジェネレータはわずか200バイト程度です。ジェネレータは値を都度生成するため、データ件数に関わらずほぼ一定のメモリ使用量を保ちます。
ジェネレータ式(ジェネレータ内包表記)の書き方
続いては、ジェネレータ式(ジェネレータ内包表記)の書き方と使い方を確認していきます。
リスト内包表記の角括弧[]を丸括弧()に変えるだけでジェネレータ式(ジェネレータオブジェクト)が生成できます。1回だけ使いたい処理にリストを作る必要がない場面で非常に有効です。
ジェネレータ式の基本
import sys
# リスト内包表記(全要素をメモリに保持)
list_comp = [n ** 2 for n in range(1_000_000)]
print(f"リスト内包表記:{sys.getsizeof(list_comp):,}バイト")
# ジェネレータ式(必要なときに都度生成)
gen_exp = (n ** 2 for n in range(1_000_000))
print(f"ジェネレータ式:{sys.getsizeof(gen_exp):,}バイト")
# ジェネレータ式はforループで使える
total = sum(n ** 2 for n in range(1, 6))
print(f"1〜5の2乗の合計:{total}")
# 出力結果:リスト内包表記:8,448,728バイト
# 出力結果:ジェネレータ式:200バイト
# 出力結果:1〜5の2乗の合計:55
sum()・any()・all()・max()・min()などの組み込み関数にはジェネレータ式を直接渡せます。リストを作らずに済むためメモリ効率が高くなります。
条件付きジェネレータ式
# 条件付きジェネレータ式
products = [
("アボカド", 298),
("キーボード", 8500),
("サーモン", 1200),
("ネジ", 50),
("マウス", 2000),
]
# 価格が500円以上の商品名を取得(ジェネレータ式)
expensive_gen = (name for name, price in products if price >= 500)
for name in expensive_gen:
print(f"高価格商品:{name}")
# 出力結果:高価格商品:キーボード
# 出力結果:高価格商品:サーモン
# 出力結果:高価格商品:マウス
ジェネレータ式も条件付きフィルタリングに対応しています。1回のイテレーションで使い捨てる処理にはリスト内包表記よりジェネレータ式の方がメモリ効率が高くなります。
リスト内包表記とジェネレータ式の使い分け
| 比較項目 | リスト内包表記 [] | ジェネレータ式 () |
|---|---|---|
| メモリ使用量 | 全要素分必要 | ほぼ一定(少ない) |
| 複数回イテレーション | 可能 | 1回のみ(使い捨て) |
| len()での件数取得 | 可能 | 不可 |
| インデックスアクセス | 可能 | 不可 |
| 向いている場面 | 複数回参照・件数把握 | 1回処理・大量データ |
yieldの応用的な使い方
続いては、yieldのより高度な使い方とyield fromを使った委譲パターンを確認していきます。
yieldはシンプルな値の返却だけでなく、無限シーケンスの生成・パイプライン処理・yield fromによるジェネレータの委譲など高度なパターンにも活用できます。
無限シーケンスを生成するジェネレータ
# 無限シーケンスを生成するジェネレータ
def infinite_counter(start=1, step=1):
current = start
while True: # 無限ループ(ジェネレータなのでメモリは増えない)
yield current
current += step
# 最初の5件だけ取得
counter = infinite_counter()
for _ in range(5):
print(next(counter), end=" ")
print()
# 2刻みで10件取得
counter2 = infinite_counter(step=2)
result = [next(counter2) for _ in range(10)]
print(result)
# 出力結果:1 2 3 4 5
# 出力結果:
ジェネレータの無限ループはメモリを圧迫しません。必要な数だけnext()で取り出すため、事前にサイズが決まっていない処理に柔軟に対応できます。
yield fromでジェネレータを委譲する
# yield fromで別のイテラブルに委譲する
def food_generator():
yield "アボカド"
yield "サーモン"
yield "ドラゴンフルーツ"
def parts_generator():
yield "ネジ"
yield "ボルト"
def all_items_generator():
yield from food_generator() # 食品ジェネレータに委譲
yield from parts_generator() # 部品ジェネレータに委譲
yield from ["キーボード", "マウス"] # リストにも委譲できる
for item in all_items_generator():
print(item, end=" ")
print()
# 出力結果:アボカド サーモン ドラゴンフルーツ ネジ ボルト キーボード マウス
yield fromは別のジェネレータやイテラブルの要素をそのまま委譲します。forループでyieldするよりも効率的で、複数のジェネレータを連結する場面で便利な構文です。
パイプライン処理をジェネレータで実装する
# ジェネレータを連結したパイプライン処理
def read_data():
"""データソース(生成)"""
data = [
("アボカド", 298, 50),
("サーモン", 1200, 5),
("ドラゴンフルーツ", 498, 30),
("ゴリラ茶", 350, 0),
]
for item in data:
yield item
def filter_in_stock(items):
"""在庫ありのみフィルタリング"""
for item in items:
if item
> 0:
yield item
def add_total(items):
"""合計金額を追加"""
for name, price, stock in items:
yield (name, price, stock, price * stock)
# パイプラインを組み立てる
pipeline = add_total(filter_in_stock(read_data()))
for name, price, stock, total in pipeline:
print(f"{name}:{price}円 × {stock}個 = {total:,}円")
# 出力結果:アボカド:298円 × 50個 = 14,900円
# 出力結果:サーモン:1200円 × 5個 = 6,000円
# 出力結果:ドラゴンフルーツ:498円 × 30個 = 14,940円
各処理をジェネレータ関数として定義して連結するパイプラインパターンは、データ処理の各ステップを独立して書けるため再利用性・保守性が高く、大容量データでもメモリ効率よく処理できます。
ジェネレータの実践的な応用パターン
続いては、ジェネレータを活用した実践的なプログラムのパターンを確認していきます。
ジェネレータは大容量ファイルの行処理・バッチ処理・フィボナッチ数列などの数学的シーケンスなど様々な実務シーンで活躍します。
大容量ファイルをジェネレータで処理する
# 大容量ファイルをジェネレータで1行ずつ処理する
def read_large_file(filepath):
"""ファイルを1行ずつyieldするジェネレータ"""
with open(filepath, "r", encoding="utf-8") as f:
for line in f:
yield line.strip()
# テスト用ファイルを作成
with open("/home/claude/large.txt", "w", encoding="utf-8") as f:
for i in range(10000):
f.write(f"商品_{i:05d},価格:{i * 10}円\n")
# ジェネレータで処理(メモリ効率が高い)
count = 0
total = 0
for line in read_large_file("/home/claude/large.txt"):
if not line:
continue
parts = line.split(",")
price = int(parts
.replace("価格:", "").replace("円", ""))
total += price
count += 1
print(f"処理件数:{count:,}件")
print(f"合計金額:{total:,}円")
# 出力結果:処理件数:10,000件
# 出力結果:合計金額:499,950,000円
ファイルを1行ずつyieldするジェネレータを使うと、100万行の大容量ファイルでもメモリをほとんど消費せずに処理できます。
チャンク処理をジェネレータで実装する
# リストをN件ずつのチャンクに分けるジェネレータ
def chunked(iterable, chunk_size):
chunk = []
for item in iterable:
chunk.append(item)
if len(chunk) >= chunk_size:
yield chunk
chunk = []
if chunk:
yield chunk
items = ["アボカド", "サーモン", "ドラゴンフルーツ",
"ゴリラ茶", "キーボード", "ネジ", "ボルト"]
for i, chunk in enumerate(chunked(items, 3), 1):
print(f"チャンク{i}:{chunk}")
# 出力結果:チャンク1:['アボカド', 'サーモン', 'ドラゴンフルーツ']
# 出力結果:チャンク2:['ゴリラ茶', 'キーボード', 'ネジ']
# 出力結果:チャンク3:['ボルト']
データをN件ずつのチャンクに分けるジェネレータはデータベースへの一括挿入・API呼び出しのバッチ化・大量データの分割処理など実務でよく使われるパターンです。
フィボナッチ数列を生成するジェネレータ
# フィボナッチ数列を無限に生成するジェネレータ
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 最初の15項を取得
fib = fibonacci()
fib_list = [next(fib) for _ in range(15)]
print(f"フィボナッチ数列:{fib_list}")
# 100以下のフィボナッチ数を取得
fib2 = fibonacci()
under_100 = list(num for num in fib2 if (num <= 100))
# ジェネレータは無限なのでtakewhileを使う
import itertools
fib3 = fibonacci()
under_100 = list(itertools.takewhile(lambda x: x <= 100, fib3))
print(f"100以下のフィボナッチ数:{under_100}")
# 出力結果:フィボナッチ数列:[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
# 出力結果:100以下のフィボナッチ数:[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
無限に続くフィボナッチ数列をジェネレータで表現するパターンです。itertools.takewhile()と組み合わせることで条件を満たす間だけ値を取り出せます。
まとめ
この記事では、Pythonのジェネレータとyieldの使い方について、基本的な仕組みからジェネレータ式・yield from・パイプライン処理・実践的な応用パターンまで幅広く解説しました。
ジェネレータはyieldを使って値を1つずつ生成する関数で、全データをメモリに保持するリストと比べて大幅にメモリ使用量を削減できます。リスト内包表記の[]を()に変えるだけでジェネレータ式が書け、sum()など組み込み関数にそのまま渡せます。yield fromで複数のジェネレータを連結したパイプライン処理は大量データの変換・フィルタリング処理に非常に効果的です。
大容量ファイルの処理・バッチ処理・無限シーケンスの生成など、今回紹介したパターンは実務のデータ処理でそのまま活用できます。ぜひ実際のプロジェクトで試してみてください。