閉じる

【Python】辞書内包表記/セット内包表記に入門する

コードを簡潔に保ちながら意図を正確に伝えるのはPythonicな書き方の要です。

本記事では、辞書とセットを1行で組み立てられる内包表記の基本から実践パターンまでを、初心者でも段階的に理解できるように詳しく解説します。

読みやすさと性能の両立を目指し、注意点や落とし穴も丁寧に説明します。

Pythonの辞書内包表記/セット内包表記の基本

内包表記とは

内包表記は、反復処理(イテレーション)と条件分岐を組み合わせて、新しいコレクションを1行で生成する構文です。

リスト内包表記が有名ですが、辞書(dict)とセット(set)にも対応しています。

「何を生成したいか」を式で宣言的に記述できるため、読みやすく保ちながらコード量を削減できます。

辞書内包表記/セット内包表記の基本構文

辞書とセットでは中身の式が少し異なります。

辞書はキーと値のペア、セットは要素のみを生成します。

  • 辞書内包表記: {key_expr: value_expr for 変数 in 反復可能オブジェクト if 条件}
  • セット内包表記: {elem_expr for 変数 in 反復可能オブジェクト if 条件}
Python
# 辞書: 数値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ループ
可読性単純な生成に強い(短く意図が明確)複雑な処理に強い(段階的に書ける)
速度しばしば速い(関数呼び出しが少ない)わずかに遅いことが多い
保守性ネストが深いと読みにくいロジックを分割しやすい
デバッグ例外箇所が読み取りにくい行ごとに追いやすい

forループからの置き換え

同じ処理をforループと内包表記で書き比べます。

Python
# 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)の使い方

キー/値を生成する

キーと値を自由に組み立てられます。

列挙番号をキーに、文字列長を値にする例です。

Python
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()で(キー, 値)を反復し、変換した新しい辞書を作ります。

Python
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や整数(円単位で扱うなど)を検討します。

Python
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を置くと、条件を満たすペアのみを取り込みます。

Python
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で値を分岐

値の式の中で条件演算子を使うと、キーはそのまま、値だけを切り替えられます。

Python
# 合否を値にする
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 ...}は「分岐」。

用途が異なります。

キー衝突は後勝ち

辞書はキーが重複すると後から代入した値で上書きされます。

内包表記で同じキーが生成される場合も同様です。

最終的な値は最後に評価されたものになります。

Python
# 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)の使い方

基本構文と重複排除

セットは要素の重複を自動で排除します。

ユニーク値を素早く取り出す用途に向いています。

Python
words = ["Apple", "banana", "APPLE", "Banana", "orange"]

# 小文字化してユニーク化
unique_lower = {w.lower() for w in words}
print(unique_lower)
実行結果
{'banana', 'apple', 'orange'}

条件で抽出・正規化

フィルタと前処理を組み合わせて、ノイズ除去や正規化を一気に行えます。

Python
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'}

順序がない点に注意

セットは順序を持ちません。

インデックスアクセスはできず、表示順も毎回一定とは限りません。

順序が必要ならsorted()で並べ替えてから使います。

Python
print(sorted(cleaned))  # アルファベット順にソート
実行結果
['alice@example.com', 'bob@example.com', 'carol@example.com']

実用パターンとベストプラクティス

反転辞書を作る

値をキーに、キーを値にした「反転辞書」を作るのは典型パターンです。

値が一意である前提なら、内包表記で簡潔に書けます。

Python
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'}

重複する値がある場合は、単純な反転だと後勝ちになり情報が失われます。

集合やリストに集約したいなら、ネストを使った書き方は可能ですが読みやすさと性能に注意します。

Python
# 値ごとに、元のキーの集合を作る(やや重い・全探索)
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にする例です。

式の中での軽いバリデーションは内包表記と相性が良いです。

Python
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}

より複雑な前処理(例外処理や複数段の検証)は、関数化してから内包表記内で呼ぶと読みやすさを保てます。

Python
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}

セットでユニーク化

タグやカテゴリの重複を取り除き、表示用に整形する例です。

Python
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)が必要です。リストや辞書はキー/要素にできません

簡易ベンチマーク例です(環境により異なります)。

Python
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

ネストは浅く保つ

ネストした内包表記は強力ですが、読みづらさの代償があります。

2段以上の入れ子はループに分解するのが無難です。

Python
# 悪例: ネストと条件が増えて読みにくい
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ループへ戻す判断が一番です。

Python 実践TIPS - コーディング効率化・Pythonic
この記事を書いた人
エーテリア編集部
エーテリア編集部

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

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

URLをコピーしました!