文章目錄
- 一、故事從變量賦值說起
- 二、不可變類型 (Immutable Types)
- 三、可變類型 (Mutable Types)
- 四、一個常見的陷阱:當元組遇到列表
- 五、為什么這個區別如此重要?
- 1. 函數參數的傳遞
- 2. 字典的鍵 (Dictionary Keys)
- 3. 函數的默認參數陷阱
- 六、進階話題與擴展
- 1. 淺拷貝 vs. 深拷貝:`copy` 與 `deepcopy`
- 2. `+=` 運算符:可變與不可變對象的差異
- 3. CPython 的對象緩存機制
- 4. 并發中的可變對象
- 5. 凍結(只讀)數據結構
- 6. 性能小貼士
- 總結
在 Python 的學習和實踐中,有一個核心概念是繞不開的,那就是"可變"(Mutable)與"不可變"(Immutable)類型。剛開始,你可能覺得這只是個定義問題,但隨著你寫出更復雜的程序,你會發現,能否深刻理解這兩者的區別,直接決定了你的代碼是否健壯、高效,以及是否會踩到一些意想不到的"坑"。
這篇文章將帶你由淺入深,徹底搞懂這個關鍵概念。
一、故事從變量賦值說起
在 Python 中,我們常說"變量是貼在對象上的標簽"。理解這句話是后續一切的基礎。
當你寫下 x = 100
時,Python 做了兩件事:
- 在內存中創建了一個代表數字
100
的對象。 - 創建了一個名為
x
的變量(標簽),然后把它"貼"到100
這個對象上。
那么,當我們"修改"變量時,會發生什么呢?這就要看對象的類型了。
二、不可變類型 (Immutable Types)
顧名思義,不可變類型的對象,其值在創建后就不能被改變。任何對它的"修改"操作,實際上都會創建一個全新的對象。
常見的不可變類型包括:
- 數字:
int
,float
,bool
- 字符串:
str
- 元組:
tuple
- 凍結集合:
frozenset
讓我們用代碼和內存地址 id()
來眼見為實。
示例 1: 字符串 str
my_string = "hello"
print(f"初始字符串: '{my_string}', 內存地址: {id(my_string)}")# 嘗試"修改"字符串
my_string = my_string + " world"print(f"修改后字符串: '{my_string}', 內存地址: {id(my_string)}")
輸出:
初始字符串: 'hello', 內存地址: 4389754112
修改后字符串: 'hello world', 內存地址: 4389754224
看到了嗎?內存地址變了!Python 并沒有修改原來的 "hello"
對象,而是創建了一個全新的 "hello world"
對象,然后把 my_string
這個標簽從舊對象身上撕下來,貼到了新對象上。
三、可變類型 (Mutable Types)
與不可變類型相反,可變類型的對象,其值可以在創建后被原地修改,而不需要創建新對象。
常見的可變類型包括:
- 列表:
list
- 字典:
dict
- 集合:
set
- 字節數組:
bytearray
示例 2: 列表 list
my_list = [1, 2, 3]
print(f"初始列表: {my_list}, 內存地址: {id(my_list)}")# 嘗試修改列表
my_list.append(4)print(f"修改后列表: {my_list}, 內存地址: {id(my_list)}")
輸出:
初始列表: [1, 2, 3], 內存地址: 4510696320
修改后列表: [1, 2, 3, 4], 內存地址: 4510696320
內存地址完全沒變!append
操作是在原始列表對象上直接進行的修改。my_list
這個標簽自始至終都貼在同一個對象上。
四、一個常見的陷阱:當元組遇到列表
元組 tuple
是不可變的,對吧?這意味著我們不能增加或刪除它的元素。但如果元組里包含了可變類型的對象(比如列表),情況就變得有趣了。
# 元組本身是不可變的
my_tuple = (1, 2, ['a', 'b'])print(f"初始元組: {my_tuple}, 內存地址: {id(my_tuple)}")# 嘗試修改元組中的列表
my_tuple[2].append('c')print(f"修改后元組: {my_tuple}, 內存地址: {id(my_tuple)}")# 嘗試直接修改元組元素(這會報錯)
# my_tuple[0] = 99 # TypeError: 'tuple' object does not support item assignment
輸出:
初始元組: (1, 2, ['a', 'b']), 內存地址: 4474840192
修改后元組: (1, 2, ['a', 'b', 'c']), 內存地址: 4474840192
元組的內存地址沒變,但它里面的列表內容卻實實在在地改變了。
結論:不可變性指的是對象本身的結構固定。對于元組來說,是它所包含的元素的"引用"不可變。它引用的那個列表還是那個列表(內存地址沒變),但列表自身的內容是可以被修改的。
五、為什么這個區別如此重要?
理解可變與不可變,在實際編程中至關重要,尤其體現在以下幾個方面:
1. 函數參數的傳遞
在 Python 中,函數參數傳遞的是對象的引用。
- 如果傳遞的是不可變對象,你在函數內部無法修改原始調用者的變量。
- 如果傳遞的是可變對象,你在函數內部的修改會直接影響到原始對象。
def process_data(immutable_str, mutable_list):immutable_str = "changed"mutable_list.append(99)print(f"函數內部: str='{immutable_str}', list={mutable_list}")s = "original"
l = [1, 2]process_data(s, l)print(f"函數外部: str='{s}', list={l}")
輸出:
函數內部: str='changed', list=[1, 2, 99]
函數外部: str='original', list=[1, 2, 99]
看到結果了嗎?字符串 s
沒變,但列表 l
被永久地改變了。
2. 字典的鍵 (Dictionary Keys)
字典的鍵必須是不可變類型。
這是因為字典的查找效率極高,其內部依賴于對鍵進行哈希運算(hash()
)。哈希值要求在對象的生命周期內保持不變。
- 不可變對象的值固定,哈希值也固定。
- 可變對象的值可以變,如果允許它當鍵,它的哈希值也可能變,整個字典的結構就會崩潰。
my_dict = {}
my_dict["key"] = "value" # 字符串可以當鍵
my_dict[123] = "value" # 整數可以當鍵
my_dict[(1, 2)] = "value" # 元組可以當鍵# 嘗試用列表當鍵
try:my_dict[[1, 2]] = "value"
except TypeError as e:print(e) # 輸出: unhashable type: 'list'
3. 函數的默認參數陷阱
這是一個經典的面試題,也是新手最容易犯的錯誤:永遠不要使用可變類型作為函數的默認參數。
def add_item(item, item_list=[]):item_list.append(item)return item_list# 第一次調用
print(add_item(1)) # 輸出: [1]# 第二次調用
print(add_item(2)) # 輸出: [1, 2] (你可能期望的是 [2])# 第三次調用
print(add_item(3)) # 輸出: [1, 2, 3]
原因:函數的默認參數 item_list=[]
只在函數定義時被創建一次。后續所有不提供 item_list
參數的調用,都共享著同一個列表對象。
正確做法:
def add_item_fixed(item, item_list=None):if item_list is None:item_list = [] # 在函數體內創建新列表item_list.append(item)return item_list
六、進階話題與擴展
1. 淺拷貝 vs. 深拷貝:copy
與 deepcopy
Python 標準庫 copy
模塊提供兩種復制策略:
- 淺拷貝 (
copy.copy
):僅復制最外層容器,新容器內部仍引用原有子對象。 - 深拷貝 (
copy.deepcopy
):遞歸復制整棵對象圖,確保任何層級的修改互不影響。
import copya = [1, [2, 3]]
b = copy.copy(a) # 淺拷貝
c = copy.deepcopy(a) # 深拷貝a[1].append(4)
print(b) # [1, [2, 3, 4]] —— 受影響
print(c) # [1, [2, 3]] —— 不受影響
2. +=
運算符:可變與不可變對象的差異
對于不可變對象(如 str
, tuple
),x += y
會創建 新對象;而對 list
等可變對象,+=
會就地修改。
s = "abc"
print(id(s))s += "d"
print(id(s)) # 地址變化,說明創建了新對象lst = [1, 2]
print(id(lst))lst += [3]
print(id(lst)) # 地址不變,說明原地修改
3. CPython 的對象緩存機制
為了性能,CPython 會緩存 小整數 (-5~256)
與部分短字符串。因此下面代碼在 CPython 中可能打印 True
,并不代表語言層面對這些對象做了特殊對待,而是實現細節:
a = 100
b = 100
print(a is b) # True (CPython)
其它解釋器(PyPy、Jython 等)不一定有相同表現,因此不要依賴該特性來做邏輯判斷。
4. 并發中的可變對象
在多線程 / 協程場景下,共享可變對象必須采用同步原語,否則會產生競態條件或數據損壞;不可變對象天然只讀,可安全共享。
from threading import Lockcounter = 0
lock = Lock()def inc():global counterwith lock:counter += 1
5. 凍結(只讀)數據結構
frozenset
:不可變集合,可作為字典鍵;types.MappingProxyType
:為字典提供只讀視圖;- 三方庫
immutables.Map
:高性能、結構共享的持久化不可變映射。
6. 性能小貼士
- 頻繁拼接字符串時,先收集到
list
再使用''.join(chunks)
,可避免創建大量中間對象; - 對可變對象使用就地修改操作(如
list.append
, 切片賦值)通常更節省內存、提升性能。
總結
特性 | 不可變類型 (Immutable) | 可變類型 (Mutable) |
---|---|---|
定義 | 創建后值不能被改變 | 創建后值可以被原地修改 |
示例 | int , str , tuple , frozenset | list , dict , set |
修改行為 | 創建新對象,變量指向新對象 | 在原對象上修改,變量指向不變 |
字典鍵 | 可以作為字典的鍵 | 不可以作為字典的鍵 |
函數傳參 | 函數內修改不影響外部原變量 | 函數內修改會影響外部原變量 |
掌握 Python 的可變與不可變類型,是寫出清晰、可預測且無 Bug 代碼的基石。希望這篇文章能讓你對這個概念有更深入的理解。