Python の集合と内包表記
集合内包表記(set comprehension)を使うと、ループを使わずに簡潔に集合を構築できる。リスト内包表記と同じ構文で、波括弧を使う点が異なる。
基本構文
集合内包表記は {式 for 変数 in イテラブル} の形式で書く。
# 0 から 9 の 2 乗の集合
squares = {x ** 2 for x in range(10)}
print(squares) # {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}
リスト内包表記 [x ** 2 for x in range(10)] と似ているが、結果は集合になる。重複は自動的に排除される。
# 重複が除去される
nums = {x % 3 for x in range(10)}
print(nums) # {0, 1, 2}
0〜9 の各数を 3 で割った余りは 0, 1, 2 のいずれかなので、結果は 3 要素になる。
条件付き内包表記
if 節を追加して、条件を満たす要素だけを含めることができる。
# 偶数だけの集合
evens = {x for x in range(20) if x % 2 == 0}
print(evens) # {0, 2, 4, 6, 8, 10, 12, 14, 16, 18}
条件は複数書ける。
# 3 でも 5 でも割り切れない数
nums = {x for x in range(30) if x % 3 != 0 if x % 5 != 0}
print(nums) # {1, 2, 4, 7, 8, 11, 13, 14, 16, 17, 19, 22, 23, 26, 28, 29}
入れ子のループ
複数の for を連ねることで、入れ子のループを表現できる。
# 2 つのリストのすべての組み合わせの和
a = [1, 2, 3]
b = [10, 20, 30]
sums = {x + y for x in a for y in b}
print(sums) # {11, 12, 13, 21, 22, 23, 31, 32, 33}
これは以下のループと同等だ。
sums = set()
for x in a:
for y in b:
sums.add(x + y)
文字列処理への応用
文字列から特定の文字を抽出するのに便利だ。
text = "Hello, World! 123"
# アルファベットのみ抽出(小文字化)
letters = {c.lower() for c in text if c.isalpha()}
print(letters) # {'h', 'e', 'l', 'o', 'w', 'r', 'd'}
文字列中の母音だけを抽出する例もある。
vowels_in_text = {c for c in "programming" if c in "aeiou"}
print(vowels_in_text) # {'o', 'a', 'i'}
辞書からの集合生成
辞書のキーや値から集合を作ることもできる。
scores = {"alice": 85, "bob": 92, "charlie": 78, "dave": 85}
# 値(スコア)のユニークな集合
unique_scores = {v for v in scores.values()}
print(unique_scores) # {78, 85, 92}
# 80 点以上の人の集合
high_scorers = {k for k, v in scores.items() if v >= 80}
print(high_scorers) # {'alice', 'bob', 'dave'}
式の変換
式の部分で変換処理を行える。
words = ["Apple", "Banana", "Cherry", "apple", "BANANA"]
# 小文字に正規化した集合
normalized = {w.lower() for w in words}
print(normalized) # {'apple', 'banana', 'cherry'}
変換後に重複が発生しても、集合なので自動的に 1 つにまとまる。
条件式(三項演算子)との組み合わせ
式の中で条件分岐もできる。
numbers = [-3, -1, 0, 1, 3, 5]
# 正の数はそのまま、負の数は絶対値に
result = {x if x > 0 else -x for x in numbers}
print(result) # {0, 1, 3, 5}
if x > 0 else -x は条件式で、内包表記の if フィルタとは別物だ。
for x in ... if 条件 の形。条件を満たす要素だけを処理。
値1 if 条件 else 値2 の形。式の中で値を切り替え。
frozenset の内包表記
frozenset には専用の内包表記がないため、frozenset() で包む。
fs = frozenset({x ** 2 for x in range(5)})
print(fs) # frozenset({0, 1, 4, 9, 16})
一度 set を作ってから frozenset に変換している。
ジェネレータ式との違い
丸括弧を使うとジェネレータ式になり、集合ではなくなる。
# 集合内包表記
s = {x for x in range(5)}
print(type(s)) # <class 'set'>
# ジェネレータ式
g = (x for x in range(5))
print(type(g)) # <class 'generator'>
ジェネレータは遅延評価されるため、大量データを扱う際はメモリ効率がよい。集合が必要なら set() で包む。
large_set = set(x ** 2 for x in range(1000000))
可読性の考慮
複雑な内包表記は可読性が下がる。以下のような場合は通常のループのほうがよい。
# 複雑すぎる例
result = {
(x, y, z)
for x in range(10)
for y in range(10)
for z in range(10)
if x + y + z == 15
if x < y < z
}
このような場合は、関数に切り出すか通常のループで書くほうが読みやすい。
def find_triplets(total, limit):
result = set()
for x in range(limit):
for y in range(x + 1, limit):
z = total - x - y
if y < z < limit:
result.add((x, y, z))
return result
print(find_triplets(15, 10))
内包表記は 1〜2 行で収まる程度の複雑さに抑えるのが望ましい。