Pythonを学び始めてしばらくすると、「リスト内包表記」や「ジェネレータ式」という言葉に出会います。
特にジェネレータ式は、コードの効率化やメモリ節約にとても役立つ重要な概念です。
しかし、初心者にとっては少し取っつきにくいのも事実です。「なんだか難しそう」「いつ使えばいいの?」といった疑問を持つ方も多いでしょう。
この記事では、ジェネレータ式とは何か、どんなときに使うのか、そしてリスト内包表記との違いについて、わかりやすく解説します。
ジェネレータ式って何?まずは基本を理解しよう

ジェネレータ式(Generator Expression)は、Pythonの強力な機能の一つです。
見た目はリスト内包表記と似ていますが、動作は全く違います。
ジェネレータ式の特徴
最大の特徴:値を一度にすべて作るのではなく、必要になったときに一つずつ生成します。
これを「遅延評価」と呼びます。
基本的な構文
(expression for item in iterable if condition)
具体例を見てみましょう
1から10までの偶数を作る場合:
# ジェネレータ式
even_numbers = (x for x in range(1, 11) if x % 2 == 0)
print(even_numbers) # <generator object at 0x...> と表示される
この式を実行しても、すぐに数値が作られるわけではありません。
代わりに「ジェネレータオブジェクト」という特別なオブジェクトが返されます。
値を取り出してみよう
ジェネレータから値を取り出すには、いくつかの方法があります:
方法1:forループで一つずつ取り出す
even_numbers = (x for x in range(1, 11) if x % 2 == 0)
for num in even_numbers:
print(num) # 2, 4, 6, 8, 10 が順番に表示される
方法2:list()でリストに変換する
even_numbers = (x for x in range(1, 11) if x % 2 == 0)
result = list(even_numbers)
print(result) # [2, 4, 6, 8, 10]
方法3:next()で一つずつ取り出す
even_numbers = (x for x in range(1, 11) if x % 2 == 0)
print(next(even_numbers)) # 2
print(next(even_numbers)) # 4
print(next(even_numbers)) # 6
よくある疑問:「なぜこんな仕組みなの?」
初心者の方は「なぜすぐに結果を返してくれないの?」と思うかもしれません。
その理由は、メモリの節約と処理効率の向上にあります。
次の章で、その理由を詳しく説明します。
なぜジェネレータ式を使うの?その驚くべきメリット
ジェネレータ式には、従来のリスト作成では得られない大きなメリットがあります。
メリット1:メモリの大幅節約
問題:大きなデータを扱うとき
たとえば、100万個の数値の合計を計算したいとします:
# リスト内包表記の場合(メモリを大量消費)
numbers_list = [x for x in range(1000000)]
total = sum(numbers_list)
print(total) # 499999500000
この方法では、100万個の数値をすべてメモリに保存します。メモリを大量に使用してしまいます。
解決:ジェネレータ式を使う
# ジェネレータ式の場合(メモリ節約)
numbers_gen = (x for x in range(1000000))
total = sum(numbers_gen)
print(total) # 499999500000
この方法では、数値を一つずつ生成して計算するため、メモリ使用量が大幅に削減されます。
メリット2:高速な初期化
リスト内包表記の場合
import time
start = time.time()
big_list = [x**2 for x in range(1000000)] # すべて計算してからリスト作成
end = time.time()
print(f"リスト作成時間: {end - start:.3f}秒")
ジェネレータ式の場合
import time
start = time.time()
big_gen = (x**2 for x in range(1000000)) # すぐに作成完了
end = time.time()
print(f"ジェネレータ作成時間: {end - start:.6f}秒") # ほぼ0秒
ジェネレータ式は、実際の計算を後回しにするため、作成が瞬時に完了します。
メリット3:無限のデータ列を扱える
ジェネレータ式では、理論上無限のデータ列も扱えます:
# フィボナッチ数列のジェネレータ
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 最初の10個だけ取り出す
fib_gen = fibonacci()
first_10 = [next(fib_gen) for _ in range(10)]
print(first_10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
メリット4:チェーンしやすい
複数の処理を連続して適用しやすくなります:
# 1から100までの数値から、3の倍数かつ奇数を抽出し、2乗する
result = (x**2 for x in range(1, 101) if x % 3 == 0 and x % 2 == 1)
print(list(result)) # [9, 81, 225, 441, 729, ...]
実際の業務でどう活用できるか、次の章で具体例を見てみましょう。
リスト内包表記との違いを徹底比較

ジェネレータ式とリスト内包表記は見た目が似ていますが、動作は大きく異なります。
構文の違い
リスト内包表記(角括弧を使用)
squares_list = [x**2 for x in range(5)]
print(squares_list) # [0, 1, 4, 9, 16]
print(type(squares_list)) # <class 'list'>
ジェネレータ式(丸括弧を使用)
squares_gen = (x**2 for x in range(5))
print(squares_gen) # <generator object at 0x...>
print(type(squares_gen)) # <class 'generator'>
括弧の種類を変えるだけで、動作が完全に変わります。
メモリ使用量の比較
実際にメモリ使用量を測定してみましょう:
import sys
# リスト内包表記
big_list = [x for x in range(100000)]
print(f"リストのサイズ: {sys.getsizeof(big_list):,} バイト")
# ジェネレータ式
big_gen = (x for x in range(100000))
print(f"ジェネレータのサイズ: {sys.getsizeof(big_gen)} バイト")
実行結果例
リストのサイズ: 800,984 バイト
ジェネレータのサイズ: 104 バイト
なんと、ジェネレータは約7,700分の1のメモリしか使用しません!
処理タイミングの違い
リスト内包表記(即座に実行)
def heavy_function(x):
print(f"Processing {x}")
return x * x
print("リスト内包表記の開始")
squares_list = [heavy_function(x) for x in range(3)]
print("リスト内包表記の完了")
print(squares_list)
実行結果
リスト内包表記の開始
Processing 0
Processing 1
Processing 2
リスト内包表記の完了
[0, 1, 4]
ジェネレータ式(必要時に実行)
print("ジェネレータ式の開始")
squares_gen = (heavy_function(x) for x in range(3))
print("ジェネレータ式の完了")
print("実際に値を取り出すとき:")
print(list(squares_gen))
実行結果
ジェネレータ式の開始
ジェネレータ式の完了
実際に値を取り出すとき:
Processing 0
Processing 1
Processing 2
[0, 1, 4]
ジェネレータ式では、実際に値が必要になるまで処理が実行されません。
使い分けの指針
リスト内包表記を使う場面
- 結果をすぐに全部見たい
- 何度も同じデータを使いたい
- データ量が少ない
- インデックスでアクセスしたい
# 成績データを何度も参照する場合
scores = [85, 92, 78, 96, 88]
high_scores = [score for score in scores if score >= 90]
print(f"優秀者数: {len(high_scores)}") # インデックスが使える
print(f"最高点: {max(high_scores)}") # 何度も使える
ジェネレータ式を使う場面
- データ量が大きい
- メモリを節約したい
- 一度だけ処理すればよい
- パイプライン処理したい
# 大きなファイルの処理
def process_large_file(filename):
with open(filename) as f:
# 一行ずつ処理してメモリ節約
return sum(len(line) for line in f)
ジェネレータ式の実践的な使い方
ここでは、実際の開発でよく使われるジェネレータ式の活用例を紹介します。
実用例1:ファイル処理
CSVファイルから特定条件のデータを抽出
# sales.csvから売上が10万円以上のデータのみ処理
def process_sales_data(filename):
with open(filename, 'r') as f:
# ヘッダーをスキップ
next(f)
# 売上が100000以上の行のみ抽出
high_sales = (line.strip().split(',') for line in f
if int(line.split(',')[2]) >= 100000)
# 顧客名と売上金額を表示
for customer, date, amount in high_sales:
print(f"{customer}: {amount}円")
# 使用例
# process_sales_data('sales.csv')
ログファイルの解析
def analyze_error_logs(filename):
with open(filename, 'r') as f:
# ERRORレベルのログのみ抽出
error_lines = (line for line in f if 'ERROR' in line)
# エラーの種類をカウント
error_types = {}
for line in error_lines:
# エラータイプを抽出(例:NullPointerException)
parts = line.split()
if len(parts) > 3:
error_type = parts[3]
error_types[error_type] = error_types.get(error_type, 0) + 1
return error_types
# 使用例
# errors = analyze_error_logs('app.log')
# print(errors)
実用例2:データベース風の処理
辞書のリストから条件に合うデータを抽出
# 社員データの例
employees = [
{'name': '田中', 'age': 28, 'department': '営業', 'salary': 350000},
{'name': '佐藤', 'age': 35, 'department': 'IT', 'salary': 450000},
{'name': '鈴木', 'age': 42, 'department': '営業', 'salary': 520000},
{'name': '高橋', 'age': 29, 'department': 'IT', 'salary': 380000},
]
# 30歳以上のIT部門の社員の給与合計
it_senior_salaries = (emp['salary'] for emp in employees
if emp['age'] >= 30 and emp['department'] == 'IT')
total_salary = sum(it_senior_salaries)
print(f"IT部門30歳以上の給与合計: {total_salary:,}円")
# 部門別平均年齢
departments = set(emp['department'] for emp in employees)
for dept in departments:
ages = (emp['age'] for emp in employees if emp['department'] == dept)
avg_age = sum(ages) / len([emp for emp in employees if emp['department'] == dept])
print(f"{dept}部門の平均年齢: {avg_age:.1f}歳")
実用例3:数学的計算
素数の生成と処理
def is_prime(n):
"""素数判定関数"""
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
# 1000以下の素数の合計
primes_sum = sum(n for n in range(2, 1001) if is_prime(n))
print(f"1000以下の素数の合計: {primes_sum}")
# 100番目の素数を見つける
def find_nth_prime(n):
count = 0
num = 2
while True:
if is_prime(num):
count += 1
if count == n:
return num
num += 1
print(f"100番目の素数: {find_nth_prime(100)}")
実用例4:Web scraping風の処理
URLリストから特定パターンのみ抽出
import re
urls = [
'https://example.com/api/users/123',
'https://example.com/blog/python-tips',
'https://example.com/api/posts/456',
'https://example.com/about',
'https://example.com/api/comments/789',
]
# APIエンドポイントのURLのみ抽出
api_urls = (url for url in urls if '/api/' in url)
print("APIエンドポイント:")
for url in api_urls:
print(f" {url}")
# URLからIDを抽出
def extract_ids(urls):
pattern = r'/(\d+)$'
return (re.search(pattern, url).group(1)
for url in urls if re.search(pattern, url))
ids = extract_ids(urls)
print(f"抽出されたID: {list(ids)}")
実用例5:設定ファイルの処理
設定ファイルから有効な設定のみ読み込み
def parse_config_file(filename):
"""設定ファイルを解析してアクティブな設定のみ返す"""
try:
with open(filename, 'r') as f:
# コメント行と空行を除外
valid_lines = (line.strip() for line in f
if line.strip() and not line.strip().startswith('#'))
# key=value形式の行のみ処理
configs = {}
for line in valid_lines:
if '=' in line:
key, value = line.split('=', 1)
configs[key.strip()] = value.strip()
return configs
except FileNotFoundError:
print(f"設定ファイル {filename} が見つかりません")
return {}
# 使用例(設定ファイルconfig.txtがある場合)
# config = parse_config_file('config.txt')
# print(config)
これらの例では、ジェネレータ式を使うことで:
- メモリ効率が良い
- コードが簡潔で読みやすい
- 大量データも安全に処理できる
といったメリットが得られます。
使うときの注意点と回避方法

ジェネレータ式は便利ですが、いくつか注意すべき点があります。
注意点1:一度使うと空になる
問題の例
numbers = (x for x in range(5))
# 最初の使用
print("1回目:", list(numbers)) # [0, 1, 2, 3, 4]
# 2回目の使用
print("2回目:", list(numbers)) # [] 空になる!
解決方法1:都度作り直す
def create_numbers():
return (x for x in range(5))
numbers1 = create_numbers()
numbers2 = create_numbers()
print("1回目:", list(numbers1)) # [0, 1, 2, 3, 4]
print("2回目:", list(numbers2)) # [0, 1, 2, 3, 4]
解決方法2:itertools.teeを使用
import itertools
original = (x for x in range(5))
gen1, gen2 = itertools.tee(original, 2)
print("1回目:", list(gen1)) # [0, 1, 2, 3, 4]
print("2回目:", list(gen2)) # [0, 1, 2, 3, 4]
注意点2:デバッグが難しい
問題の例
# 何が入っているかわからない
data = (x * 2 for x in range(10) if x % 3 == 0)
print(data) # <generator object at 0x...>
解決方法:必要に応じてリスト化して確認
data = (x * 2 for x in range(10) if x % 3 == 0)
# デバッグ時のみリスト化
if __debug__: # デバッグモードの時のみ実行
print("Debug:", list((x * 2 for x in range(10) if x % 3 == 0)))
# 本来の処理
result = sum(data)
print("結果:", result)
注意点3:エラーが遅延して発生する
問題の例
def risky_function(x):
if x == 5:
raise ValueError("5は処理できません")
return x * 2
# ジェネレータ作成時にはエラーが発生しない
data = (risky_function(x) for x in range(10))
print("ジェネレータ作成完了")
# エラーは値を取り出すときに発生
try:
result = list(data)
except ValueError as e:
print(f"エラー発生: {e}")
解決方法:適切なエラーハンドリング
def safe_generator(iterable, func):
"""安全なジェネレータのラッパー"""
for item in iterable:
try:
yield func(item)
except Exception as e:
print(f"警告: {item} の処理でエラー: {e}")
continue # エラーの項目をスキップして継続
# 使用例
data = safe_generator(range(10), risky_function)
result = list(data)
print("結果:", result)
注意点4:無限ループの危険性
問題の例
# 無限に続くジェネレータ
infinite_gen = (x for x in itertools.count())
# これを実行すると止まらない!
# all_numbers = list(infinite_gen) # 絶対に実行しないこと
解決方法:制限を設ける
import itertools
# 最初のN個だけ取得
def take(n, iterable):
return list(itertools.islice(iterable, n))
infinite_gen = (x for x in itertools.count())
first_10 = take(10, infinite_gen)
print("最初の10個:", first_10)
# または条件で止める
limited_gen = (x for x in itertools.count() if x < 10)
result = list(limited_gen)
print("10未満:", result)
注意点5:ネストした場合の複雑さ
問題の例
# 複雑すぎて読みにくい
complex_gen = (
y for x in range(10)
for y in (z**2 for z in range(x) if z % 2 == 0)
if y > 5
)
解決方法:段階的に分割する
# 読みやすく分割
def create_squares(n):
return (z**2 for z in range(n) if z % 2 == 0)
def process_data():
for x in range(10):
squares = create_squares(x)
for y in squares:
if y > 5:
yield y
readable_gen = process_data()
result = list(readable_gen)
print("結果:", result)
これらの注意点を理解して使えば、ジェネレータ式の恩恵を安全に受けられます。
応用テクニック:ittertoolsと組み合わせよう

Pythonのitertools
モジュールと組み合わせることで、ジェネレータ式がさらに強力になります。
よく使うitertoolsの関数
itertools.islice():指定範囲を切り出し
import itertools
# 大きなデータから一部だけ取得
big_gen = (x**2 for x in range(1000000))
first_10 = list(itertools.islice(big_gen, 10))
print("最初の10個:", first_10)
# 100番目から110番目まで取得
big_gen2 = (x**2 for x in range(1000000))
slice_100_110 = list(itertools.islice(big_gen2, 100, 110))
print("100-109番目:", slice_100_110)
itertools.chain():複数のジェネレータを結合
# 複数のデータソースを統合
gen1 = (x for x in range(5))
gen2 = (x for x in range(10, 15))
gen3 = (x for x in range(20, 25))
combined = itertools.chain(gen1, gen2, gen3)
result = list(combined)
print("結合結果:", result) # [0, 1, 2, 3, 4, 10, 11, 12, 13, 14, 20, 21, 22, 23, 24]
itertools.groupby():グループ化
# データをグループ化
data = [
{'name': '田中', 'dept': '営業'},
{'name': '佐藤', 'dept': '営業'},
{'name': '鈴木', 'dept': 'IT'},
{'name': '高橋', 'dept': 'IT'},
]
# 部門でグループ化(事前にソートが必要)
sorted_data = sorted(data, key=lambda x: x['dept'])
grouped = itertools.groupby(sorted_data, key=lambda x: x['dept'])
for dept, members in grouped:
member_names = (person['name'] for person in members)
print(f"{dept}部門: {list(member_names)}")
itertools.compress():条件に基づくフィルタリング
# ブール値のリストに基づいてフィルタリング
data = ['a', 'b', 'c', 'd', 'e']
selectors = [1, 0, 1, 0, 1] # True/Falseでも可
filtered = itertools.compress(data, selectors)
print("フィルタ結果:", list(filtered)) # ['a', 'c', 'e']
# ジェネレータ式と組み合わせ
numbers = range(10)
is_even = (x % 2 == 0 for x in numbers)
even_numbers = itertools.compress(numbers, is_even)
print("偶数:", list(even_numbers))
実践的な組み合わせ例
バッチ処理
def batch_process(iterable, batch_size):
"""データを指定サイズのバッチに分割して処理"""
iterator = iter(iterable)
while True:
batch = list(itertools.islice(iterator, batch_size))
if not batch:
break
yield batch
# 大量のデータを100件ずつ処理
big_data = (x for x in range(1000))
for batch in batch_process(big_data, 100):
print(f"バッチサイズ: {len(batch)}, 最初: {batch[0]}, 最後: {batch[-1]}")
データ変換パイプライン
def data_pipeline(filename):
"""CSV処理のパイプライン例"""
with open(filename, 'r') as f:
# 1. 空行とコメント行を除去
valid_lines = (line.strip() for line in f
if line.strip() and not line.startswith('#'))
# 2. CSV解析
parsed_data = (line.split(',') for line in valid_lines)
# 3. 数値変換
converted_data = ([row[0], int(row[1]), float(row[2])]
for row in parsed_data if len(row) >= 3)
# 4. フィルタリング
filtered_data = (row for row in converted_data if row[2] > 0)
return filtered_data
# 使用例(ファイルが存在する場合)
# for name, count, value in data_pipeline('data.csv'):
# print(f"{name}: {count}個, 値: {value}")
まとめ
ここまでジェネレータ式について詳しく解説してきました。
最後に重要なポイントをまとめます。
ジェネレータ式の核心的メリット
メモリ効率
- 大量のデータでもメモリ使用量を抑制
- 無限データ列も安全に扱える
処理効率
- 必要な分だけ計算するため無駄がない
- 初期化が高速
コードの簡潔性
- 複雑な処理をシンプルに表現
- 関数型プログラミングの恩恵
いつ使うべきか?判断基準
ジェネレータ式を選ぶべき場面
- データサイズが大きい(1万件以上が目安)
- 一度だけ処理すればよい
- メモリ使用量を抑えたい
- ファイル処理やネットワーク処理
- パイプライン的な処理
リスト内包表記を選ぶべき場面
- データサイズが小さい(数百件程度)
- 何度も参照する
- インデックスアクセスが必要
- 即座に全結果が必要
よくある質問と回答
Q: いつもジェネレータ式を使えばいいの? A: いいえ。小さなデータや何度も使用するデータにはリスト内包表記の方が適しています。適材適所で使い分けましょう。
Q: ジェネレータ式はどのくらい速いの? A: メモリ使用量は大幅に削減されますが、計算速度自体は同程度です。初期化が速く、大量データでメモリ不足を回避できる点がメリットです。
Q: 他の人が読みやすいコードになる? A: 適切に使えば、むしろ読みやすくなります。ただし、複雑なネストは避けて、段階的に分割することが大切です。
コメント