Python 可變與不可變類型
要真正理解 Python 的資料類型,我們必須先掌握一個最核心的概念:在 Python 中,變數並不是一個儲存值的「容器」,而是一個名稱(或標籤),它指向記憶體中的某個 Object (物件)。 這個Object本身有自己的類型,而這類型也決定了它的值是否能被改變,這大致可分為「可變 (mutable)」和「不可變 (immutable)」。
當你操作一個變數時,關鍵在於:
- 改變變數的指向 (賦值):當你寫
x = 20
時,你只是讓x
這個名稱去指向20
這個 Object。 - 修改 Object 本身:
- 如果 Object 是不可變的,你無法修改它。任何看似「修改」的操作(如
x = x + 1
),實際上是 Python 創建了一個新的 Object,然後讓變數名稱指向這個新 Object。 - 如果 Object 是可變的,你可以直接在原地 (in-place) 修改它,而不需要創建新 Object。所有指向這個 Object 的變數都會看到這個變化。
- 如果 Object 是不可變的,你無法修改它。任何看似「修改」的操作(如

圖中可見,把 x 的值加 1,其實是把 x 指向其他的 Object,而不是修改同一 Object,因 int 是 immutable 類型。
🧱 什麼是不可變類型 (Immutable Types)?
不可變類型指的是一個 Object 在被創建後,它的值就不能被修改。如果你嘗試修改它,Python 會創建一個全新的 Object。
Python 中常見的不可變類型包括:
int
(整數)float
(浮點數)str
(字串)tuple
(元組)bool
(布林值)
讓我們透過程式碼來看看這是什麼意思。
範例 1:整數 (int)
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)
字串也是不可變的。你不能修改字串中的某個字元。
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)
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)
字典也是同樣的道理。
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 本身的內容(只適用於可變類型)。
讓我們比較一下:
# 不可變類型範例
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_a
和 list_b
都指向同一個 Object,所以透過任何一個變數都能看到這個修改。
⚙️ 實際應用:函數參數的陷阱
這個概念在處理函數參數時尤其重要。Python 的參數傳遞方式是「傳遞物件參考 (pass-by-object-reference)」。簡單來說,函數會得到一個指向外部 Object 的參考(或者說是副本)。
傳遞不可變類型
當你傳入一個不可變類型(如數字)時,函數內部對參數的重新賦值不會影響外部的變數。
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
函數內部的 n
在 n = n + 10
後指向了一個新的 Object 15
,但這完全不影響外部的 num
,它仍然指向原來的 Object 5
。
傳遞可變類型
當你傳入一個可變類型(如 list)時,如果在函數內部修改了這個 Object,這個修改會反映到函數外部。
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,你應該在函數內部創建一個副本進行操作。
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 , bool | list , dict , set |
📝 總結
掌握可變與不可變類型的區別,是從 Python 新手邁向熟練開發者的重要一步。這個看似簡單的概念,實際上是 Python 記憶體管理和變數賦值模型的基礎,並直接影響到程式的行為,尤其是在處理複雜數據結構和函數時。
下次當你遇到變數的值「神秘地」改變時,不妨問問自己:我操作的是變數的指向,還是Object 本身?我處理的是可變類型還是不可變類型?並善用 id()
函數來驗證你的想法。這將使你對程式碼的掌握更上一層樓。