コードを簡潔に保ちながら意図を正確に伝えるのはPythonicな書き方の要です。
本記事では、辞書とセットを1行で組み立てられる内包表記の基本から実践パターンまでを、初心者でも段階的に理解できるように詳しく解説します。
- 関連記事:辞書(dict)とは?キーと値で管理する基本と使い方
- 関連記事:リストの重複を削除するsetの使い方
読みやすさと性能の両立を目指し、注意点や落とし穴も丁寧に説明します。
Pythonの辞書内包表記/セット内包表記の基本
内包表記とは
内包表記は、反復処理(イテレーション)と条件分岐を組み合わせて、新しいコレクションを1行で生成する構文です。
リスト内包表記が有名ですが、辞書(dict
)とセット(set
)にも対応しています。
「何を生成したいか」を式で宣言的に記述できるため、読みやすく保ちながらコード量を削減できます。
辞書内包表記/セット内包表記の基本構文
辞書とセットでは中身の式が少し異なります。
辞書はキーと値のペア、セットは要素のみを生成します。
- 辞書内包表記:
{key_expr: value_expr for 変数 in 反復可能オブジェクト if 条件}
- セット内包表記:
{elem_expr for 変数 in 反復可能オブジェクト if 条件}
# 辞書: 数値nに対して n: n*n のマップを作る
squares = {n: n * n for n in range(5)}
print(squares)
# セット: 文字列の長さの集合(重複は自動で排除)
lengths = {len(s) for s in ["a", "bb", "ccc", "bb", "a"]}
print(lengths)
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
{1, 2, 3}
ここで生成される辞書はキーに0〜4、値にその2乗が入ります。
セットは重複を持てないため長さの集合はユニークな値のみになります。
いつ使うか(メリットと注意点)
内包表記の主な利点は次のとおりです。
説明的で短く、しばしば高速です。
式の形で「生成」そのものを表現できるため、目的指向で読みやすいコードになりやすいです。
一方で、式が複雑になりすぎると読みづらくなります。
ネストや条件が増えるなら通常のforループに戻す判断が重要です。
また、辞書・セットはメモリに全体を保持するため、非常に大きなデータでは構築コストを意識します。
内包表記とforループの違いを簡単にまとめます。
観点 | 内包表記 | 通常のforループ |
---|---|---|
可読性 | 単純な生成に強い(短く意図が明確) | 複雑な処理に強い(段階的に書ける) |
速度 | しばしば速い(関数呼び出しが少ない) | わずかに遅いことが多い |
保守性 | ネストが深いと読みにくい | ロジックを分割しやすい |
デバッグ | 例外箇所が読み取りにくい | 行ごとに追いやすい |
- 関連記事:PEP8の書き方まとめ Pythonの命名/インデント/空白のルール
- 関連記事:ジェネレータの基本と使い方: 遅延評価で省メモリ処理
- 関連記事:in演算子は遅い? リストとセット/辞書の計算量比較
forループからの置き換え
同じ処理をforループと内包表記で書き比べます。
# forループ版: 偶数だけを2乗にして辞書化
result = {}
for n in range(10):
if n % 2 == 0:
result[n] = n * n
print(result)
# 内包表記版
result2 = {n: n * n for n in range(10) if n % 2 == 0}
print(result2)
{0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
{0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
同じ結果でも、内包表記の方が意図(偶数の2乗マップを作る)が短く明示されています。
辞書内包表記(dict comprehension)の使い方
キー/値を生成する
キーと値を自由に組み立てられます。
列挙番号をキーに、文字列長を値にする例です。
names = ["Alice", "Bob", "Charlie"]
# enumerateで(インデックス, 要素)を取り出し、キーと値を作る
name_lengths = {i: len(name) for i, name in enumerate(names, start=1)}
print(name_lengths)
{1: 5, 2: 3, 3: 7}
既存dictを選択・変換(items)
既存の辞書に対して.items()
で(キー, 値)を反復し、変換した新しい辞書を作ります。
prices = {"apple": "100", "banana": "80", "orange": "120"}
# 文字列の数字をintに変換し、税抜→税込(10%)へ
with_tax = {item: int(price) * 1.10 for item, price in prices.items()}
print(with_tax)
{'apple': 110.00000000000001, 'banana': 88.0, 'orange': 132.0}
浮動小数点誤差が気になる場合はDecimal
や整数(円単位で扱うなど)を検討します。
from decimal import Decimal, ROUND_HALF_UP
with_tax_decimal = {
item: (Decimal(price) * Decimal("1.10")).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
for item, price in prices.items()
}
print(with_tax_decimal)
{'apple': Decimal('110'), 'banana': Decimal('88'), 'orange': Decimal('132')}
条件でフィルタする(if)
末尾にif
を置くと、条件を満たすペアのみを取り込みます。
scores = {"Alice": 82, "Bob": 59, "Charlie": 91, "Dana": 74}
# 合格(>= 70)だけを抽出
passed = {name: s for name, s in scores.items() if s >= 70}
print(passed)
{'Alice': 82, 'Charlie': 91, 'Dana': 74}
if-elseで値を分岐
値の式の中で条件演算子を使うと、キーはそのまま、値だけを切り替えられます。
# 合否を値にする
result = {name: ("PASS" if s >= 70 else "FAIL") for name, s in scores.items()}
print(result)
{'Alice': 'PASS', 'Bob': 'FAIL', 'Charlie': 'PASS', 'Dana': 'PASS'}
{k: expr for ... if cond}
は「フィルタ」です。
一方、{k: (a if cond else b) for ...}
は「分岐」。
用途が異なります。
キー衝突は後勝ち
辞書はキーが重複すると後から代入した値で上書きされます。
内包表記で同じキーが生成される場合も同様です。
最終的な値は最後に評価されたものになります。
# 0,1,2 のキーに対して繰り返し代入が起こる例
d = {n % 3: n for n in range(6)} # n = 0..5
print(d) # 値は「後勝ち」
print(list(d.items())) # 挿入順は最初の出現位置のまま
{0: 3, 1: 4, 2: 5}
[(0, 3), (1, 4), (2, 5)]
ここで順序は最初の挿入位置を保ちつつ、値だけが上書きされています。
キーの衝突が想定される場合は、上書きでよいのか、集合やリストに集約したいのかを事前に設計しましょう。
セット内包表記(set comprehension)の使い方
基本構文と重複排除
セットは要素の重複を自動で排除します。
ユニーク値を素早く取り出す用途に向いています。
words = ["Apple", "banana", "APPLE", "Banana", "orange"]
# 小文字化してユニーク化
unique_lower = {w.lower() for w in words}
print(unique_lower)
{'banana', 'apple', 'orange'}
条件で抽出・正規化
フィルタと前処理を組み合わせて、ノイズ除去や正規化を一気に行えます。
raw_emails = [" ALICE@example.com ", "bob@EXAMPLE.com", "", "carol@example.com", " "]
# 前後の空白を取り、空文字を捨て、ドメインを小文字化してユニーク化
cleaned = {
e.strip().lower()
for e in raw_emails
if e and e.strip() # 空や空白のみを除外
}
print(cleaned)
{'bob@example.com', 'carol@example.com', 'alice@example.com'}
- 関連記事:strip/lstrip/rstripの違いと使い分け
- 関連記事:大文字小文字変換(upper, lower, capitalize, title)の使い方
- 関連記事:文字列が数字/英字/空白か判定する方法まとめ
順序がない点に注意
セットは順序を持ちません。
インデックスアクセスはできず、表示順も毎回一定とは限りません。
順序が必要ならsorted()
で並べ替えてから使います。
print(sorted(cleaned)) # アルファベット順にソート
['alice@example.com', 'bob@example.com', 'carol@example.com']
実用パターンとベストプラクティス
反転辞書を作る
値をキーに、キーを値にした「反転辞書」を作るのは典型パターンです。
値が一意である前提なら、内包表記で簡潔に書けます。
pref_to_capital = {"Hokkaido": "Sapporo", "Aomori": "Aomori", "Iwate": "Morioka"}
# 値がユニークな場合
capital_to_pref = {capital: pref for pref, capital in pref_to_capital.items()}
print(capital_to_pref)
{'Sapporo': 'Hokkaido', 'Aomori': 'Aomori', 'Morioka': 'Iwate'}
重複する値がある場合は、単純な反転だと後勝ちになり情報が失われます。
集合やリストに集約したいなら、ネストを使った書き方は可能ですが読みやすさと性能に注意します。
# 値ごとに、元のキーの集合を作る(やや重い・全探索)
d = {"a": 1, "b": 2, "c": 1}
inv_to_keys = {
v: {k for k, vv in d.items() if vv == v}
for v in set(d.values())
}
print(inv_to_keys)
{1: {'a', 'c'}, 2: {'b'}}
本格的に集約するならcollections.defaultdict(list)
など通常のループの方が読みやすく高速なことが多いです。
辞書の値を前処理する
文字列の数値を安全に整数へ変換し、変換できないものはNone
にする例です。
式の中での軽いバリデーションは内包表記と相性が良いです。
raw = {"A": "10", "B": "x", "C": "003"}
# 数字のみならint、それ以外はNone
normalized = {k: (int(v) if v.isdigit() else None) for k, v in raw.items()}
print(normalized)
{'A': 10, 'B': None, 'C': 3}
より複雑な前処理(例外処理や複数段の検証)は、関数化してから内包表記内で呼ぶと読みやすさを保てます。
def to_int_or_none(s: str) -> int | None:
# 先頭の0は許容し、空文字や負数、混在は除外する簡易例
return int(s) if s.isdigit() else None
normalized2 = {k: to_int_or_none(v) for k, v in raw.items()}
print(normalized2)
{'A': 10, 'B': None, 'C': 3}
セットでユニーク化
タグやカテゴリの重複を取り除き、表示用に整形する例です。
tags = ["Python", "python", "AI", "ai", "AI ", " Data ", "data"]
unique_tags = {t.strip().lower() for t in tags}
print(unique_tags)
print(sorted(unique_tags)) # 表示は並べ替えると安定
{'ai', 'python', 'data'}
['ai', 'data', 'python']
可読性の基準(Pythonic)
読みやすさが正義です。
内包表記を使うか迷った時の判断基準を示します。
指針 | 説明 |
---|---|
1スクリーンで読めること | 1行の式が長くなりすぎないようにする |
ネストは1段まで | 入れ子が2段以上はループや関数化を検討 |
命名で意味を出す | 使い捨てでも変数名で意図を伝える |
フィルタか分岐かを明確に | if の位置で意味が変わる点に注意 |
複雑な副作用は避ける | 式は「生成」に集中し、副作用はループで |
性能の目安と落とし穴
- 速度の目安: CPythonでは、同等のforループより内包表記が約1.1〜1.5倍程度速いことが多いです。ただし重い関数呼び出しがあると差は小さくなります。
- メモリ: 辞書・セットは全要素を構築するためピークメモリが増加します。巨大データでは段階処理や分割を検討します。
- 変数スコープ: Python 3では内包表記のループ変数は外側に漏れません。外側の同名変数を汚染しないので安心です。
- ハッシュ可能性: セットの要素や辞書のキーには
hashable
(不変)なオブジェクト(例:str
,int
,tuple
)が必要です。リストや辞書はキー/要素にできません。
簡易ベンチマーク例です(環境により異なります)。
import timeit
setup = "data = list(range(10000))"
stmt_loop = """
d = {}
for x in data:
if x % 2 == 0:
d[x] = x * x
"""
stmt_comp = "{x: x*x for x in data if x % 2 == 0}"
t_loop = timeit.timeit(stmt_loop, setup=setup, number=200)
t_comp = timeit.timeit(stmt_comp, setup=setup, number=200)
print(f"forループ: {t_loop:.3f}s, 内包表記: {t_comp:.3f}s, 倍率: {t_loop/t_comp:.2f}x")
forループ: 0.220s, 内包表記: 0.170s, 倍率: 1.29x
- 関連記事:実行時間を計測する方法まとめ(timeitとperf_counter)
- 内包表記(スコープの挙動)— Python 3.13.7
- データ型 — 辞書/集合のキー要件 — Python 3.13.7
- timeit — Python 3.13.7
ネストは浅く保つ
ネストした内包表記は強力ですが、読みづらさの代償があります。
2段以上の入れ子はループに分解するのが無難です。
# 悪例: ネストと条件が増えて読みにくい
pairs = [(i, j) for i in range(5) for j in range(5) if (i + j) % 3 == 0 if i != j]
# 良例: 条件を名前付きで分け、段階的に書く
pairs2 = []
for i in range(5):
for j in range(5):
valid_sum = (i + j) % 3 == 0
different = i != j
if valid_sum and different:
pairs2.append((i, j))
print(len(pairs), len(pairs2), set(pairs) == set(pairs2))
8 8 True
可読性を優先して「分解する勇気」を持つと、将来の保守コストが下がります。
まとめ
辞書内包表記とセット内包表記は、データの生成と正規化を短く表現できる強力な構文です。
基本は{key: value for ...}
と{elem for ...}
で、末尾のif
でフィルタ、値の中のif-else
で分岐と覚えると混乱しません。
キー衝突時の後勝ちやセットの順序なしといった性質を理解し、ネストは浅く保ちましょう。
迷ったときは、読みやすさを最優先に通常のforループへ戻す判断が一番です。