本文 AI 產出,尚未審核

Python 資料結構 – 集合(set、frozenset)

主題:集合去重


簡介

在日常開發中,我們常會碰到「同樣的資料出現多次」的情況,例如從檔案、資料庫或 API 取得的列表裡可能包含重複的元素。重複資料不僅佔用記憶體,還會影響後續的統計、搜尋或比對結果。

Python 提供的 集合(set)與 不可變集合(frozenset)天生具備 唯一性(每個元素只能出現一次)的特性,使用它們即可在 O(1) 平均時間複雜度下完成去重作業。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你全面掌握集合去重的技巧,讓你在專案中快速、正確地處理重複資料。


核心概念

什麼是 set 與 frozenset

特色 set frozenset
可變性 可變(可以新增、刪除元素) 不可變(建立後內容固定)
可作為字典的鍵 否 是(因為不可變)
建立語法 my_set = {1, 2, 3} 或 set([1,2,3]) my_fset = frozenset([1,2,3])
常見用途 暫時性的去重、集合運算(交、並、差) 作為常數集合、作為字典鍵或集合的元素

重點:set 和 frozenset 內部使用 雜湊表(hash table)儲存元素,因此只要元素是 可雜湊(hashable),就能放入集合。

為什麼集合可以去重

  • 雜湊唯一性:每個元素在集合中以其雜湊值作為索引,若兩個元素的雜湊值相同且相等,集合只會保留第一個出現的那個。
  • 自動過濾:在把資料加入集合的同時,Python 會自動檢查是否已存在相同元素,若已存在則忽略。

基本去重方式

  1. 直接把列表轉成集合
    >>> numbers = [1, 2, 2, 3, 4, 4, 4]
    >>> unique = set(numbers)          # {1, 2, 3, 4}
    >>> list(unique)                  # [1, 2, 3, 4](順序不固定)
    
  2. 保持原始順序的去重(利用 dict.fromkeys 或 OrderedDict)
    >>> numbers = [1, 2, 2, 3, 4, 4, 4]
    >>> unique_ordered = list(dict.fromkeys(numbers))
    >>> unique_ordered
    [1, 2, 3, 4]
    

程式碼範例

以下示範 5 種常見且實用的去重技巧,並說明每個範例的適用情境與注意事項。

1️⃣ 基本集合去重(最簡單)

# 範例 1:將列表直接轉為 set 再回到 list
def dedup_basic(seq):
    """使用 set 去除重複,返回不保留順序的列表"""
    return list(set(seq))

data = ['apple', 'banana', 'apple', 'orange', 'banana']
print(dedup_basic(data))   # 輸出:['orange', 'banana', 'apple'](順序隨機)

說明:此方法最快速,適合不在乎元素順序的情況,例如統計不同商品種類的數量。


2️⃣ 保留原始順序的去重(Python 3.7+)

# 範例 2:利用 dict.fromkeys(保持插入順序)
def dedup_preserve_order(seq):
    """利用 dict.fromkeys 保留原始順序"""
    return list(dict.fromkeys(seq))

data = [5, 3, 5, 2, 3, 1, 2]
print(dedup_preserve_order(data))   # 輸出:[5, 3, 2, 1]

說明:自 Python 3.7 起,普通 dict 已保證插入順序,故可直接使用 dict.fromkeys,寫法簡潔且效能良好。


3️⃣ 使用 frozenset 作為字典鍵

# 範例 3:把不可變集合當作鍵,實作「組合出現次數」統計
def combo_counter(pairs):
    """
    pairs 為 [(a, b), (b, a), (a, c), ...] 的列表,
    以 frozenset 去除順序差異並計數。
    """
    counter = {}
    for a, b in pairs:
        key = frozenset([a, b])   # {a, b} 與 {b, a} 會視為相同
        counter[key] = counter.get(key, 0) + 1
    return counter

pairs = [('apple', 'banana'), ('banana', 'apple'), ('apple', 'orange')]
print(combo_counter(pairs))
# 輸出:{frozenset({'banana', 'apple'}): 2, frozenset({'apple', 'orange'}): 1}

說明:frozenset 可作為字典鍵,用於「不關心元素順序」的組合統計,例如社交網路的好友配對、兩項商品的同時購買次數。


4️⃣ 大量資料的分批去重(記憶體友好)

# 範例 4:分批讀取檔案並即時去重
def dedup_file_lines(filepath, chunk_size=1024*1024):
    """
    逐行讀取大型文字檔,每讀取一個 chunk 就把已見過的行放入 set。
    只保留第一次出現的行,避免一次把整個檔案載入記憶體。
    """
    seen = set()
    with open(filepath, 'r', encoding='utf-8') as f:
        while True:
            lines = f.readlines(chunk_size)
            if not lines:
                break
            for line in lines:
                line = line.rstrip('\n')
                if line not in seen:
                    seen.add(line)
                    yield line   # 產生唯一的行

