Python 变量赋值陷阱:浅拷贝与深拷贝解析
讲解 Python 变量赋值的引用传递本质,通过 id() 函数展示内存地址。区分不可变对象与可变对象的赋值差异,重点阐述浅拷贝仅复制外层而内层共享的陷阱,以及深拷贝递归复制所有层级的解决方案。提供四种浅拷贝实现方式及性能对比,总结实战中函数参数、配置文件修改等场景的正确拷贝策略,帮助开发者避免数据污染 bug。

讲解 Python 变量赋值的引用传递本质,通过 id() 函数展示内存地址。区分不可变对象与可变对象的赋值差异,重点阐述浅拷贝仅复制外层而内层共享的陷阱,以及深拷贝递归复制所有层级的解决方案。提供四种浅拷贝实现方式及性能对比,总结实战中函数参数、配置文件修改等场景的正确拷贝策略,帮助开发者避免数据污染 bug。

你是否遇到过这样的困惑:明明只修改了列表 b,却发现列表 a 的值也跟着变了?在 Python 中,这不是 bug,而是变量赋值的'底层逻辑'导致的——Python 的变量本质是'对象的引用'(类似标签),赋值操作 a = b 不是复制数据,而是给同一块内存里的对象贴了两个标签。
这种'引用传递'的特性,在处理整数、字符串等不可变对象时影响不大,但在处理列表、字典等可变对象时,很容易引发'牵一发而动全身'的隐性 bug。本文将通过 id() 函数可视化内存地址,从'赋值本质→浅拷贝局限→深拷贝解决方案'层层拆解,结合实战案例帮你避开拷贝陷阱,精准控制数据独立性。所有代码基于 Python 3.13.6 测试,可直接复现。
在 Python 中,'变量'和'数据'是分离的——数据(如列表、整数)存放在内存中,变量只是指向这片内存的'引用'(类似地址标签)。赋值操作 a = b 的核心是'让 a 和 b 指向同一片内存',而非'把 b 的数据复制给 a'。
id() 函数看穿内存地址id(object) 是 Python 的内置函数,返回对象的唯一内存地址标识符(整数)。通过比较两个变量的 id,就能判断它们是否指向同一个对象。
不可变对象(整数、字符串、元组等)的核心特点是'数据创建后无法修改'——若要'修改',本质是创建新对象并让变量指向新内存。因此,不可变对象的赋值不会出现'改一个影响另一个'的问题。
# 示例 1:整数(不可变)
x = 10
y = x # y 和 x 指向同一块内存(存储 10 的地址)
print(f"赋值后:x 的地址={id(x)}, y 的地址={id(y)}") # 输出相同地址,如 2898567296528
# '修改'y:实际是创建新对象(存储 20),y 指向新地址
y = 20
print(f"修改后:x 的地址={id(x)}, y 的地址={id(y)}") # x 地址不变,y 地址变化
print(f"x 的值={x}, y 的值={y}") # 输出:x=10, y=20(x 不受影响)
# 示例 2:字符串(不可变)
s1 = "hello"
s2 = s1 # s2 和 s1 指向同一字符串
print(f"赋值后:s1 地址={id(s1)}, s2 地址={id(s2)}") # 地址相同
# '修改's2:创建新字符串"hello world",s2 指向新地址
s2 += " world"
print(f"修改后:s1={s1}, s2={s2}") # 输出:s1=hello, s2=hello world(s1 不受影响)
关键原理:不可变对象的'修改'本质是'创建新对象',原变量仍指向旧对象,因此不会相互影响。
可变对象(列表、字典、集合等)的核心特点是'数据可直接修改'——修改操作会直接改变内存中的数据,而非创建新对象。因此,若两个变量指向同一个可变对象,修改其中一个会同步影响另一个。
# 示例 1:列表(可变)
a = [1, 2, 3]
b = a # a 和 b 指向同一块内存(存储列表 [1,2,3] 的地址)
print(f"赋值后:a 地址={id(a)}, b 地址={id(b)}") # 地址相同,如 2451458888256
# 修改 b 的元素:直接修改内存中的列表数据
b[0] = 100 # 改变列表第一个元素的值
print(f"修改后:a={a}, b={b}") # 输出:a=[100,2,3], b=[100,2,3](a 同步变化)
print(f"修改后:a 地址={id(a)}, b 地址={id(b)}") # 地址仍相同(未创建新对象)
# 示例 2:字典(可变)
dict1 = {"name": "Alice", "age": 25}
dict2 = dict1 # 指向同一字典
dict2["age"] = 26 # 修改 dict2 的 age 字段
print(f"dict1={dict1}, dict2={dict2}") # 输出:dict1={"name":"Alice","age":26}, dict2=...(同步变化)
致命陷阱:新手常误以为 b = a 是'复制列表',实际只是'复制引用'——a 和 b 是同一列表的'两个名字',改一个必然影响另一个。
Python 为优化性能,对部分不可变对象做了'缓存复用',导致看似'不同对象'却指向同一内存,这是赋值逻辑的'例外情况',但不影响核心原理。
-5~256 范围内的整数,Python 会提前创建并缓存,所有赋值都指向同一对象;# 小整数池示例:256 以内的整数复用内存
x = 100
y = 100
print(id(x) == id(y)) # 输出:True(指向同一对象)
x = 300 # 超出小整数池范围
y = 300
print(id(x) == id(y)) # 输出:False(创建两个不同对象)
# 字符串驻留示例:纯字母数字字符串复用
s1 = "python123"
s2 = "python123"
print(id(s1) == id(s2)) # 输出:True(复用缓存)
s1 = "python 123" # 含空格,不满足驻留条件
s2 = "python 123"
print(id(s1) == id(s2)) # 输出:False(创建新对象)
注意:这是 Python 的优化细节,不改变'不可变对象赋值无副作用'的核心结论——即使 x 和 y 指向同一对象,'修改'时仍会创建新对象。
为解决'可变对象赋值同步变化'的问题,需要复制对象本身而非引用。浅拷贝是最常用的拷贝方式,它会创建一个'新的外层对象',但内层嵌套的可变对象仍共享引用——相当于'复制了壳子,没复制里面的内容'。
Python 中针对不同对象,有多种浅拷贝方法,核心效果一致:
| 对象类型 | 浅拷贝方法 | 示例 |
|---|---|---|
| 列表 | 1. list.copy()2. 切片 a[:]<br>3. list(a)` | a = [1,2,3]; b = a.copy() |
| 字典 | 1. dict.copy()2. dict(a) | a = {"k":1}; b = a.copy() |
| 集合 | 1. set.copy()2. set(a) | a = {1,2}; b = a.copy() |
| 通用对象 | copy 模块的 copy() 函数 | import copy; b = copy.copy(a) |
import copy
# 原始列表(含嵌套列表,模拟'外层 + 内层'结构)
a = [1, 2, [3, 4]] # 外层:[1,2, 内层列表];内层:[3,4]
# 方法 1:list.copy()
b = a.copy()
# 方法 2:切片(最简洁,推荐)
c = a[:]
# 方法 3:list() 构造函数
d = list(a)
# 方法 4:copy 模块的 copy()(通用)
e = copy.copy(a)
# 验证:外层对象是新的(地址不同)
print(f"原列表 a 地址:{id(a)}")
print(f"拷贝后 b 地址:{id(b)},与 a 是否相同:{id(b) == id(a)}") # 输出:False
print(f"拷贝后 c 地址:{id(c)},与 a 是否相同:{id(c) == id(a)}") # 输出:False
浅拷贝仅复制'外层对象',对于内层嵌套的可变对象(如列表中的列表、字典中的列表),新对象和原对象仍共享引用——修改内层数据,两边会同步变化,这是浅拷贝最容易被忽略的问题。
import copy
# 原始列表:外层列表 + 内层嵌套列表(可变对象)
a = [1, 2, [3, 4]]
b = a.copy() # 浅拷贝
# 场景 1:修改外层元素(互不影响)
b[0] = 100 # 修改 b 的外层元素(索引 0)
print(f"a 的外层:{a[0]},b 的外层:{b[0]}") # 输出:a=1,b=100(外层独立)
print(f"a 的完整列表:{a},b 的完整列表:{b}") # 输出:a=[1,2,[3,4]], b=[100,2,[3,4]]
# 场景 2:修改内层嵌套列表(同步变化)
b[2][0] = 300 # 修改 b 的内层列表(索引 2 是内层列表,再改索引 0)
print(f"\na 的内层列表:{a[2]},b 的内层列表:{b[2]}") # 输出:a=[300,4], b=[300,4](同步变化)
print(f"a 的完整列表:{a},b 的完整列表:{b}") # 输出:a=[1,2,[300,4]], b=[100,2,[300,4]]
# 验证内层地址:a 和 b 的内层列表指向同一内存
print(f"\na 的内层列表地址:{id(a[2])},b 的内层列表地址:{id(b[2])}") # 地址相同
原理图解:
a 和 b 是两个不同的外层列表(地址不同);a[2] 和 b[2] 指向同一个内层列表(地址相同),因此修改内层会联动。浅拷贝并非'没用',以下场景下优先使用浅拷贝(性能比深拷贝高):
[1,2,3]、单层字典 {"k1":1, "k2":2}——无内层可变对象,浅拷贝后完全独立;[1, "hello", (3,4)]——内层元组是不可变对象,即使共享引用,也无法修改,因此安全;# 适用场景 1:单层列表(无嵌套)
a = [1, 2, 3]
b = a.copy()
b.append(4) # 仅修改外层
print(f"a={a}, b={b}") # 输出:a=[1,2,3], b=[1,2,3,4](完全独立)
# 适用场景 2:内层是不可变对象(元组)
a = [1, "hi", (3,4)]
b = a.copy()
b[2] = (5,6) # '修改'内层元组:实际创建新元组,不影响 a
print(f"a={a}, b={b}") # 输出:a=[1,"hi",(3,4)], b=[1,"hi",(5,6)](安全)
当对象包含多层嵌套的可变对象(如 [1, [2, [3,4]]]、{"db": {"host": "localhost", "port": 3306}})时,浅拷贝的'内层共享'问题会导致数据混乱,此时需要深拷贝——递归复制所有层级的对象,新对象与原对象完全独立,修改任何层级都不会相互影响。
copy.deepcopy()深拷贝仅有一种通用实现方式:copy 模块的 deepcopy() 函数,它会自动递归处理所有嵌套层级,无论多少层可变对象,都能完全复制。
import copy
# 复杂嵌套对象:列表→字典→列表(多层可变对象)
a = [
1,
{"name": "Alice", "hobbies": ["reading", "coding"]}, # 内层字典 + 列表
[5, 6, [7, 8]] # 内层列表嵌套列表
]
# 深拷贝
b = copy.deepcopy(a)
# 验证:所有层级的地址均不同(完全独立)
print(f"外层地址:a={id(a)}, b={id(b)} → 不同") # 外层不同
print(f"内层字典地址:a[1]={id(a[1])}, b[1]={id(b[1])} → 不同") # 字典不同
print(f"字典内列表地址:a[1]['hobbies']={id(a[1]['hobbies'])}, b[1]['hobbies']={id(b[1]['hobbies'])} → 不同") # 列表不同
print(f"深层列表地址:a[2][2]={id(a[2][2])}, b[2][2]={id(b[2][2])} → 不同") # 深层列表不同
# 修改任意层级:均不影响原对象
b[0] = 100 # 修改外层
b[1]["name"] = "Bob" # 修改内层字典
b[1]["hobbies"].append("running") # 修改字典内的列表
b[2][2][0] = 700 # 修改深层列表
# 对比原对象和深拷贝对象
print(f"\n原对象 a:{a}")
print(f"深拷贝对象 b:{b}") # 输出结果:a 的所有值未变,b 的修改完全独立
核心效果:深拷贝后,a 和 b 是'两个完全无关的对象',无论嵌套多少层,修改其中一个都不会影响另一个。
深拷贝的'完全独立'是有代价的——它需要递归遍历所有层级并复制,因此比浅拷贝慢,且消耗更多内存。数据越复杂、嵌套越深,性能差异越明显。
import copy
import time
# 构建复杂嵌套数据(1000 个内层列表,每层含 10 个元素)
complex_data = []
for i in range(1000):
complex_data.append([j for j in range(10)]) # 外层列表 +1000 个内层列表
# 测试浅拷贝耗时
start = time.time()
shallow_copy = copy.copy(complex_data)
shallow_time = time.time() - start
# 测试深拷贝耗时
start = time.time()
deep_copy = copy.deepcopy(complex_data)
deep_time = time.time() - start
# 输出结果(单位:秒)
print(f"浅拷贝耗时:{shallow_time:.6f}") # 约 0.0001 秒
print(f"深拷贝耗时:{deep_time:.6f}") # 约 0.01 秒(慢 100 倍)
print(f"深拷贝比浅拷贝慢约{int(deep_time/shallow_time)}倍")
性能结论:
为了更直观区分,我们用'多层嵌套字典'作为测试对象,对比赋值、浅拷贝、深拷贝的效果差异:
import copy
# 原始数据:多层嵌套字典(模拟配置文件场景)
original = {
"app": "PythonCopyDemo",
"settings": {
"log": {
"level": "INFO",
"path": "./logs"
},
"timeout": [30, 60] # 内层可变列表
}
}
# 1. 赋值(引用传递)
assign_copy = original
# 2. 浅拷贝
shallow_copy = copy.copy(original)
# 3. 深拷贝
deep_copy = copy.deepcopy(original)
# 修改原始数据的 3 个层级
original["app"] = "ModifiedApp" # 层级 1:外层字符串(不可变)
original["settings"]["log"]["level"] = "DEBUG" # 层级 3:深层字典(可变)
original["settings"]["timeout"][0] = 10 # 层级 2:内层列表(可变)
# 对比结果
print("=== 1. 赋值(引用传递)===")
print(f"assign_copy['app']: {assign_copy['app']} → 同步修改(同对象)")
print(f"assign_copy['settings']['log']['level']: {assign_copy['settings']['log']['level']} → 同步修改")
print(f"assign_copy['settings']['timeout'][0]: {assign_copy['settings']['timeout'][0]} → 同步修改")
print("\n=== 2. 浅拷贝 ===")
print(f"shallow_copy['app']: {shallow_copy['app']} → 未修改(外层字符串不可变,创建新对象)")
print(f"shallow_copy['settings']['log']['level']: {shallow_copy['settings']['log']['level']} → 同步修改(内层共享)")
print(f"shallow_copy['settings']['timeout'][0]: {shallow_copy['settings']['timeout'][0]} → 同步修改(内层共享)")
print("\n=== 3. 深拷贝 ===")
print(f"deep_copy['app']: {deep_copy['app']} → 未修改")
print(f"deep_copy['settings']['log']['level']: {deep_copy['settings']['log']['level']} → 未修改(完全独立)")
print(f"deep_copy['settings']['timeout'][0]: {deep_copy['settings']['timeout'][0]} → 未修改(完全独立)")
| 特性维度 | 赋值(引用传递) | 浅拷贝(copy()) | 深拷贝(deepcopy()) |
|---|---|---|---|
| 内存地址 | 与原对象完全相同 | 外层不同,内层相同 | 所有层级均不同 |
| 修改外层可变元素 | 原对象同步变化 | 原对象不变 | 原对象不变 |
| 修改内层可变元素 | 原对象同步变化 | 原对象同步变化 | 原对象不变 |
| 性能开销 | 无(仅复制引用) | 小(仅复制外层) | 大(递归复制所有层级) |
| 适用场景 | 仅读数据,不修改 | 单层对象/内层不可变 | 多层嵌套可变对象 |
| 典型案例 | 函数传参(仅读) | 单层列表去重 | 嵌套配置文件修改 |
函数传参本质是'引用传递',若参数是可变对象,直接修改会影响外部数据。此时需根据对象复杂度选择浅拷贝或深拷贝。
import copy
def safe_modify(data):
# 若 data 是单层对象,用浅拷贝
# data_copy = data.copy()
# 若 data 是嵌套对象,用深拷贝
data_copy = copy.deepcopy(data)
data_copy.append("modified") # 修改拷贝后的对象
return data_copy
# 测试嵌套列表
original = [1, 2, [3, 4]]
modified = safe_modify(original)
print(f"原列表:{original} → 未修改") # 输出:[1,2,[3,4]]
print(f"修改后列表:{modified} → 已修改") # 输出:[1,2,[3,4],"modified"]
项目中常需基于'默认配置'修改个性化配置,若直接赋值会污染默认配置,需用深拷贝。
import copy
# 默认配置(多层嵌套)
DEFAULT_CONFIG = {
"db": {
"host": "localhost",
"port": 3306,
"params": {"charset": "utf8"}
},
"timeout": 30
}
# 个性化配置:基于默认配置修改,不污染原配置
user_config = copy.deepcopy(DEFAULT_CONFIG)
user_config["db"]["host"] = "192.168.1.100" # 修改数据库地址
user_config["db"]["params"]["charset"] = "utf8mb4" # 修改内层参数
print(f"默认配置 db.host:{DEFAULT_CONFIG['db']['host']} → 仍为 localhost")
print(f"用户配置 db.host:{user_config['db']['host']} → 192.168.1.100")
列表去重无需修改内层数据,用浅拷贝即可,性能更高。
def deduplicate(lst):
# 浅拷贝:先复制列表,再去重(用集合去重后转列表)
return list(set(lst.copy()))
original = [1, 2, 2, 3, 3, 3]
unique_lst = deduplicate(original)
print(f"原列表:{original} → 未修改")
print(f"去重后列表:{unique_lst} → [1,2,3]")
若数据量大且仅需修改某一层级,手动复制该层级比深拷贝更高效(避免递归复制所有数据)。
# 复杂数据:外层列表 +1000 个内层字典(仅需修改第 1 个内层字典)
big_data = [{"id": i, "value": i*10} for i in range(1000)]
# 手动部分拷贝:仅复制需要修改的内层字典,其他共享(性能高)
modified_data = big_data.copy() # 浅拷贝外层
modified_data[0] = {"id": 0, "value": 999} # 替换第 1 个内层字典(创建新对象)
print(f"原数据第 1 个元素:{big_data[0]} → 未修改") # 输出:{"id":0,"value":0}
print(f"修改后第 1 个元素:{modified_data[0]} → 已修改") # 输出:{"id":0,"value":999}
函数默认参数若为可变对象(如 def func(lst=[])),会导致多次调用共享同一对象,需用 None+ 深拷贝规避。
import copy
# 错误写法:默认参数是可变对象,多次调用共享
def add_item_wrong(item, lst=[]):
lst.append(item)
return lst
print(add_item_wrong(1)) # 输出:[1]
print(add_item_wrong(2)) # 输出:[1,2](错误:共享列表)
# 正确写法:用 None+ 深拷贝,每次调用创建新对象
def add_item_correct(item, lst=None):
if lst is None:
lst = []
lst_copy = copy.deepcopy(lst) # 若 lst 是嵌套对象,用深拷贝
lst_copy.append(item)
return lst_copy
print(add_item_correct(1)) # 输出:[1]
print(add_item_correct(2)) # 输出:[2](正确:独立列表)
遇到'是否需要拷贝'的问题时,按以下 3 步决策,可避免 99% 的陷阱:
copy()/切片,性能高);deepcopy(),完全独立)。最终口诀:
'只读不拷,单层浅拷,嵌套深拷,量大手拷'
通过理解变量的'引用本质'和拷贝的'层级差异',你就能精准控制数据的独立性,避开'改一个影响另一个'的隐性 bug,写出更健壮、更高效的 Python 代码。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online