当サイトは、アフィリエイト広告を利用しています

【Python】ジェネレータ式とリスト内包表記の使い分け|設計判断とアンチパターン

作成日:2024月12月02日
更新日:2026年03月06日

pPythonでリストから特定の値を取得する際、

  • リスト内包表記
  • ジェネレータ式

のどちらを使うか迷う場面がある。

実際のソースとしては構文上の違いは小さい。

違い
# リスト内包表記
[for x in iterable if 条件]
# ジェネレータ式
(for x in iterable if 条件)

が、構造が違うためメモリの使い方が違い、それゆえに
設計上の意味あいが大き変わる。

当記事では

  • ジェネレータ式とは?
  • 基本的な使い方
  • 使うべきケース
  • 使うべきでないケース
  • 逆に不利になるケース

を整理する

ジェネレータ式とは?

ジェネレータ式はpythonで効率的にデータを処理する方法の一つで
遅延評価を利用して要素を一つずつ生成する方法のこと。

具体的にはリストなどの反復可能オブジェクト(iterable)から
ジェネレータオブジェクトを生成する

ジェネレータ式の目的としては

  • コレクションから条件に一致する値を抽出

になる。
取り出した値は必要な時に遅延評価をして使う

ジェネレータオブジェクトとは?

ジェネレータオブジェクトはジェネレータ式を実行した結果生成される
特殊なオブジェクトのこと。

ジェネレータオブジェクトは遅延評価の仕組みを採用しており
値が必要になるまで計算はしない。
そのため、データを使用する場合はジェネレータオブジェクトから
値を取り出す必要がある。

ジェネレータオブジェクト
# 反復可能オブジェクト
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
# terget_userはジェネレータオブジェクト
terget_user = (user for user in users if user["user_id"] == "1")

terget_userhはジェネレータオブジェクトであるため
使うには値を取り出す必要がある
※このままでは何もできない

遅延評価とは?

ジェネレータオブジェクトの仕組みである遅延評価は
作成時に即座に値を計算するのではなく、必要な時に評価して使う仕組みのこと。
評価方法としては主に

  • next
  • list
  • for

がよく使われる
※詳しくは後述する

ジェネレータ式の利点

ジェネレータ式は上記の通り、遅延評価のジェネレータオブジェクトを生成するので
必要な時まで計算を行わない。つまり、評価するまではメモリを消費しない。

そのため

  • メモリ効率が良い
  • 大量データの処理に適してる

の利点がある。

ジェネレータ式の使い方

ジェネレータ式の具体的な使い方をまとめる

構文

ジェネレータ式の構文は

ジェネレータ式の構文
(for 変数 in iterable if 条件)
  • 式:作成するジェネレータオブジェクトに追加する要素
  • 変数:iterableからの要素を一時的に保持する変数
  • iterable:リスト、タプル、文字列、集合、辞書などの反復可能なオブジェクト
  • 条件:ジェネレータオブジェクトへの追加条件を指定。真の場合に追加される

のようになる
※()以外はリスト内包表記と同じ。

ジェネレータ式でジェネレータオブジェクトを生成する

ジェネレータオブジェクト生成
# ディクショナリリスト
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
# ジェネレータ式でジェネレータオブジェクト生成
user_names = (user["name"] for user in users)

ディクショナリリストから「name」だけのジェネレータオブジェクトを生成
この段階ではuser_namesはまだなにも計算していない。

遅延評価で値を取り出す

遅延評価で値を取り出す
# ディクショナリリスト
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
# ジェネレータ式でジェネレータオブジェクト生成
user_names = (user["name"] for user in users)
# forで遅延評価して値を取り出す
for name in user_names:
print(name) # ['Tujimura', 'mori', 'shimada', 'kyogoku']

for文を使って値を評価してprintで出力している。

next()を使う場合は下記のようになる。

next()を使う
# ディクショナリリスト
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
# ジェネレータ式でジェネレータオブジェクト生成
user_names = (user["name"] for user in users)
# nextで遅延評価して値を取り出す
print(next(user_names)) # Tujimura
print(next(user_names)) # mori
print(next(user_names)) # shimada
print(next(user_names)) # kyogoku
print(next(user_names)) # StopIteration

nextを使って順次、ジェネレータオブジェクトを遅延評価して取り出す。
値がなくなるとStopIterationとなる。

注意点

ジェネレータオブジェクトを注意点として

  • 一度使用したジェネレータオブジェクトは、再度繰り返して使うことはできない

というのがあるので注意。

注意点
# nextで遅延評価して値を取り出す
print(next(user_names)) # Tujimura
print(next(user_names)) # mori
print(next(user_names)) # shimada
print(next(user_names)) # kyogoku
print(next(user_names)) # StopIteration

でいうと、next()で前の値に戻ることはできないということ。
また一度使い切ったジェネレータは再利用できない。

ジェネレータを使うべきケース

