Python 資料結構 — 集合(set, frozenset)
主題:不可變集合(frozenset)
簡介
在日常的程式開發中,我們常常需要儲存不重複且無順序的元素集合。Python 內建的 set 提供了快速的成員測試、交集、差集等集合運算,卻是可變的(mutable),這意味著它的內容可以隨時被加入或刪除。
然而,在某些情境下,我們希望集合本身保持不變,例如作為 字典的鍵、作為 集合的元素,或是需要保證資料在多執行緒環境下不被意外修改。此時,frozenset(不可變集合)就成為理想的選擇。
本文將深入探討 frozenset 的概念、使用方式、常見陷阱與最佳實踐,並提供實務範例,幫助從初學者到中級開發者都能在專案中正確運用不可變集合。
核心概念
什麼是 frozenset
frozenset 是 Python 的內建類別,與 set 的差別在於 內容不可變,因此它是 hashable(可雜湊),可以當作字典的鍵或其他集合的成員。它的所有集合運算(聯集、交集、差集、對稱差集)仍然返回新的 frozenset,不會改變原本的物件。
# 建立 frozenset
immutable = frozenset([1, 2, 3])
print(type(immutable)) # <class 'frozenset'>
為什麼需要不可變集合
| 使用情境 | 需要 set |
需要 frozenset |
|---|---|---|
| 作為字典鍵 | ❌ | ✅ |
| 作為集合的元素(set of sets) | ❌ | ✅ |
| 多執行緒共享資料,避免競爭條件 | ✅(需加鎖) | ✅(天生安全) |
| 需要保證資料不被意外改寫 | ✅(手動約束) | ✅(語言層面保證) |
建立 frozenset 的方式
直接使用建構子
fs = frozenset([1, 2, 3])從其他集合類型轉換
s = {4, 5, 6} fs2 = frozenset(s) # 直接把 set 轉成 frozenset使用字串或其他可迭代物件
fs3 = frozenset('hello') # {'h', 'e', 'l', 'o'}
注意:
frozenset只能接受可迭代且其元素本身必須是可雜湊的資料型別(如int,str,tuple),不能直接放入list、dict、或其他set。
常用方法與屬性
| 方法 | 說明 |
|---|---|
len(fs) |
回傳集合大小 |
x in fs |
成員測試 |
fs.union(other) / `fs |
other` |
fs.intersection(other) / fs & other |
交集 |
fs.difference(other) / fs - other |
差集 |
fs.symmetric_difference(other) / fs ^ other |
對稱差集 |
fs.isdisjoint(other) |
判斷是否互不相交(返回布林值) |
提示:所有運算皆不會改變原本的
frozenset,而是產生全新的物件。
程式碼範例
1️⃣ 基本操作:建立、測試、計算大小
# 建立不可變集合
prime_numbers = frozenset([2, 3, 5, 7, 11])
print(prime_numbers) # frozenset({2, 3, 5, 7, 11})
# 成員測試
print(7 in prime_numbers) # True
print(9 in prime_numbers) # False
# 取得集合大小
print(len(prime_numbers)) # 5
2️⃣ 集合運算:聯集、交集、差集
evens = frozenset([2, 4, 6, 8])
odds = frozenset([1, 3, 5, 7, 9])
# 聯集 (union)
all_numbers = evens.union(odds)
print(all_numbers) # frozenset({1, 2, 3, 4, 5, 6, 7, 8, 9})
# 交集 (intersection)
common = evens.intersection(prime_numbers)
print(common) # frozenset({2})
# 差集 (difference)
only_evens = evens.difference(prime_numbers)
print(only_evens) # frozenset({4, 6, 8})
3️⃣ 作為字典鍵的實例
# 使用 frozenset 作為字典的鍵
permissions = {
frozenset(['read']): '只能閱讀',
frozenset(['read', 'write']): '可讀可寫',
frozenset(['admin']): '管理員權限'
}
user_perm = frozenset(['read', 'write'])
print(permissions[user_perm]) # 可讀可寫
4️⃣ 嵌套集合:set of frozenset
# 想要儲存多個集合的集合,必須使用 frozenset 作為內層元素
group_a = frozenset(['alice', 'bob'])
group_b = frozenset(['carol', 'dave'])
all_groups = {group_a, group_b} # set of frozenset
print(all_groups)
# {frozenset({'alice', 'bob'}), frozenset({'carol', 'dave'})}
5️⃣ 多執行緒安全的共享資料
import threading
shared = frozenset([0, 1, 2]) # 只讀的全域資料
def worker():
# 只做讀取,不會產生 race condition
if 1 in shared:
print('found 1')
threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
把可變物件放入 frozenset |
list、dict、set 本身不可雜湊,會拋出 TypeError。 |
先將可變物件轉成不可變類型(如 tuple、frozenset),再加入。 |
誤以為 frozenset 完全不可變 |
雖然集合本身不可變,但其裡面的 可變元素(若是 list 包含在 tuple 中)仍可被改變,破壞整體不可變性。 |
確保所有元素本身也是不可變的,或在建立時做深拷貝。 |
使用 frozenset 代替 list 進行排序 |
frozenset 沒有順序,也不支援索引或切片。 |
若需要保持不變且有序,使用 tuple;若僅需集合運算,仍使用 frozenset。 |
將大量資料直接轉成 frozenset |
轉換過程會一次性把所有元素載入記憶體,對大資料集可能造成記憶體壓力。 | 逐批處理或使用生成器先過濾,再一次性建立 frozenset。 |
忘記 frozenset 不能使用 add、remove 等方法 |
嘗試呼叫 add 會得到 AttributeError。 |
改用集合運算產生新集合,或在需要可變操作的階段使用 set,最後再轉回 frozenset。 |
最佳實踐
- 先使用
set做暫時的變更,完成後一次性轉成frozenset。 - 將
frozenset作為常量(module level constant)或 類別屬性,避免在程式執行期間不小心重新賦值。 - 在字典或集合的鍵值,盡量使用 已排序的 tuple 或 frozenset,確保一致的 hash 值。
- 寫測試時,利用
assert isinstance(obj, frozenset)確保資料型別不會被意外改變。
實際應用場景
1️⃣ 權限系統的角色組合
在企業級應用中,使用者可能同時擁有多個權限(read、write、delete)。將每組權限表示為 frozenset,即可作為字典的鍵,快速查找對應的說明或等級。
ROLE_DESCRIPTIONS = {
frozenset(['read']): '只讀者',
frozenset(['read', 'write']): '編輯者',
frozenset(['admin']): '系統管理員'
}
2️⃣ 內容去重與快取
當爬蟲或資料處理流程需要 去除重複的集合(例如不同資料來源產生的相同標籤集合),使用 frozenset 作為快取的鍵,可在 O(1) 時間檢查是否已處理過。
processed = {}
def handle_tags(tags):
key = frozenset(tags) # tags 可能是 list
if key in processed:
return processed[key] # 直接返回快取結果
# ... 進行耗時的計算 ...
result = expensive_compute(tags)
processed[key] = result
return result
3️⃣ 數學或圖論演算法
在圖的演算法中,邊的集合常常需要作為狀態的唯一標識。利用 frozenset 表示「已拜訪的邊集合」或「當前的獨立集」可讓演算法的狀態空間使用 set 直接存取,提升效能。
def dfs(graph, visited_edges=frozenset()):
for edge in graph.edges():
if edge not in visited_edges:
new_visited = visited_edges.union({edge})
dfs(graph, new_visited)
4️⃣ 多執行緒或多進程共享的只讀資料
在需要將大型參考資料(例如不變的配置、常量表)在多執行緒間共享時,將其包裝成 frozenset 可以避免加鎖的開銷,同時保證資料不會被意外改寫。
總結
frozenset 是 Python 中一個簡潔卻強大的資料型別,讓集合同時具備 不重複、無序 的特性與 不可變、可雜湊 的優勢。透過本文的介紹,我們可以:
- 了解 為什麼在需要作為鍵值、嵌套集合或多執行緒安全的情境下,
frozenset是最合適的選擇。 - 掌握 建立、轉換與常用集合運算的語法,並能用程式碼範例快速上手。
- 避免 常見的類型錯誤與不可變性的誤解,遵循最佳實踐寫出更可靠、可維護的程式。
- 應用 在權限系統、快取去重、圖論演算法與多執行緒共享資料等實務場景,提升程式的效能與安全性。
最後,記得在需要「變」的時候使用 set,在需要「不變」且「可作為鍵」的時候選擇 frozenset。適時切換兩者,才能在 Python 的資料結構中發揮最大的彈性與效率。祝你寫程式愉快!