Python
資料結構 - 元組(tuple)
主題:不可變特性
簡介
在 Python 中,元組(tuple) 是最常見的「不可變」資料結構之一。與可變的 list 相比,元組在建立之後,其內部的元素 不能 被新增、刪除或改寫。這看似限制其彈性,卻在多種情境下提供了安全性、效能與可雜湊性(hashability),讓它成為函式參數、字典鍵值、以及需要保證資料不被意外改變的場景的首選。
了解元組的不可變特性不只是語法層面的認知,更是設計穩健程式的重要基礎。本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整剖析為什麼以及如何善用「不可變」這個特性。
核心概念
1. 什麼是「不可變」
在 Python 中,不可變(immutable) 意味著物件在建立後,其內部狀態(即儲存的資料)無法被直接修改。對元組而言,以下操作都是 不允許 的:
tuple[0] = 1(直接賦值)tuple.append(5)(呼叫可變方法)del tuple[1](刪除元素)
如果嘗試上述操作,Python 會拋出 TypeError。
重點:不可變物件一旦被建立,就會在記憶體中保持「只讀」的狀態,只有透過重新建立新元組的方式才能達成「變更」的效果。
2. 為什麼元組是不可變的
- 記憶體效能:元組的大小在建立時即固定,Python 可以一次性分配連續記憶體,查找速度快於 list。
- 可雜湊性:因為內容不會改變,元組可以作為
dict或set的鍵(只要其中的元素本身也是可雜湊的)。 - 程式安全:傳遞給函式的資料若不希望被函式內部意外修改,使用元組能避免副作用。
3. 建立元組的語法
# 空元組
empty = ()
# 單一元素元組(必須加逗號)
single = (42,)
# 多元素元組
coordinates = (10, 20, 30)
# 不使用括號的「隱式」元組
colors = 'red', 'green', 'blue'
4. 元組的常見操作
即使是不可變的,元組仍提供許多只讀的操作,例如索引、切片、串接、重複、以及內建函式 len()、count()、index() 等。
t = (1, 2, 3, 2, 5)
# 取得元素
print(t[1]) # 2
# 切片會產生新元組
sub = t[1:4] # (2, 3, 2)
# 串接產生新元組
new = t + (9, 10) # (1, 2, 3, 2, 5, 9, 10)
# 重複
repeat = (0,) * 5 # (0, 0, 0, 0, 0)
# 內建函式
print(len(t)) # 5
print(t.count(2)) # 2
print(t.index(5)) # 4
5. 透過「重新建立」實作「修改」
若真的需要「改變」元組的內容,常見的做法是先轉換成 list,做修改後再轉回 tuple。
orig = (10, 20, 30)
# 轉成 list
tmp = list(orig)
tmp[1] = 99 # 修改第二個元素
# 重新轉回 tuple
modified = tuple(tmp)
print(modified) # (10, 99, 30)
此技巧常用於需要「部分更新」但又想保留元組不可變的語意。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 忘記在單元素元組後加逗號 | t = (5) 其實是整數 5,不是元組 |
必須寫成 t = (5,) |
| 將可變物件放入元組 | 元組本身不可變,但若內含 list、dict 等可變物件,內容仍可被改變 | 只放入不可變物件,或使用 tuple(map(...)) 產生深層不可變結構 |
| 把元組當作「可變」的緩衝區 | 嘗試 t.append() 會拋 AttributeError |
改用 list 作為緩衝,最後再轉成 tuple |
| 使用元組作為字典鍵時忘了檢查可雜湊性 | 若元組裡有 list,會出現 TypeError: unhashable type: 'list' |
確保所有元素都是可雜湊的(int、str、frozenset 等) |
| 過度使用元組導致程式可讀性下降 | 把所有集合都硬塞成元組,失去語意 | 依需求選擇 list(可變)或 tuple(不可變),保持程式語意清晰 |
最佳實踐
- 使用元組傳遞「常量」參數:函式接受的座標、設定值等不應被改變的資料,使用元組明確表達不可變。
- 將元組作為字典鍵:需要快速查找唯一組合時(如
(year, month)),使用元組可直接作為鍵。 - 避免在元組裡放入可變物件:若必須包含可變資料,考慮使用
frozenset或自行實作深層不可變的資料結構。 - 利用切片產生新元組:不需要手動 copy,直接
new_tuple = old_tuple[:i] + (new_item,) + old_tuple[i+1:]。
實際應用場景
1. 函式的多返回值
Python 常利用元組一次回傳多個值,呼叫端可以直接解包,且回傳的資料不會被意外改變。
def divide(a, b):
"""回傳商與餘數,使用元組作為不可變的返回值"""
return a // b, a % b
quotient, remainder = divide(17, 5)
print(f"商={quotient}, 餘數={remainder}") # 商=3, 餘數=2
2. 作為字典的複合鍵
在資料分析或快取機制中,常需要以多個欄位組成唯一索引。元組的可雜湊性讓它成為理想的「複合鍵」。
price_lookup = {
('AAPL', '2024-01-01'): 172.5,
('GOOG', '2024-01-01'): 138.2,
}
def get_price(symbol, date):
return price_lookup.get((symbol, date), None)
print(get_price('AAPL', '2024-01-01')) # 172.5
3. 配置檔與常量集合
在大型專案中,常量集合(如支援的檔案類型、允許的指令)不應被程式執行時意外改變,使用元組可保護這些資料。
SUPPORTED_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.gif')
def is_supported(filename):
return filename.lower().endswith(SUPPORTED_EXTENSIONS)
print(is_supported('photo.PNG')) # True
4. 多執行緒/多程序共享的只讀資料
在使用 multiprocessing 或 threading 時,若有大量只讀資料需要共享,將它們放在元組裡可以避免同步鎖的開銷,因為資料永遠不會被改寫。
from multiprocessing import Process, Queue
# 假設這是一筆只讀的參考表
REFERENCE_DATA = (
(1, 'Apple'),
(2, 'Banana'),
(3, 'Cherry')
)
def worker(q):
# 直接讀取 REFERENCE_DATA,不需要鎖
q.put(len(REFERENCE_DATA))
if __name__ == '__main__':
q = Queue()
p = Process(target=worker, args=(q,))
p.start()
p.join()
print('資料筆數:', q.get()) # 資料筆數: 3
總結
元組的不可變特性是 Python 資料結構設計裡的一個關鍵優勢。它不僅提供了記憶體效能與可雜湊性,還在程式安全、函式接口、以及多執行緒/多程序環境中扮演了「只讀」的角色。
- 了解 什麼是不可變、為何元組不可變,以及 如何透過重新建立 實作「修改」是使用元組的基礎。
- 注意常見陷阱(單元素逗號、可變元素、錯誤的可變方法)並遵循最佳實踐(作為常量、複合鍵、只讀資料)。
- 在實務上,元組廣泛應用於 多返回值、字典鍵、配置常量、以及 跨執行緒的只讀共享 等情境。
掌握這些概念後,你將能更自信地在程式中選擇適當的資料結構,寫出既安全又高效的 Python 程式碼。祝你在 Python 的資料結構旅程中,玩得開心、寫得順手!