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
add(), remove(), update() など変更メソッドあり。ハッシュ不可。
変更メソッドなし。ハッシュ可能で辞書のキーや集合の要素になれる。
ハッシュ可能である利点
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 を積極的に使うことで意図しない変更を防げる。