# 使用方式(示範):
# for unique_line in dedup_file_lines('big_data.txt'):
#     process(unique_line)

說明:當資料量過大無法一次載入時,利用 set 的 O(1) 查找特性,搭配生成器(generator)即可在 流式 的方式完成去重。


5️⃣ 多層集合運算:交集、聯集、差集

# 範例 5:利用 set 套件完成「共同商品」與「僅在某平台」的分析
platform_a = {'apple', 'banana', 'cherry', 'date'}
platform_b = {'banana', 'date', 'elderberry', 'fig'}

# 共同商品(交集)
common = platform_a & platform_b          # {'banana', 'date'}

# 任一平台都有的商品(聯集)
all_items = platform_a | platform_b       # {'apple', 'banana', 'cherry', 'date', 'elderberry', 'fig'}

# 僅在平台 A 有的商品(差集)
only_a = platform_a - platform_b          # {'apple', 'cherry'}

print(f"共同商品: {common}")
print(f"全部商品: {all_items}")
print(f"僅平台A: {only_a}")

說明:集合的交集、聯集、差集在資料分析、推薦系統或權限管理上非常實用,能在 數毫秒 內完成大規模比對。


常見陷阱與最佳實踐

陷阱 說明 解決方案
元素不可雜湊 嘗試把 list、dict 直接放入 set 會拋 TypeError。 只放入 不可變(hashable)類型;若需要去重可先把列表轉為 元組 (tuple) 再加入集合。
順序遺失 set 本身不保證順序,直接轉換會改變原始排列。 若需保留順序,使用 dict.fromkeys、list(dict.fromkeys(...)) 或 OrderedDict(Python <3.7)。
大量資料佔用記憶體 大型資料一次性放入 set 可能導致記憶體不足。 使用 分批 或 生成器(如上範例 4)逐步加入,或考慮外部工具(如 SQLite)做去重。
誤用 frozenset 把可變物件(list)直接放入 frozenset 仍會錯。 先把內部可變物件轉為不可變(如 tuple),再建立 frozenset。
集合運算的副作用 使用 set1 &= set2 會在原地修改 set1,可能影響後續程式。 若不想改變原集合,使用 非破壞性 的運算(set1 & set2)或先 copy()。

最佳實踐小結

  1. 先判斷是否需要保留順序:不需要則直接 set(seq);需要則使用 dict.fromkeys。
  2. 確保元素可雜湊:基本型別(int、float、str、tuple)皆可;自訂類別需實作 __hash__ 與 __eq__。
  3. 記憶體敏感時採用流式處理:配合生成器、分批讀取,避免一次性載入全部資料。
  4. 善用 frozenset:當集合本身要作為鍵或要放入另一個集合時,使用不可變的 frozenset。
  5. 利用集合運算簡化邏輯:交集、聯集、差集等一行代碼即可完成複雜的資料比對。

實際應用場景

場景 需求 使用集合的好處
電商商品去重 從多個供應商 API 抓取商品清單,需去除重複品項。 set 可在毫秒內去除重複,並可配合 frozenset 記錄商品屬性組合。
日誌分析 分析大型伺服器 log,找出唯一的錯誤訊息。 以 set 逐行加入,避免重複儲存相同訊息,節省磁碟與記憶體。
社群關係圖 統計兩位使用者是否為好友,或共同參與的群組。 frozenset 作為鍵,快速查詢「無序」的兩人組合是否已存在。
資料清理 CSV 檔案中某欄位出現大量重複值,需要建立參考表。 先把欄位值轉為 set,再寫回資料庫作為唯一索引。
機器學習特徵工程 需要把類別特徵轉為唯一值集合,以便編碼。 set 獲得所有唯一類別,配合 enumerate 建立編碼映射。

總結

集合(set、frozenset)是 Python 中最直接、效能最高的去重工具。透過雜湊表的特性,我們可以在常數時間內判斷元素是否已出現,從而快速取得唯一值。本文介紹了:

  • 基本的 set 去重與保留順序的技巧
  • frozenset 作為不可變鍵的特殊用途
  • 大量資料分批去重的記憶體友好寫法
  • 常見的集合運算(交、聯、差)在實務中的應用

掌握上述概念與範例後,你將能在 資料清理、統計分析、關係圖建構 等多種情境下,輕鬆、正確地處理重複資料,提升程式的效能與可讀性。祝你在 Python 的資料結構世界裡玩得開心,寫出更乾淨、更高效的程式碼!