使いどころとしてはリストなどの反復可能なオブジェクトなオブジェクトから
指定した条件のデータを取得する等のリスト内包表記と同じような使い方ができる

使い方としてリスト内包表記と同じなので、処理対象のデータが大量データであったり
またメモリを効率良く使う必要がある等の対象データで判断する。

代表的な使用パターン

  • 1件だけ取得する
  • 逐次処理
  • パイプライン的な処理

を実装して確認する

1件だけ取得(最も実務的)

ジェネレータ式を使ってリストの中からデータを1件抽出する場合

  • 条件に合う最初の1件のみ取得
  • 見つかった時点で停止したい
  • 全件評価しない

となり、これがジェネレータ式の代表的な使いどころ。

1件だけ取得なので、next()を使って値を取り出す

辞書リストから抽出(next)

next
# 辞書リスト
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "1", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
# ジェネレータ式で抽出(2件とれる)
terget_user = (user for user in users if user["user_id"] == "1")
# 遅延評価で最初の1件取得
terget_user_val = next(terget_user)
print(terget_user_val)
# 実行結果
# {'user_id': '1', 'name': 'Tujimura', 'age': 11}
  • next()で遅延評価して1件ずつ取り出す
  • 途中で打ち切る可能性がある場合はジェネレータ式は有効

逐次処理

リストの値を1件ずつ取り出し、逐次処理する場合は

  • 1件ずつ取り出して処理する
  • 中間リストを作らない

のでジェネレータ式を使うほうがメモリ効率がよくなる

逐次処理なので、for文を使って値を取り出す

辞書リストから抽出(for)

for
# 辞書リスト
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
# ジェネレータ式で1件を抽出
terget_user = (user for user in users if user["user_id"] == "1")
# 遅延評価(for)
for user in terget_user:
print(user)
# 実行結果
# {'user_id': '1', 'name': 'Tujimura', 'age': 11}
  • for文内では遅延評価が実行される

パイプライン的に処理する

リストに対してパイプライン的に処理する場合は

  • 中間リストを作らない
  • 必要なときに1件ずつ流れる

のでジェネレータ式を使うほうがメモリ効率がよくなる

辞書リストから抽出(パイプライン)

パイプライン
# 辞書リスト
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
# ① user_id が "1" のユーザーを抽出
filtered = (user for user in users if user["user_id"] == "1")
# ② name だけに変換
names = (user["name"] for user in filtered)
# ③ 文字列長が5以上のものだけ通す(例)
validated = (name for name in names if len(name) >= 5)
# ④ 最終出力
for name in validated:
print(name)
# 実行結果
# Tujimura

この設計のポイントとしては

  • 中間リストは一切作られない
  • 各段階は独立している
  • 必要なときに1件ずつ流れる
  • 最後の for が next() を内部で呼び出す

つまりこれは

  • データを「保持」しているのではなく、「流している」

構造になっている

関数化してしまう方法もある

関数化
def filter_user(users):
return (user for user in users if user["user_id"] == "1")
def extract_name(users):
return (user["name"] for user in users)
def validate_name(names):
return (name for name in names if len(name) >= 5)
pipeline = validate_name(extract_name(filter_user(users)))
for name in pipeline:
print(name)
  • よりストリーム処理っぽい

## ジェネレータ式をつかうべきではないケース 上記ではジェネレータ式を使った有効利用パターンを示してたが
逆に使うべきでないアンチパターンもまとめておく

代表的なアンチパターン

  • 最終的に全件保持する
  • 複数回使う
  • ランダムアクセスや長さ取得が必要

を実装して確認する

最終的に全件保持する(結局list化する)

ジェネレータ式でパイプラインを作っても、最終的にlist()で全件保持するなら
最初からリスト内包表記で良いケースが多い

最終的に全件保持する
# 辞書リスト
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
# パイプライン(遅延評価)
filtered = (user for user in users if user["age"] >= 20)
names = (user["name"] for user in filtered)
validated = (name for name in names if len(name) >= 5)
# ここで全件保持してしまう(遅延評価のメリットが消える)
result = list(validated)
print(result)
# 実行結果: ['shimada', 'kyogoku'] など

これを内包表記なら1発で書ける

最終的に全件保持する
result = [u["name"] for u in users if u["age"] >= 20 and len(u["name"]) >= 5]
print(result)
# 実行結果: ['shimada', 'kyogoku'] など

複数回使う

ジェネレータは 一度消費すると戻れない。
複数回使う前提なら、最初からリストで持つほうが自然である。

複数回使う
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
validated = (
user["name"]
for user in users
if user["age"] >= 20 and len(user["name"]) >= 5
)
# 1回目の走査(OK)
print(list(validated)) # ['shimada', 'kyogoku']
# 2回目の走査(空になる)
print(list(validated)) # []

複数回使うならlist内包表記で書くほうが自然である。

複数回使う
validated_list = [
user["name"]
for user in users
if user["age"] >= 20 and len(user["name"]) >= 5
]
print(validated_list) # 何度でも使える

