本文 AI 產出,尚未審核

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 的方式

  1. 直接使用建構子

    fs = frozenset([1, 2, 3])
    
  2. 從其他集合類型轉換

    s = {4, 5, 6}
    fs2 = frozenset(s)   # 直接把 set 轉成 frozenset
    
  3. 使用字串或其他可迭代物件

    fs3 = frozenset('hello')   # {'h', 'e', 'l', 'o'}
    

注意frozenset 只能接受可迭代其元素本身必須是可雜湊的資料型別(如 int, str, tuple),不能直接放入 listdict、或其他 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 listdictset 本身不可雜湊,會拋出 TypeError 先將可變物件轉成不可變類型(如 tuplefrozenset),再加入。
誤以為 frozenset 完全不可變 雖然集合本身不可變,但其裡面的 可變元素(若是 list 包含在 tuple 中)仍可被改變,破壞整體不可變性。 確保所有元素本身也是不可變的,或在建立時做深拷貝。
使用 frozenset 代替 list 進行排序 frozenset 沒有順序,也不支援索引或切片。 若需要保持不變且有序,使用 tuple;若僅需集合運算,仍使用 frozenset
將大量資料直接轉成 frozenset 轉換過程會一次性把所有元素載入記憶體,對大資料集可能造成記憶體壓力。 逐批處理或使用生成器先過濾,再一次性建立 frozenset
忘記 frozenset 不能使用 addremove 等方法 嘗試呼叫 add 會得到 AttributeError 改用集合運算產生新集合,或在需要可變操作的階段使用 set,最後再轉回 frozenset

最佳實踐

  1. 先使用 set 做暫時的變更,完成後一次性轉成 frozenset
  2. frozenset 作為常量(module level constant)或 類別屬性,避免在程式執行期間不小心重新賦值。
  3. 在字典或集合的鍵值,盡量使用 已排序的 tuplefrozenset,確保一致的 hash 值。
  4. 寫測試時,利用 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 的資料結構中發揮最大的彈性與效率。祝你寫程式愉快!