python可變/不可變對(duì)象及+=和=+舉例詳解
前言
在Python開發(fā)中,“可變對(duì)象”與“不可變對(duì)象”是一個(gè)高頻基礎(chǔ)概念,也是初學(xué)者容易混淆的難點(diǎn)。這兩類對(duì)象的核心差異不僅影響變量賦值、函數(shù)傳參的邏輯,更直接關(guān)系到代碼的性能與安全性。本文將從定義區(qū)分→底層原理→核心差異→實(shí)戰(zhàn)場(chǎng)景四個(gè)維度,幫你徹底搞懂這兩類對(duì)象,避免開發(fā)中因概念模糊導(dǎo)致的BUG。
一、先明確:什么是可變對(duì)象?什么是不可變對(duì)象?
首先用最通俗的語(yǔ)言定義兩類對(duì)象,再通過示例直觀感受差異——核心區(qū)別在于“對(duì)象創(chuàng)建后,能否修改其內(nèi)部數(shù)據(jù)”。
1. 不可變對(duì)象(Immutable)
定義:對(duì)象創(chuàng)建后,其內(nèi)部數(shù)據(jù)(值)無法被修改,若要“修改”,只能創(chuàng)建一個(gè)新對(duì)象并指向新地址。
Python中常見的不可變對(duì)象:
- 基礎(chǔ)類型:
int(整數(shù))、str(字符串)、float(浮點(diǎn)數(shù))、bool(布爾值) - 容器類型:
tuple(元組)、frozenset(凍結(jié)集合)
# 示例1:int類型(不可變)
a = 10
print(f"修改前a的值:{a},地址:{id(a)}") # 輸出:修改前a的值:10,地址:140708484554720
a = a + 2 # 看似“修改”,實(shí)際創(chuàng)建新對(duì)象
print(f"修改后a的值:{a},地址:{id(a)}") # 輸出:修改后a的值:12,地址:140708484554784(地址變化)
# 示例2:str類型(不可變)
s = "Python"
print(f"修改前s的值:{s},地址:{id(s)}") # 輸出:修改前s的值:Python,地址:2524607223408
s = s.replace("P", "p") # replace()返回新字符串,原對(duì)象不變
print(f"修改后s的值:{s},地址:{id(s)}") # 輸出:修改后s的值:python,地址:2524607223664(地址變化)
# 示例3:tuple類型(不可變)
t = (1, 2, 3)
# t[0] = 100 # 報(bào)錯(cuò):TypeError: 'tuple' object does not support item assignment(無法修改元素)2. 可變對(duì)象(Mutable)
定義:對(duì)象創(chuàng)建后,其內(nèi)部數(shù)據(jù)(值)可以被直接修改,且修改后對(duì)象的地址(身份標(biāo)識(shí))保持不變。
Python中常見的可變對(duì)象:
- 容器類型:
list(列表)、dict(字典)、set(集合) - 其他:
bytearray(字節(jié)數(shù)組)、自定義類實(shí)例(默認(rèn)可變)
# 示例1:list類型(可變)
lst = [1, 2, 3]
print(f"修改前l(fā)st的值:{lst},地址:{id(lst)}") # 輸出:修改前l(fā)st的值:[1,2,3],地址:2524607215232
lst.append(4) # 直接修改列表內(nèi)部數(shù)據(jù)
print(f"修改后lst的值:{lst},地址:{id(lst)}") # 輸出:修改后lst的值:[1,2,3,4],地址:2524607215232(地址不變)
# 示例2:dict類型(可變)
d = {"name": "張三", "age": 25}
print(f"修改前d的值:k6see6o,地址:{id(d)}") # 輸出:修改前d的值:{'name':'張三','age':25},地址:2524607214848
d["age"] = 26 # 直接修改字典value
print(f"修改后d的值:qiemwuq,地址:{id(d)}") # 輸出:修改后d的值:{'name':'張三','age':26},地址:2524607214848(地址不變)
# 示例3:set類型(可變)
s = {1, 2, 3}
print(f"修改前s的值:{s},地址:{id(s)}") # 輸出:修改前s的值:{1,2,3},地址:2524607198976
s.add(4) # 直接修改集合內(nèi)部數(shù)據(jù)
print(f"修改后s的值:{s},地址:{id(s)}") # 輸出:修改后s的值:{1,2,3,4},地址:2524607198976(地址不變)二、底層原理:為什么會(huì)有“可變”與“不可變”之分?
兩類對(duì)象的差異本質(zhì)是內(nèi)存存儲(chǔ)機(jī)制與對(duì)象身份標(biāo)識(shí)(id) 的設(shè)計(jì)不同,核心在于“修改操作是否改變對(duì)象的內(nèi)存地址”。
1. 核心概念:id、value、type
在Python中,每個(gè)對(duì)象都有三個(gè)核心屬性:
- id:對(duì)象的唯一身份標(biāo)識(shí),對(duì)應(yīng)內(nèi)存地址(通過
id()函數(shù)查看); - value:對(duì)象的實(shí)際數(shù)據(jù)(如
10、"Python"、[1,2,3]); - type:對(duì)象的類型(通過
type()函數(shù)查看)。
兩類對(duì)象的關(guān)鍵差異:
- 不可變對(duì)象:
id與value綁定,value一旦確定,id就固定;修改value必須創(chuàng)建新對(duì)象(新id); - 可變對(duì)象:
id與“容器本身”綁定,value(容器內(nèi)的數(shù)據(jù))可修改,且修改后id不變。
2. 內(nèi)存存儲(chǔ)示意(直觀理解)
(1)不可變對(duì)象(以int為例)
# 初始賦值:a指向id=140708484554720的對(duì)象(value=10) a = 10 內(nèi)存:a → [id=140708484554720, value=10, type=int] # “修改”操作:創(chuàng)建新對(duì)象(id=140708484554784,value=12),a重新指向新對(duì)象 a = a + 2 內(nèi)存:a → [id=140708484554784, value=12, type=int](原對(duì)象10仍存在,等待垃圾回收)
(2)可變對(duì)象(以list為例)
# 初始賦值:lst指向id=2524607215232的列表對(duì)象(內(nèi)部存儲(chǔ)[1,2,3]) lst = [1, 2, 3] 內(nèi)存:lst → [id=2524607215232, value=[1,2,3], type=list] # 直接修改:列表內(nèi)部數(shù)據(jù)變?yōu)閇1,2,3,4],但lst仍指向原id lst.append(4) 內(nèi)存:lst → [id=2524607215232, value=[1,2,3,4], type=list](id不變,僅value修改)
3. 不可變對(duì)象的“緩存機(jī)制”(額外知識(shí)點(diǎn))
Python對(duì)部分不可變對(duì)象(如小整數(shù)、短字符串)有緩存機(jī)制,即重復(fù)創(chuàng)建相同值的對(duì)象時(shí),會(huì)復(fù)用已有的對(duì)象(避免頻繁創(chuàng)建銷毀,節(jié)省內(nèi)存)。
# 示例1:小整數(shù)緩存(范圍通常是-5~256) a = 10 b = 10 print(id(a) == id(b)) # 輸出:True(復(fù)用同一對(duì)象) c = 1000 d = 1000 print(id(c) == id(d)) # 輸出:False(超出小整數(shù)范圍,創(chuàng)建新對(duì)象) # 示例2:短字符串緩存(字符串駐留機(jī)制) s1 = "Python" s2 = "Python" print(id(s1) == id(s2)) # 輸出:True(復(fù)用同一對(duì)象) s3 = "Python123" s4 = "Python123" print(id(s3) == id(s4)) # 輸出:True(短字符串通常會(huì)被緩存) s5 = "Python " + "123" # 動(dòng)態(tài)拼接的字符串,是否緩存取決于解釋器 print(id(s5) == id(s3)) # 輸出:False(動(dòng)態(tài)拼接未觸發(fā)緩存)
注意:緩存機(jī)制是Python的內(nèi)部?jī)?yōu)化,開發(fā)者不應(yīng)依賴此特性(如不能通過id判斷兩個(gè)不可變對(duì)象的值是否相等,應(yīng)直接用==比較值)。
三、核心差異對(duì)比:從賦值、傳參到使用場(chǎng)景
兩類對(duì)象在變量賦值、函數(shù)傳參、使用場(chǎng)景上的差異,是開發(fā)中最容易踩坑的地方,用表格直觀對(duì)比:
對(duì)比維度 | 不可變對(duì)象(如int、str、tuple) | 可變對(duì)象(如list、dict、set) |
賦值邏輯 | 新賦值會(huì)創(chuàng)建新對(duì)象,變量指向新id | 新賦值僅讓變量指向原對(duì)象(共享引用),修改內(nèi)部數(shù)據(jù)會(huì)影響所有引用 |
函數(shù)傳參 | 傳“值的引用”,函數(shù)內(nèi)修改不會(huì)影響外部變量 | 傳“對(duì)象的引用”,函數(shù)內(nèi)修改會(huì)影響外部對(duì)象 |
|
|
|
安全性 | 線程安全(不可修改,無并發(fā)修改風(fēng)險(xiǎn)) | 非線程安全(多線程修改需加鎖) |
適用場(chǎng)景 | 存儲(chǔ)固定數(shù)據(jù)(如配置、常量、字典鍵) | 存儲(chǔ)動(dòng)態(tài)數(shù)據(jù)(如待處理列表、實(shí)時(shí)更新的字典) |
1. 變量賦值:共享引用 vs 獨(dú)立對(duì)象
(1)不可變對(duì)象:賦值創(chuàng)建新對(duì)象
a = 10
b = a # b指向a的對(duì)象(id相同)
print(f"a: {a}, id(a): {id(a)}; b: , id(b): {id(b)}") # 輸出:a:10, id(a):140708484554720; b:10, id(b):140708484554720
a = a + 2 # a指向新對(duì)象(id變化)
print(f"a: {a}, id(a): {id(a)}; b: , id(b): {id(b)}") # 輸出:a:12, id(a):140708484554784; b:10, id(b):140708484554720(b不受影響)(2)可變對(duì)象:賦值共享引用
lst1 = [1, 2, 3]
lst2 = lst1 # lst2與lst1指向同一對(duì)象(共享引用)
print(f"lst1: {lst1}, id(lst1): {id(lst1)}; lst2: {lst2}, id(lst2): {id(lst2)}") # 輸出:lst1:[1,2,3], id(lst1):2524607215232; lst2:[1,2,3], id(lst2):2524607215232
lst1.append(4) # 修改lst1(共享對(duì)象)
print(f"lst1: {lst1}, id(lst1): {id(lst1)}; lst2: {lst2}, id(lst2): {id(lst2)}") # 輸出:lst1:[1,2,3,4], id(lst1):2524607215232; lst2:[1,2,3,4], id(lst2):2524607215232(lst2同步變化)2. 函數(shù)傳參:傳值 vs 傳引用(Python的“傳對(duì)象引用”機(jī)制)
Python的函數(shù)傳參既不是純“傳值”,也不是純“傳引用”,而是 “傳對(duì)象引用” ——本質(zhì)是將變量指向的對(duì)象地址(id)傳給函數(shù)參數(shù),參數(shù)與原變量共享同一對(duì)象。
(1)不可變對(duì)象:函數(shù)內(nèi)修改不影響外部
def modify_immutable(x):
x = x + 10 # 創(chuàng)建新對(duì)象,參數(shù)x指向新id
print(f"函數(shù)內(nèi)x: {x}, id(x): {id(x)}")
a = 5
print(f"調(diào)用前a: {a}, id(a): {id(a)}") # 輸出:調(diào)用前a:5, id(a):140708484554560
modify_immutable(a) # 輸出:函數(shù)內(nèi)x:15, id(x):140708484554880
print(f"調(diào)用后a: {a}, id(a): {id(a)}") # 輸出:調(diào)用后a:5, id(a):140708484554560(a不受影響)(2)可變對(duì)象:函數(shù)內(nèi)修改影響外部
def modify_mutable(lst):
lst.append(10) # 直接修改共享對(duì)象
print(f"函數(shù)內(nèi)lst: {lst}, id(lst): {id(lst)}")
lst = [1, 2, 3]
print(f"調(diào)用前l(fā)st: {lst}, id(lst): {id(lst)}") # 輸出:調(diào)用前l(fā)st:[1,2,3], id(lst):2524607215232
modify_mutable(lst) # 輸出:函數(shù)內(nèi)lst:[1,2,3,10], id(lst):2524607215232
print(f"調(diào)用后lst: {lst}, id(lst): {id(lst)}") # 輸出:調(diào)用后lst:[1,2,3,10], id(lst):2524607215232(lst被修改)避坑技巧:若想避免函數(shù)修改外部可變對(duì)象,可在函數(shù)內(nèi)創(chuàng)建對(duì)象的副本(如lst.copy()、dict.copy()、copy.deepcopy()):
def modify_mutable_safe(lst):
new_lst = lst.copy() # 創(chuàng)建副本,修改副本不影響原對(duì)象
new_lst.append(10)
print(f"函數(shù)內(nèi)new_lst: {new_lst}")
lst = [1, 2, 3]
modify_mutable_safe(lst) # 輸出:函數(shù)內(nèi)new_lst: [1,2,3,10]
print(f"調(diào)用后lst: {lst}") # 輸出:調(diào)用后lst: [1,2,3](原對(duì)象未修改)四、實(shí)戰(zhàn)場(chǎng)景:如何選擇可變/不可變對(duì)象?
兩類對(duì)象沒有絕對(duì)的“優(yōu)劣”,只有“適用場(chǎng)景”的差異,開發(fā)中需根據(jù)需求選擇:
1. 優(yōu)先用不可變對(duì)象的場(chǎng)景
- 存儲(chǔ)固定不變的數(shù)據(jù):如程序常量(
PI = 3.14159)、配置參數(shù)(DB_HOST = "localhost")、字典的鍵(字典鍵必須不可變); - 多線程環(huán)境:不可變對(duì)象無需擔(dān)心并發(fā)修改問題(線程安全);
- 需要哈希的場(chǎng)景:如集合元素(集合元素必須可哈希,不可變對(duì)象通??晒#?。
# 示例:不可變對(duì)象作為字典鍵(合法)
config = {
("db", "host"): "localhost", # 元組(不可變)作為鍵
("db", "port"): 3306,
"timeout": 30 # 字符串(不可變)作為鍵
}
# 錯(cuò)誤示例:列表(可變)不能作為字典鍵
invalid_config = {
["db", "host"]: "localhost" # 報(bào)錯(cuò):TypeError: unhashable type: 'list'
}2. 優(yōu)先用可變對(duì)象的場(chǎng)景
- 存儲(chǔ)動(dòng)態(tài)變化的數(shù)據(jù):如待處理的任務(wù)列表(
tasks = [])、實(shí)時(shí)更新的用戶數(shù)據(jù)(user_info = {"name": "張三", "score": 0}); - 需要頻繁修改內(nèi)部數(shù)據(jù)的場(chǎng)景:如列表的
append()、remove(),字典的update()等操作(無需創(chuàng)建新對(duì)象,效率更高); - 傳遞復(fù)雜數(shù)據(jù)結(jié)構(gòu)并允許修改:如函數(shù)間傳遞列表,允許函數(shù)補(bǔ)充數(shù)據(jù)(需明確告知修改邏輯,避免隱藏BUG)。
# 示例:可變對(duì)象存儲(chǔ)動(dòng)態(tài)數(shù)據(jù)
user_scores = {"張三": 85, "李四": 92}
# 動(dòng)態(tài)更新分?jǐn)?shù)(無需創(chuàng)建新字典,直接修改)
user_scores["張三"] = 88 # 覆蓋原分?jǐn)?shù)
user_scores["王五"] = 79 # 新增數(shù)據(jù)
print(user_scores) # 輸出:{'張三': 88, '李四': 92, '王五': 79}
# 示例:函數(shù)間傳遞可變對(duì)象并協(xié)作修改
def add_task(tasks, task):
tasks.append(task) # 直接修改傳入的列表
task_list = ["寫代碼", "測(cè)功能"]
add_task(task_list, "修復(fù)BUG")
print(task_list) # 輸出:['寫代碼', '測(cè)功能', '修復(fù)BUG'](原列表被更新)五、避坑指南:這些場(chǎng)景最容易出錯(cuò)!
因?qū)勺?不可變對(duì)象理解不清導(dǎo)致的BUG,在開發(fā)中非常常見,以下是高頻坑點(diǎn)及解決方案:
1. 坑點(diǎn)1:可變對(duì)象作為函數(shù)默認(rèn)參數(shù)
問題:函數(shù)默認(rèn)參數(shù)在定義時(shí)僅初始化一次,若默認(rèn)參數(shù)是可變對(duì)象(如列表、字典),多次調(diào)用函數(shù)會(huì)共享該對(duì)象,導(dǎo)致意外累積數(shù)據(jù)。
# 錯(cuò)誤示例:用列表作為默認(rèn)參數(shù)
def add_item(item, lst=[]): # lst在函數(shù)定義時(shí)初始化一次
lst.append(item)
return lst
print(add_item(1)) # 輸出:[1](首次調(diào)用正常)
print(add_item(2)) # 輸出:[1, 2](二次調(diào)用復(fù)用了同一個(gè)列表,意外累積)解決方案:默認(rèn)參數(shù)用None代替可變對(duì)象,在函數(shù)內(nèi)初始化:
def add_item(item, lst=None):
if lst is None:
lst = [] # 每次調(diào)用時(shí)重新初始化列表
lst.append(item)
return lst
print(add_item(1)) # 輸出:[1]
print(add_item(2)) # 輸出:[2](符合預(yù)期)2. 坑點(diǎn)2:誤修改共享的可變對(duì)象
問題:多個(gè)變量引用同一可變對(duì)象時(shí),修改其中一個(gè)變量會(huì)影響其他變量,導(dǎo)致數(shù)據(jù)不一致。
# 問題場(chǎng)景:復(fù)制列表時(shí)直接賦值(共享引用)
user_list = ["張三", "李四", "王五"]
admin_list = user_list # admin_list與user_list指向同一列表
admin_list.remove("王五") # 修改admin_list
print(user_list) # 輸出:['張三', '李四'](user_list也被修改,可能非預(yù)期)解決方案:創(chuàng)建可變對(duì)象的副本(淺拷貝/深拷貝),避免共享引用:
# 方案1:淺拷貝(適用于元素為不可變對(duì)象的情況)
user_list = ["張三", "李四", "王五"]
admin_list = user_list.copy() # 列表專用拷貝
# 或 admin_list = list(user_list)
# 或 admin_list = user_list[:]
admin_list.remove("王五")
print(user_list) # 輸出:['張三', '李四', '王五'](原列表不受影響)
# 方案2:深拷貝(適用于嵌套可變對(duì)象的情況,需用copy模塊)
import copy
nested_list = [1, [2, 3], 4]
shallow_copy = nested_list.copy() # 淺拷貝:內(nèi)層列表仍共享
deep_copy = copy.deepcopy(nested_list) # 深拷貝:完全獨(dú)立
shallow_copy[1].append(5)
print(nested_list) # 輸出:[1, [2, 3, 5], 4](淺拷貝影響原對(duì)象)
deep_copy[1].append(6)
print(nested_list) # 輸出:[1, [2, 3, 5], 4](深拷貝不影響原對(duì)象)3. 坑點(diǎn)3:混淆“不可變對(duì)象的修改”與“重新賦值”
問題:試圖直接修改不可變對(duì)象(如字符串、元組)會(huì)報(bào)錯(cuò),需通過重新賦值生成新對(duì)象。
# 錯(cuò)誤示例:直接修改字符串 s = "Python" s[0] = "p" # 報(bào)錯(cuò):TypeError: 'str' object does not support item assignment # 錯(cuò)誤示例:直接修改元組 t = (1, 2, 3) t[0] = 100 # 報(bào)錯(cuò):TypeError: 'tuple' object does not support item assignment
解決方案:通過拼接、切片等方式生成新對(duì)象,再重新賦值:
# 正確:字符串重新賦值 s = "Python" s = "p" + s[1:] # 生成新字符串 print(s) # 輸出:python # 正確:元組重新賦值(通過切片生成新元組) t = (1, 2, 3) t = (100,) + t[1:] # 生成新元組 print(t) # 輸出:(100, 2, 3)
六、總結(jié):理解本質(zhì),靈活運(yùn)用
可變對(duì)象與不可變對(duì)象的核心差異,在于“修改操作是否改變對(duì)象的內(nèi)存地址(id)”:
- 不可變對(duì)象:修改即創(chuàng)建新對(duì)象(id改變),適合存儲(chǔ)固定數(shù)據(jù),線程安全;
- 可變對(duì)象:修改不改變id,適合存儲(chǔ)動(dòng)態(tài)數(shù)據(jù),操作高效但需注意共享引用問題。
掌握這一本質(zhì)后,就能理解:
- 為什么函數(shù)傳參時(shí),列表會(huì)被修改而整數(shù)不會(huì);
- 為什么字典的鍵必須是不可變對(duì)象;
- 為什么默認(rèn)參數(shù)不能用可變對(duì)象。
實(shí)際開發(fā)中,沒有“必須用哪種”的絕對(duì)規(guī)則,關(guān)鍵是根據(jù)場(chǎng)景選擇:需要固定數(shù)據(jù)用不可變對(duì)象,需要?jiǎng)討B(tài)修改用可變對(duì)象,并注意規(guī)避共享引用導(dǎo)致的意外修改。
到此這篇關(guān)于python可變/不可變對(duì)象及+=和=+的文章就介紹到這了,更多相關(guān)python可變/不可變對(duì)象及+=和=+內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用python模塊plotdigitizer摳取論文圖片中的數(shù)據(jù)實(shí)例詳解
這篇文章主要介紹了使用python模塊plotdigitizer摳取論文圖片中的數(shù)據(jù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
OpenCV Python實(shí)現(xiàn)拼圖小游戲
這篇文章主要為大家詳細(xì)介紹了OpenCV Python實(shí)現(xiàn)拼圖版小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03
Python腳本實(shí)現(xiàn)一鍵執(zhí)行MySQL與達(dá)夢(mèng)數(shù)據(jù)庫(kù)的SQL
這篇文章主要為大家詳細(xì)介紹了如何使用Python 腳本 + PyInstaller 打包成可執(zhí)行文件,一鍵搞定 MySQL 和達(dá)夢(mèng)(DM)數(shù)據(jù)庫(kù)的 SQL 執(zhí)行,希望對(duì)大家有所幫助2025-08-08
Python將文件映射到內(nèi)存使用mmap.mmap()函數(shù)方式
這篇文章主要介紹了Python將文件映射到內(nèi)存使用mmap.mmap()函數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02
python調(diào)用另外一個(gè)py文件中函數(shù)的具體步驟
這篇文章主要給大家介紹了關(guān)于python調(diào)用另外一個(gè)py文件中函數(shù)的具體步驟,要在一個(gè)Python文件中調(diào)用其他Python文件中的方法,可以使用Python的模塊導(dǎo)入功能,需要的朋友可以參考下2023-11-11
如何利用Python提取pdf中的表格數(shù)據(jù)(附實(shí)戰(zhàn)案例)
從PDF表格中獲取數(shù)據(jù)是一項(xiàng)痛苦的工作,下面這篇文章主要給大家介紹了關(guān)于如何利用Python提取pdf中的表格數(shù)據(jù)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11