ランダムアクセスや長さ取得が必要

ジェネレータは

  • len() が取れない
  • [] でのインデックスアクセスができない

用途として「先頭から順に流す」以外には向かない。

ランダムアクセスや長さ取得が必要
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
validated = (
user["name"]
for user in users
if user["age"] >= 20
)
# 長さが欲しい(できない)
# print(len(validated)) # TypeError
# 3番目が欲しい(できない)
# print(validated[2]) # TypeError

これがしたいなら、listで保持すべき

ランダムアクセスや長さ取得が必要
validated_list = [user["name"] for user in users if user["age"] >= 20]
print(len(validated_list)) # 3
print(validated_list[1]) # 'shimada' など

ジェネレータが逆に不利になるパターン

ジェネレータを使うと逆に不利になるパターンもまとめておく。
いわゆるジェネレータ式のデメリット。

① デバッグしづらい(中身が見えない / printすると消費される)

ジェネレータは「値を流す」構造なので、
途中で print(list(gen)) のように中身確認をすると その時点で消費される。

デバッグ
# 辞書リスト
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
validated = (
user["name"]
for user in users
if user["age"] >= 20
)
# デバッグのつもりで中身を確認(ここで全消費される)
print(list(validated)) # ['mori', 'shimada', 'kyogoku']
# その後に処理しようとしても、もう空
for name in validated:
print(name) # 何も出ない

対策としては

  • デバッグ目的なら一時的に list() で受ける(=遅延評価の利点は捨てる)

② 無意識に list 化している(結局メモリに展開している)

ジェネレータ式を使っていても、途中で list() を挟んだ瞬間に 全件メモリ展開が確定する。

無意識にlist化
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
# パイプライン(遅延評価)
gen = (user for user in users if user["age"] >= 20)
# ここで「確認のため」「使い回しのため」と list 化してしまう
tmp = list(gen) # ← この時点で全件保持(遅延評価のメリットが消える)
# 以降は結局 list を処理しているだけ
names = (user["name"] for user in tmp)
print(list(names)) # ['mori', 'shimada', 'kyogoku']
  • 「どこかで全件を保持していないか」が重要

③ 処理が複雑化しすぎる(可読性低下 / 例外箇所が追いにくい)

多段ジェネレータを「全部1行でつなぐ」と、
どこで何をしているか分かりにくくなり、例外も追いづらい。

悪い例:処理が潰れていて追いにくい

処理が潰れていて追いにくい
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70},
{"user_id": "5", "name": None, "age": 30}, # 例外の原因を混ぜる
]
# どこで壊れたのか分かりにくい
pipeline = (
name.strip().lower()
for name in (u["name"] for u in users if u["age"] >= 20)
if len(name) >= 5
)
for x in pipeline:
print(x)
# 実行時に AttributeError: 'NoneType' object has no attribute 'strip' 等

この例では nameNone があるとに壊れるが、
パイプラインが潰れているため原因箇所が見えにくい。

良い例:段階を分けて意図と例外点を見える化

見える化
def select_users(users):
return (u for u in users if u["age"] >= 20)
def extract_name(users):
return (u["name"] for u in users)
def normalize(names):
# ここが落ちうる(Noneなど)
for name in names:
yield name.strip().lower()
def filter_len(names):
return (name for name in names if len(name) >= 5)
u1 = select_users(users)
n1 = extract_name(u1)
n2 = normalize(n1)
n3 = filter_len(n2)
for x in n3:
print(x)
  • 段階を分けることで可読性が上がる
  • 「どの段階で例外が出たか」が追いやすい
  • 必要なら各段階で一時的に list() を挟んで調査もできる

判断基準(まとめ)

ジェネレータ式かリスト内包表記かどちらを使うべきか
判断基準は一言いうと

  • 全件を保持して再利用するならリスト、全件を保持せず流すならジェネレータ式

であるが、細かいチェックリストにすると下記のようになる

リスト内包表記が無難(=保持が必要 / 小さくて問題ない)

  • 結果を複数回使う(再走査する)
  • len() が必要
  • インデックスアクセス(x[0])が必要
  • デバッグで中身を頻繁に確認したい
  • 全件をメモリに展開しても問題ない(データが小さい / 余裕がある)
  • データが小さく、可読性を優先したい

ジェネレータ式が向いている(=全件展開したくない / 流したい)

  • 途中で打ち切りたい(最初の1件だけ欲しい等)
  • 逐次処理したい(1件ずつ処理して捨てる)
  • 中間リストを作りたくない(パイプライン処理)
  • 全件をメモリに展開したくない(メモリ制約がある / データが大きい)
  • データが大きい / 上限が読めない

迷ったらまずリストで書き、必要になった時にジェネレータへ置き換えるという選択もあり。

ジェネレータの利点を生かした有効活用のについては下記記事でも紹介している

関連記事

新着記事

目次
タグ一覧
top