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

