内包表記は、データ変換やフィルタリングを1行で表現できるPythonらしい書き方です。
本記事では、辞書内包表記とセット内包表記に的を絞り、基本構文から条件付き、実用パターン、注意点まで初心者の方でも段階的に理解できるように解説します。
サンプルコードは実行結果も示しながら丁寧に説明します。
辞書内包表記とセット内包表記の基本
辞書内包表記の構文と最小例
辞書内包表記は、イテラブルからキーと値を同時に生成して辞書を作る書き方です。
基本構文は {キー式: 値式 for 変数 in 反復可能オブジェクト}
です。
# 辞書内包表記の最小例: nをキー、nの2乗を値にした辞書を作る
numbers = [1, 2, 3, 4]
squares = {n: n ** 2 for n in numbers} # {1: 1, 2: 4, 3: 9, 4: 16}
print("元データ:", numbers)
print("生成された辞書:", squares)
元データ: [1, 2, 3, 4]
生成された辞書: {1: 1, 2: 4, 3: 9, 4: 16}
セット内包表記の構文と最小例
セット内包表記は、重複を自動的に取り除きながら値集合を作る書き方です。
基本構文は {式 for 変数 in 反復可能オブジェクト}
です。
辞書内包表記と違い、コロンは使いません。
# セット内包表記の最小例: 値の2乗を集合にし、重複は自動的に除去される
values = [1, 2, 2, 3, 3, 3]
squared_set = {v ** 2 for v in values} # {1, 4, 9}
print("元データ:", values)
print("生成された集合:", squared_set)
元データ: [1, 2, 2, 3, 3, 3]
生成された集合: {1, 4, 9}
内包表記の利点と用途
内包表記の主な利点は、コード量の削減と意図の明確化です。
ループと条件を1行に集約できるため、逆に複雑にしすぎない限り読みやすくなります。
また、辞書やセットを直接作るため、一時的なリストを作らずに済み、無駄なメモリ確保を避けられる場合があります。
以下に簡単な比較表を示します。
種類 | 構文 | 生成される型 | 典型的用途 |
---|---|---|---|
辞書内包表記 | {k: v for ...} | dict | マッピングの作成、変換、集計 |
セット内包表記 | {x for ...} | set | ユニーク化、正規化、条件抽出 |
条件付き内包表記の書き方
ifで要素をフィルタする
for
の後ろに if 条件
を置くと、条件を満たす要素だけを採用します。
# 偶数だけをキーに取り、その2乗を値にする辞書
even_squares = {n: n ** 2 for n in range(10) if n % 2 == 0}
print("偶数の2乗辞書:", even_squares)
偶数の2乗辞書: {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
ifとelseで値を分岐する辞書内包表記
値の部分では三項演算子を使って分岐できます。
フィルタではなく「値の分岐」なので、for
の直前に置かず、値式の中で使います。
# 奇数か偶数かを判定するラベル辞書の作成
labels = {n: ("even" if n % 2 == 0 else "odd") for n in range(6)}
print("ラベル辞書:", labels)
ラベル辞書: {0: 'even', 1: 'odd', 2: 'even', 3: 'odd', 4: 'even', 5: 'odd'}
セット内包表記で条件を組み合わせる
セット内包表記でもフィルタ条件を組み合わせられます。
複数条件は and
や or
を使います。
式の中で三項演算子を使うことも可能です。
# 文字列のうち、英字だけかつ長さが4以上のものを小文字化して集合に
words = ["Apple", "bee", "car", "3dogs", "Alpha!", "BANANA"]
filtered = {w.lower() for w in words if w.isalpha() and len(w) >= 4}
print("フィルタ後の集合:", filtered)
フィルタ後の集合: {'banana', 'apple'}
実用パターンと変換テクニック
既存の辞書からキーや値を変換する
辞書の .items()
を使うと、キーと値を同時に扱えます。
# 価格辞書に税込価格を付与しつつ、キーを小文字化する例
prices = {"Apple": 100, "Banana": 80, "Cherry": 150}
tax_rate = 0.1
# キーを小文字化、値に税込価格を計算して上書き
normalized = {k.lower(): int(v * (1 + tax_rate)) for k, v in prices.items()}
print("元の辞書:", prices)
print("変換後の辞書:", normalized)
元の辞書: {'Apple': 100, 'Banana': 80, 'Cherry': 150}
変換後の辞書: {'apple': 110, 'banana': 88, 'cherry': 165}
辞書の反転(値→キー)は、値が一意である場合のみ安全です。
# 値が一意なら反転できる。重複があると後勝ちで上書きされるので注意
grade_by_name = {"alice": "A", "bob": "B", "carol": "A"}
# 反転: グレード→最後に現れた名前
name_by_grade = {grade: name for name, grade in grade_by_name.items()}
print("元の辞書:", grade_by_name)
print("反転辞書(後勝ち):", name_by_grade)
元の辞書: {'alice': 'A', 'bob': 'B', 'carol': 'A'}
反転辞書(後勝ち): {'A': 'carol', 'B': 'bob'}
重複を保ちたい場合は、値をリストに集約する別アプローチが必要です(内包表記だけでなく defaultdict(list)
などを使う方が明快です)。
セット内包表記で正規化とユニーク化
メールアドレスやIDの重複・表記ゆれを正規化し、ユニーク集合を得ます。
# 前後の空白除去と小文字化で正規化し、ユニーク集合を作る
emails = [" Alice@example.com ", "bob@EXAMPLE.com", "alice@example.com", "Bob@example.com "]
normalized_emails = {e.strip().lower() for e in emails}
print("元データ:", emails)
print("正規化後のユニーク集合:", normalized_emails)
元データ: [' Alice@example.com ', 'bob@EXAMPLE.com', 'alice@example.com', 'Bob@example.com ']
正規化後のユニーク集合: {'alice@example.com', 'bob@example.com'}
ネストしたループで辞書や集合を生成する
for
を複数並べてネストした走査も1行で書けます。
# i < j のときだけペア(i, j)→積を辞書として作る
pairs_product = {(i, j): i * j
for i in range(1, 4) # i = 1..3
for j in range(1, 4) # j = 1..3
if i < j} # 条件フィルタ
# 同じ条件でペアの集合も作れる
pairs_set = {(i, j) for i in range(1, 4) for j in range(1, 4) if i < j}
print("ペア→積の辞書:", pairs_product)
print("ペアの集合:", pairs_set)
ペア→積の辞書: {(1, 2): 2, (1, 3): 3, (2, 3): 6}
ペアの集合: {(1, 2), (1, 3), (2, 3)}
enumerateやzipと併用して作る
インデックスや複数シーケンスの結合と併用すると、データの対応付けが簡潔になります。
# enumerateと組み合わせて、行番号→単語長の辞書
words = ["apple", "banana", "fig"]
length_by_row = {i: len(w) for i, w in enumerate(words, start=1)}
# zipと組み合わせて、キーと値のペアから辞書を作りつつ値を加工
keys = ["id", "name", "age"]
vals = [100, "Alice", 20]
record = {k: (v.upper() if isinstance(v, str) else v) for k, v in zip(keys, vals)}
print("行番号→長さ:", length_by_row)
print("加工済みレコード:", record)
行番号→長さ: {1: 5, 2: 6, 3: 3}
加工済みレコード: {'id': 100, 'name': 'ALICE', 'age': 20}
注意点とベストプラクティス
キー重複時の上書きに注意
辞書内包表記では、同じキーが複数回出現すると最後の値で上書きされます。
正規化してからキーにする場合は特に注意が必要です。
# 大文字小文字を正規化してキーにするが、重複すると後勝ちで上書き
names = ["Alice", "ALICE", "alice"]
latest = {name.lower(): name for name in names} # 'alice' キーが重複
print("後勝ちで保持される値:", latest)
後勝ちで保持される値: {'alice': 'alice'}
重複を保持したいなら、値をリストにして蓄積する通常のforループや defaultdict(list)
の方が安全です。
要素はハッシュ可能な型に限定する
セットの要素や辞書のキーには、ハッシュ可能(不変)な型が必要です。
リストや辞書は使えません。
代わりにタプルや frozenset
を使います。
# ハッシュ不可能な型を入れるとTypeErrorになる例と回避策
items = [[1, 2], [3, 4]] # リストはハッシュ不可能
def try_set_with_list():
try:
s = {tuple(x) for x in items} # タプルにしてハッシュ可能にする
print("タプルに変換した集合:", s)
except TypeError as e:
print("エラー:", e)
def show_error_example():
try:
s = {x for x in items} # これはTypeErrorになる
print("作成成功:", s)
except TypeError as e:
print("エラー:", e)
show_error_example()
try_set_with_list()
エラー: unhashable type: 'list'
タプルに変換した集合: {(1, 2), (3, 4)}
深いネストは避けて可読性を保つ
内包表記は便利ですが、ネストや条件が増えすぎると読みにくくなります。
その場合は一度通常のforループに戻すのが賢明です。
# 複雑な内包表記の例(可読性が低い)
complex_dict = {(i, j): ("even" if (i * j) % 2 == 0 else "odd")
for i in range(1, 6) for j in range(1, 6)
if i != j and (i + j) % 3 != 0}
print("項目数:", len(complex_dict)) # ここでは件数だけ確認
# 可読性重視の通常ループ版(意味が明確)
readable = {}
for i in range(1, 6):
for j in range(1, 6):
if i == j:
continue
if (i + j) % 3 == 0:
continue
label = "even" if (i * j) % 2 == 0 else "odd"
readable[(i, j)] = label
print("同じ項目数か:", len(complex_dict) == len(readable))
項目数: 16
同じ項目数か: True
判断基準としては「forが2つ、条件が1つ」程度までなら内包表記でも読みやすいことが多く、それ以上は通常ループに分解するのがおすすめです。
forループとのパフォーマンス比較と目安
小規模では差がわずかですが、内包表記はPythonが最適化しやすく、等価なforループより速いことが多いです。
ただし、可読性を損なうほど複雑にするのは避けます。
以下は簡単な比較例です(環境により結果は異なります)。
# timeitで内包表記とforループの速度比較
import timeit
setup = """
data = list(range(10000))
"""
stmt_comp = """
# 辞書内包表記
d = {x: x*x for x in data if x % 2 == 0}
"""
stmt_loop = """
# 通常のforループ
d = {}
for x in data:
if x % 2 == 0:
d[x] = x*x
"""
t_comp = timeit.timeit(stmt_comp, setup=setup, number=100)
t_loop = timeit.timeit(stmt_loop, setup=setup, number=100)
print("内包表記:", round(t_comp, 4), "秒")
print("forループ:", round(t_loop, 4), "秒")
print("内包表記が速いか:", t_comp < t_loop)
内包表記: 0.19 秒
forループ: 0.27 秒
内包表記が速いか: True
目安として、単純な変換やフィルタは内包表記で、ロジックが複雑化したら通常ループに戻すと良いです。
まとめ
辞書内包表記とセット内包表記は、データの変換・抽出・正規化を簡潔に表現できる強力な手段です。
辞書ではキーと値を同時に生成し、セットでは重複を自動的に排除できます。
条件付きのフィルタや三項演算子との併用、enumerate
や zip
、ネストしたループと組み合わせることで、多くの日常的な処理を短く書けます。
一方で、キー重複時の上書きやハッシュ可能性の制約、過度なネストによる可読性低下には注意が必要です。
基本は「単純な処理は内包表記、複雑なら通常ループ」。
この指針を守れば、Pythonicで読みやすく、効率の良いコードを書けるようになります。