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 フィルタとは別物だ。

フィルタの if

for x in ... if 条件 の形。条件を満たす要素だけを処理。

条件式の 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 行で収まる程度の複雑さに抑えるのが望ましい。