Skip to content

Python 可變與不可變類型

要真正理解 Python 的資料類型,我們必須先掌握一個最核心的概念:在 Python 中,變數並不是一個儲存值的「容器」,而是一個名稱(或標籤),它指向記憶體中的某個 Object (物件)。 這個Object本身有自己的類型,而這類型也決定了它的是否能被改變,這大致可分為「可變 (mutable)」和「不可變 (immutable)」。

當你操作一個變數時,關鍵在於:

  1. 改變變數的指向 (賦值):當你寫 x = 20 時,你只是讓 x 這個名稱去指向 20 這個 Object。
  2. 修改 Object 本身
    • 如果 Object 是不可變的,你無法修改它。任何看似「修改」的操作(如 x = x + 1),實際上是 Python 創建了一個新的 Object,然後讓變數名稱指向這個新 Object。
    • 如果 Object 是可變的,你可以直接在原地 (in-place) 修改它,而不需要創建新 Object。所有指向這個 Object 的變數都會看到這個變化。
Image

圖中可見,把 x 的值加 1,其實是把 x 指向其他的 Object,而不是修改同一 Object,因 int 是 immutable 類型。

🧱 什麼是不可變類型 (Immutable Types)?

不可變類型指的是一個 Object 在被創建後,它的值就不能被修改。如果你嘗試修改它,Python 會創建一個全新的 Object。

Python 中常見的不可變類型包括:

  • int (整數)
  • float (浮點數)
  • str (字串)
  • tuple (元組)
  • bool (布林值)

讓我們透過程式碼來看看這是什麼意思。

範例 1:整數 (int)

python
x = 10
print(f"x 的值: {x}, 記憶體地址: {id(x)}")

# 這是一個賦值操作 (reassignment)
# Python 創建了新 Object 20,並讓 x 指向它
x = 20
print(f"x 的值: {x}, 記憶體地址: {id(x)}")

輸出結果:

x 的值: 10, 記憶體地址: 3089259168272
x 的值: 20, 記憶體地址: 3089259168592

注意看,當 x 的值從 10 變為 20 時,它的記憶體地址 (id()) 也改變了。這證明 Python 並沒有修改原來值為 10 的那個 Object,而是創建了一個新 Object 20,並讓變數 x 指向這個新 Object。

範例 2:字串 (str)

字串也是不可變的。你不能修改字串中的某個字元。

python
my_string = "hello"
print(f"原始字串的記憶體地址: {id(my_string)}")

# 嘗試修改字串的第一個字元會引發錯誤
# my_string[0] = 'H'  # 這行會報 TypeError

# "修改" 字串實際上是創建一個新字串,並重新賦值
my_string = my_string + " world"
print(f"新字串: '{my_string}'")
print(f"新字串的記憶體地址: {id(my_string)}")

輸出結果:

原始字串的記憶體地址: 1955314092144
新字串: 'hello world'
新字串的記憶體地址: 1955314142576

同樣地,記憶體地址發生了變化,表示 + 運算符創建了一個全新的字串 Object。

💧 什麼是可變類型 (Mutable Types)?

與不可變類型相反,可變類型的 Object 在創建後,它的值可以被修改,而不需要創建新 Object。修改是「原地 (in-place)」發生的。

Python 中常見的可變類型包括:

  • list (列表)
  • dict (字典)
  • set (集合)

範例 1:列表 (list)

python
my_list = [1, 2, 3]
print(f"原始 list: {my_list}, 記憶體地址: {id(my_list)}")

# 這些是原地修改 (in-place modification) 操作
my_list.append(4)
my_list[0] = 99
print(f"修改後 list: {my_list}, 記憶體地址: {id(my_list)}")

輸出結果:

原始 list: [1, 2, 3], 記憶體地址: 2031177053888
修改後 list: [99, 2, 3, 4], 記憶體地址: 2031177053888

看到嗎?即使 my_list 的內容改變了,它的記憶體地址 id() 始終如一。這證明我們是在同一個 Object 上進行修改。

範例 2:字典 (dict)

字典也是同樣的道理。

python
my_dict = {'name': 'Alice'}
print(f"原始 dict: {my_dict}, 記憶體地址: {id(my_dict)}")

# 原地修改 dict
my_dict['age'] = 25
print(f"修改後 dict: {my_dict}, 記憶體地址: {id(my_dict)}")

輸出結果:

原始 dict: {'name': 'Alice'}, 記憶體地址: 2985127135168
修改後 dict: {'name': 'Alice', 'age': 25}, 記憶體地址: 2985127135168

記憶體地址沒有改變,證明字典是可變的。

🤔 賦值 vs. 修改:關鍵區別

理解可變與不可變的關鍵在於分清「賦值 (assignment)」和「修改 (modification)」。

  • 賦值 (=):讓一個變數名稱指向一個 Object。
  • 修改:改變 Object 本身的內容(只適用於可變類型)。

讓我們比較一下:

