本文 AI 產出,尚未審核

Python

資料結構 - 元組(tuple)

主題:不可變特性


簡介

在 Python 中,元組(tuple) 是最常見的「不可變」資料結構之一。與可變的 list 相比,元組在建立之後,其內部的元素 不能 被新增、刪除或改寫。這看似限制其彈性,卻在多種情境下提供了安全性效能可雜湊性(hashability),讓它成為函式參數、字典鍵值、以及需要保證資料不被意外改變的場景的首選。

了解元組的不可變特性不只是語法層面的認知,更是設計穩健程式的重要基礎。本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整剖析為什麼以及如何善用「不可變」這個特性。


核心概念

1. 什麼是「不可變」

在 Python 中,不可變(immutable) 意味著物件在建立後,其內部狀態(即儲存的資料)無法被直接修改。對元組而言,以下操作都是 不允許 的:

  • tuple[0] = 1(直接賦值)
  • tuple.append(5)(呼叫可變方法)
  • del tuple[1](刪除元素)

如果嘗試上述操作,Python 會拋出 TypeError

重點:不可變物件一旦被建立,就會在記憶體中保持「只讀」的狀態,只有透過重新建立新元組的方式才能達成「變更」的效果。

2. 為什麼元組是不可變的

  1. 記憶體效能:元組的大小在建立時即固定,Python 可以一次性分配連續記憶體,查找速度快於 list。
  2. 可雜湊性:因為內容不會改變,元組可以作為 dictset 的鍵(只要其中的元素本身也是可雜湊的)。
  3. 程式安全:傳遞給函式的資料若不希望被函式內部意外修改,使用元組能避免副作用。

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(不可變),保持程式語意清晰

最佳實踐

  1. 使用元組傳遞「常量」參數:函式接受的座標、設定值等不應被改變的資料,使用元組明確表達不可變。
  2. 將元組作為字典鍵:需要快速查找唯一組合時(如 (year, month)),使用元組可直接作為鍵。
  3. 避免在元組裡放入可變物件:若必須包含可變資料,考慮使用 frozenset 或自行實作深層不可變的資料結構。
  4. 利用切片產生新元組:不需要手動 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. 多執行緒/多程序共享的只讀資料

在使用 multiprocessingthreading 時,若有大量只讀資料需要共享,將它們放在元組裡可以避免同步鎖的開銷,因為資料永遠不會被改寫。

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 的資料結構旅程中,玩得開心、寫得順手!