Python の frozenset(イミュータブルな集合)

frozenset は変更不可能(イミュータブル)な集合だ。通常の set と同じ要素を持てるが、作成後に要素を追加・削除できない。

基本的な作成方法

frozenset() コンストラクタにイテラブルを渡して作成する。

fs = frozenset([1, 2, 3])
print(fs)       # frozenset({1, 2, 3})
print(type(fs)) # <class 'frozenset'>

set と同様に、重複は自動的に排除される。

fs = frozenset([1, 2, 2, 3, 3, 3])
print(fs)  # frozenset({1, 2, 3})

空の frozenset を作るには引数なしで呼び出す。

empty = frozenset()
print(empty)  # frozenset()

set との違い

frozenset はイミュータブルなため、要素を変更するメソッドが存在しない。

fs = frozenset([1, 2, 3])

# これらはすべてエラー
# fs.add(4)           # AttributeError
# fs.remove(1)        # AttributeError
# fs.update([4, 5])   # AttributeError
set

add(), remove(), update() など変更メソッドあり。ハッシュ不可。

frozenset

変更メソッドなし。ハッシュ可能で辞書のキーや集合の要素になれる。

ハッシュ可能である利点

frozenset はハッシュ可能なため、辞書のキーとして使える。

# frozenset を辞書のキーに
permissions = {
    frozenset(["read"]): "viewer",
    frozenset(["read", "write"]): "editor",
    frozenset(["read", "write", "delete"]): "admin"
}

user_perms = frozenset(["read", "write"])
print(permissions[user_perms])  # editor

通常の set ではこれができない。

# set は辞書のキーにできない
d = {
    {1, 2}: "value"  # TypeError: unhashable type: 'set'
}

集合の要素としても使える。つまり「集合の集合」が作れる。

# 冪集合(べきしゅうごう)を作る例
original = {1, 2}
power_set = {
    frozenset(),
    frozenset([1]),
    frozenset([2]),
    frozenset([1, 2])
}
print(power_set)

集合演算

frozenset でも和集合、積集合などの演算ができる。結果は frozenset になる。

a = frozenset([1, 2, 3])
b = frozenset([3, 4, 5])

print(a | b)  # frozenset({1, 2, 3, 4, 5})
print(a & b)  # frozenset({3})
print(a - b)  # frozenset({1, 2})
print(a ^ b)  # frozenset({1, 2, 4, 5})

set と frozenset を混ぜて演算すると、左側のオペランドの型になる。

s = {1, 2, 3}
fs = frozenset([3, 4, 5])

result1 = s | fs
print(type(result1))   # <class 'set'>

result2 = fs | s
print(type(result2))   # <class 'frozenset'>

set との相互変換

set と frozenset は簡単に変換できる。

# set から frozenset へ
s = {1, 2, 3}
fs = frozenset(s)

# frozenset から set へ
fs2 = frozenset([4, 5, 6])
s2 = set(fs2)

一時的に要素を変更したい場合、set に変換してから操作し、また frozenset に戻すパターンがある。

fs = frozenset([1, 2, 3])

# 変更したい場合は一度 set にする
temp = set(fs)
temp.add(4)
temp.remove(1)

# 再び frozenset に
fs = frozenset(temp)
print(fs)  # frozenset({2, 3, 4})

比較と判定

frozenset でも部分集合の判定などが可能だ。set との比較もできる。

fs1 = frozenset([1, 2])
fs2 = frozenset([1, 2, 3])
s = {1, 2, 3}

print(fs1 <= fs2)  # True
print(fs1 <= s)    # True
print(fs1.issubset(s))  # True

等価判定では、set と frozenset は要素が同じなら等しいと判定される。

s = {1, 2, 3}
fs = frozenset([1, 2, 3])

print(s == fs)  # True

ただし型は異なるので、is による同一性判定は False になる。

実用的なユースケース

関数の引数にデフォルト値として集合を使いたい場合、frozenset が安全だ。

# 危険な例(ミュータブルなデフォルト引数)
def bad_func(items=set()):
    items.add("new")
    return items

# 安全な例
def good_func(items=frozenset()):
    return set(items) | {"new"}

キャッシュのキーとしても活用できる。

from functools import lru_cache

@lru_cache(maxsize=100)
def expensive_operation(items: frozenset):
    # 重い処理
    return sum(items)

result = expensive_operation(frozenset([1, 2, 3]))

set はハッシュできないため lru_cache と組み合わせられないが、frozenset なら問題ない。

メモリと性能

frozenset は set とほぼ同じメモリ使用量で、検索の計算量も O(1) だ。イミュータブルであることによるオーバーヘッドはほとんどない。

import sys

s = set(range(1000))
fs = frozenset(range(1000))

print(sys.getsizeof(s))   # 32984
print(sys.getsizeof(fs))  # 32984

変更不可という制約が許容できる場面では、frozenset を積極的に使うことで意図しない変更を防げる。