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 會自動檢查是否已存在相同元素,若已存在則忽略。
基本去重方式
- 直接把列表轉成集合
>>> numbers = [1, 2, 2, 3, 4, 4, 4] >>> unique = set(numbers) # {1, 2, 3, 4} >>> list(unique) # [1, 2, 3, 4](順序不固定) - 保持原始順序的去重(利用
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()。 |
最佳實踐小結
- 先判斷是否需要保留順序:不需要則直接
set(seq);需要則使用dict.fromkeys。 - 確保元素可雜湊:基本型別(int、float、str、tuple)皆可;自訂類別需實作
__hash__與__eq__。 - 記憶體敏感時採用流式處理:配合生成器、分批讀取,避免一次性載入全部資料。
- 善用
frozenset:當集合本身要作為鍵或要放入另一個集合時,使用不可變的frozenset。 - 利用集合運算簡化邏輯:交集、聯集、差集等一行代碼即可完成複雜的資料比對。
實際應用場景
| 場景 | 需求 | 使用集合的好處 |
|---|---|---|
| 電商商品去重 | 從多個供應商 API 抓取商品清單,需去除重複品項。 | set 可在毫秒內去除重複,並可配合 frozenset 記錄商品屬性組合。 |
| 日誌分析 | 分析大型伺服器 log,找出唯一的錯誤訊息。 | 以 set 逐行加入,避免重複儲存相同訊息,節省磁碟與記憶體。 |
| 社群關係圖 | 統計兩位使用者是否為好友,或共同參與的群組。 | frozenset 作為鍵,快速查詢「無序」的兩人組合是否已存在。 |
| 資料清理 | CSV 檔案中某欄位出現大量重複值,需要建立參考表。 | 先把欄位值轉為 set,再寫回資料庫作為唯一索引。 |
| 機器學習特徵工程 | 需要把類別特徵轉為唯一值集合,以便編碼。 | set 獲得所有唯一類別,配合 enumerate 建立編碼映射。 |
總結
集合(set、frozenset)是 Python 中最直接、效能最高的去重工具。透過雜湊表的特性,我們可以在常數時間內判斷元素是否已出現,從而快速取得唯一值。本文介紹了:
- 基本的
set去重與保留順序的技巧 frozenset作為不可變鍵的特殊用途- 大量資料分批去重的記憶體友好寫法
- 常見的集合運算(交、聯、差)在實務中的應用
掌握上述概念與範例後,你將能在 資料清理、統計分析、關係圖建構 等多種情境下,輕鬆、正確地處理重複資料,提升程式的效能與可讀性。祝你在 Python 的資料結構世界裡玩得開心,寫出更乾淨、更高效的程式碼!