python
# 不可變類型範例
a = 10
b = a  # b 和 a 指向同一個 Object 10
print(f"a: {a} (id: {id(a)}), b: {b} (id: {id(b)})")

a = 20 # 賦值操作:a 指向一個新的 Object 20
print(f"a: {a} (id: {id(a)}), b: {b} (id: {id(b)})") # b 仍然指向 10

# 可變類型範例
list_a = [1, 2]
list_b = list_a # list_b 和 list_a 指向同一個 list Object
print(f"list_a: {list_a} (id: {id(list_a)}), list_b: {list_b} (id: {id(list_b)})")

list_a.append(3) # 修改操作:修改 list_a 和 list_b 共同指向的那個 Object
print(f"list_a: {list_a} (id: {id(list_a)}), list_b: {list_b} (id: {id(list_b)})")

對於不可變類型,a = 20 只是讓 a 這個標籤換去貼在 20 這個新 Object 上,b 標籤依然貼在 10 上。 對於可變類型,list_a.append(3) 是直接修改了那個 list Object 本身。因為 list_alist_b 都指向同一個 Object,所以透過任何一個變數都能看到這個修改。

⚙️ 實際應用:函數參數的陷阱

這個概念在處理函數參數時尤其重要。Python 的參數傳遞方式是「傳遞物件參考 (pass-by-object-reference)」。簡單來說,函數會得到一個指向外部 Object 的參考(或者說是副本)。

傳遞不可變類型

當你傳入一個不可變類型(如數字)時,函數內部對參數的重新賦值不會影響外部的變數。

python
def update_number(n):
    print(f"Inner (before): n={n}, id={id(n)}")
    n = n + 10 # 創建新 Object,並讓 n 這個局部變數指向它
    print(f"Inner (after): n={n}, id={id(n)}")

num = 5
print(f"Outer (before): num={num}, id={id(num)}")
update_number(num)
print(f"Outer (after): num={num}, id={id(num)}")

輸出結果:

Outer (before): num=5, id=1749855568240
Inner (before): n=5, id=1749855568240
Inner (after): n=15, id=1749855568560
Outer (after): num=5, id=1749855568240

函數內部的 nn = n + 10 後指向了一個新的 Object 15,但這完全不影響外部的 num,它仍然指向原來的 Object 5

傳遞可變類型

當你傳入一個可變類型(如 list)時,如果在函數內部修改了這個 Object,這個修改會反映到函數外部。

python
def add_item_to_list(items):
    print(f"函數內部 (之前): items={items}, id={id(items)}")
    items.append('new_item') # 原地修改 items 指向的 Object
    print(f"函數內部 (之後): items={items}, id={id(items)}")

my_list = ['old_item']
print(f"函數外部 (之前): my_list={my_list}, id={id(my_list)}")
add_item_to_list(my_list)
print(f"函數外部 (之後): my_list={my_list}, id={id(my_list)}")

輸出結果:

函數外部 (之前): my_list=['old_item'], id=1889902540480
函數內部 (之前): items=['old_item'], id=1889902540480
函數內部 (之後): items=['old_item', 'new_item'], id=1889902540480
函數外部 (之後): my_list=['old_item', 'new_item'], id=1889902540480

因為函數內的 items 和函數外的 my_list 指向同一個 list Object,所以 .append() 的修改是永久性的,從外部也能看到。

如何避免不必要的修改?

如果你不希望函數修改原始的 list,你應該在函數內部創建一個副本進行操作。

python
def safe_add_item(items):
    # 創建一個副本來操作,不影響原始 list
    local_list = items.copy() # 或 list(items)
    local_list.append('new_item')
    print(f"在函數內操作副本: {local_list}")
    return local_list

my_list = ['old_item']
new_list = safe_add_item(my_list)

print(f"原始 list: {my_list}") # 保持不變
print(f"新 list: {new_list}")

📋 總結比較

特性 (Characteristic)不可變類型 (Immutable Types)可變類型 (Mutable Types)
能否原地修改❌ 否。任何修改都會創建新 Object。✅ 是。可以在不改變記憶體地址的情況下修改。
賦值行為 (b = a)b 指向與 a 相同的 Object。若 a 重新賦值,b 不受影響。b 指向與 a 相同的 Object。若透過 a 修改 Object,b 也會看到變化。
作為函數參數在函數內重新賦值不會影響外部變數。在函數內修改 Object 會影響外部變數。
常見例子int, float, str, tuple, boollist, dict, set

📝 總結

掌握可變與不可變類型的區別,是從 Python 新手邁向熟練開發者的重要一步。這個看似簡單的概念,實際上是 Python 記憶體管理和變數賦值模型的基礎,並直接影響到程式的行為,尤其是在處理複雜數據結構和函數時。

下次當你遇到變數的值「神秘地」改變時,不妨問問自己:我操作的是變數的指向,還是Object 本身?我處理的是可變類型還是不可變類型?並善用 id() 函數來驗證你的想法。這將使你對程式碼的掌握更上一層樓。

📚 參考資料

KF Software House