本文 AI 產出,尚未審核

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

主題:集合去重


簡介

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

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


核心概念

什麼是 setfrozenset

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

重點setfrozenset 內部使用 雜湊表(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.fromkeysOrderedDict
    >>> 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)

說明:當資料量過大無法一次載入時,利用 setO(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}")

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


常見陷阱與最佳實踐

陷阱 說明 解決方案
元素不可雜湊 嘗試把 listdict 直接放入 set 會拋 TypeError 只放入 不可變(hashable)類型;若需要去重可先把列表轉為 元組 (tuple) 再加入集合。
順序遺失 set 本身不保證順序,直接轉換會改變原始排列。 若需保留順序,使用 dict.fromkeyslist(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 建立編碼映射。

總結

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

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

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