本文 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),不能直接放入 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。

最佳實踐

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