跳到主要内容Python 变量赋值陷阱:浅拷贝与深拷贝解析 | 极客日志Python
Python 变量赋值陷阱:浅拷贝与深拷贝解析
讲解 Python 变量赋值的引用传递本质,通过 id() 函数展示内存地址。区分不可变对象与可变对象的赋值差异,重点阐述浅拷贝仅复制外层而内层共享的陷阱,以及深拷贝递归复制所有层级的解决方案。提供四种浅拷贝实现方式及性能对比,总结实战中函数参数、配置文件修改等场景的正确拷贝策略,帮助开发者避免数据污染 bug。
人间失格36 浏览 引言:为什么改了 b,a 也跟着变?
你是否遇到过这样的困惑:明明只修改了列表 b,却发现列表 a 的值也跟着变了?在 Python 中,这不是 bug,而是变量赋值的'底层逻辑'导致的——Python 的变量本质是'对象的引用'(类似标签),赋值操作 a = b 不是复制数据,而是给同一块内存里的对象贴了两个标签。
这种'引用传递'的特性,在处理整数、字符串等不可变对象时影响不大,但在处理列表、字典等可变对象时,很容易引发'牵一发而动全身'的隐性 bug。本文将通过 id() 函数可视化内存地址,从'赋值本质→浅拷贝局限→深拷贝解决方案'层层拆解,结合实战案例帮你避开拷贝陷阱,精准控制数据独立性。所有代码基于 Python 3.13.6 测试,可直接复现。
1. 赋值的本质:不是值传递,而是引用传递
在 Python 中,'变量'和'数据'是分离的——数据(如列表、整数)存放在内存中,变量只是指向这片内存的'引用'(类似地址标签)。赋值操作 a = b 的核心是'让 a 和 b 指向同一片内存',而非'把 b 的数据复制给 a'。
1.1 用 id() 函数看穿内存地址
id(object) 是 Python 的内置函数,返回对象的唯一内存地址标识符(整数)。通过比较两个变量的 id,就能判断它们是否指向同一个对象。
场景 1:不可变对象的赋值(无副作用)
不可变对象(整数、字符串、元组等)的核心特点是'数据创建后无法修改'——若要'修改',本质是创建新对象并让变量指向新内存。因此,不可变对象的赋值不会出现'改一个影响另一个'的问题。
x = 10
y = x
print(f"赋值后:x 的地址={id(x)}, y 的地址={id(y)}")
y = 20
print(f"修改后:x 的地址={id(x)}, y 的地址={id(y)}")
print(f"x 的值={x}, y 的值={y}")
s1 = "hello"
s2 = s1
()
s2 +=
()
print
f"赋值后:s1 地址={id(s1)}, s2 地址={id(s2)}"
" world"
print
f"修改后:s1={s1}, s2={s2}"
关键原理:不可变对象的'修改'本质是'创建新对象',原变量仍指向旧对象,因此不会相互影响。
场景 2:可变对象的赋值(有副作用)
可变对象(列表、字典、集合等)的核心特点是'数据可直接修改'——修改操作会直接改变内存中的数据,而非创建新对象。因此,若两个变量指向同一个可变对象,修改其中一个会同步影响另一个。
a = [1, 2, 3]
b = a
print(f"赋值后:a 地址={id(a)}, b 地址={id(b)}")
b[0] = 100
print(f"修改后:a={a}, b={b}")
print(f"修改后:a 地址={id(a)}, b 地址={id(b)}")
dict1 = {"name": "Alice", "age": 25}
dict2 = dict1
dict2["age"] = 26
print(f"dict1={dict1}, dict2={dict2}")
致命陷阱:新手常误以为 b = a 是'复制列表',实际只是'复制引用'——a 和 b 是同一列表的'两个名字',改一个必然影响另一个。
1.2 不可变对象的'特殊情况':小整数池与字符串驻留
Python 为优化性能,对部分不可变对象做了'缓存复用',导致看似'不同对象'却指向同一内存,这是赋值逻辑的'例外情况',但不影响核心原理。
- 小整数池:对
-5~256 范围内的整数,Python 会提前创建并缓存,所有赋值都指向同一对象;
- 字符串驻留:对纯字母、数字、下划线组成的短字符串,Python 会缓存并复用。
x = 100
y = 100
print(id(x) == id(y))
x = 300
y = 300
print(id(x) == id(y))
s1 = "python123"
s2 = "python123"
print(id(s1) == id(s2))
s1 = "python 123"
s2 = "python 123"
print(id(s1) == id(s2))
注意:这是 Python 的优化细节,不改变'不可变对象赋值无副作用'的核心结论——即使 x 和 y 指向同一对象,'修改'时仍会创建新对象。
2. 浅拷贝(Shallow Copy):只复制'外层壳子'
为解决'可变对象赋值同步变化'的问题,需要复制对象本身而非引用。浅拷贝是最常用的拷贝方式,它会创建一个'新的外层对象',但内层嵌套的可变对象仍共享引用——相当于'复制了壳子,没复制里面的内容'。
2.1 浅拷贝的 4 种实现方式
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]]
b = a.copy()
c = a[:]
d = list(a)
e = copy.copy(a)
print(f"原列表 a 地址:{id(a)}")
print(f"拷贝后 b 地址:{id(b)},与 a 是否相同:{id(b) == id(a)}")
print(f"拷贝后 c 地址:{id(c)},与 a 是否相同:{id(c) == id(a)}")
2.2 浅拷贝的'隐形陷阱':内层对象仍共享
浅拷贝仅复制'外层对象',对于内层嵌套的可变对象(如列表中的列表、字典中的列表),新对象和原对象仍共享引用——修改内层数据,两边会同步变化,这是浅拷贝最容易被忽略的问题。
代码演示:浅拷贝的内层共享问题
import copy
a = [1, 2, [3, 4]]
b = a.copy()
b[0] = 100
print(f"a 的外层:{a[0]},b 的外层:{b[0]}")
print(f"a 的完整列表:{a},b 的完整列表:{b}")
b[2][0] = 300
print(f"\na 的内层列表:{a[2]},b 的内层列表:{b[2]}")
print(f"a 的完整列表:{a},b 的完整列表:{b}")
print(f"\na 的内层列表地址:{id(a[2])},b 的内层列表地址:{id(b[2])}")
- 浅拷贝后,
a 和 b 是两个不同的外层列表(地址不同);
- 但
a[2] 和 b[2] 指向同一个内层列表(地址相同),因此修改内层会联动。
2.3 浅拷贝的适用场景
浅拷贝并非'没用',以下场景下优先使用浅拷贝(性能比深拷贝高):
- 对象无嵌套:如单层列表
[1,2,3]、单层字典 {"k1":1, "k2":2}——无内层可变对象,浅拷贝后完全独立;
- 内层是不可变对象:如列表
[1, "hello", (3,4)]——内层元组是不可变对象,即使共享引用,也无法修改,因此安全;
- 仅需修改外层:如仅需添加/删除外层元素,不碰内层数据。
a = [1, 2, 3]
b = a.copy()
b.append(4)
print(f"a={a}, b={b}")
a = [1, "hi", (3,4)]
b = a.copy()
b[2] = (5,6)
print(f"a={a}, b={b}")
3. 深拷贝(Deep Copy):复制'所有层级'的完全独立
当对象包含多层嵌套的可变对象(如 [1, [2, [3,4]]]、{"db": {"host": "localhost", "port": 3306}})时,浅拷贝的'内层共享'问题会导致数据混乱,此时需要深拷贝——递归复制所有层级的对象,新对象与原对象完全独立,修改任何层级都不会相互影响。
3.1 深拷贝的实现: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 是'两个完全无关的对象',无论嵌套多少层,修改其中一个都不会影响另一个。
3.2 深拷贝的性能代价:递归复制的开销
深拷贝的'完全独立'是有代价的——它需要递归遍历所有层级并复制,因此比浅拷贝慢,且消耗更多内存。数据越复杂、嵌套越深,性能差异越明显。
代码示例:浅拷贝 vs 深拷贝的性能对比
import copy
import time
complex_data = []
for i in range(1000):
complex_data.append([j for j in range(10)])
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}")
print(f"深拷贝耗时:{deep_time:.6f}")
print(f"深拷贝比浅拷贝慢约{int(deep_time/shallow_time)}倍")
- 简单数据:浅拷贝和深拷贝性能差异可忽略;
- 复杂嵌套数据:深拷贝耗时是浅拷贝的 10~100 倍,需谨慎使用。
4. 浅拷贝 vs 深拷贝:3 分钟看懂核心区别
为了更直观区分,我们用'多层嵌套字典'作为测试对象,对比赋值、浅拷贝、深拷贝的效果差异:
4.1 对比实验:修改不同层级的数据
import copy
original = {
"app": "PythonCopyDemo",
"settings": {
"log": {
"level": "INFO",
"path": "./logs"
},
"timeout": [30, 60]
}
}
assign_copy = original
shallow_copy = copy.copy(original)
deep_copy = copy.deepcopy(original)
original["app"] = "ModifiedApp"
original["settings"]["log"]["level"] = "DEBUG"
original["settings"]["timeout"][0] = 10
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]} → 未修改(完全独立)")
4.2 核心区别总结表
| 特性维度 | 赋值(引用传递) | 浅拷贝(copy()) | 深拷贝(deepcopy()) |
|---|
| 内存地址 | 与原对象完全相同 | 外层不同,内层相同 | 所有层级均不同 |
| 修改外层可变元素 | 原对象同步变化 | 原对象不变 | 原对象不变 |
| 修改内层可变元素 | 原对象同步变化 | 原对象同步变化 | 原对象不变 |
| 性能开销 | 无(仅复制引用) | 小(仅复制外层) | 大(递归复制所有层级) |
| 适用场景 | 仅读数据,不修改 | 单层对象/内层不可变 | 多层嵌套可变对象 |
| 典型案例 | 函数传参(仅读) | 单层列表去重 | 嵌套配置文件修改 |
5. 实战避坑:5 个高频场景的正确拷贝方式
场景 1:函数参数避免修改外部数据
函数传参本质是'引用传递',若参数是可变对象,直接修改会影响外部数据。此时需根据对象复杂度选择浅拷贝或深拷贝。
import copy
def safe_modify(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} → 未修改")
print(f"修改后列表:{modified} → 已修改")
场景 2:配置文件的个性化修改
项目中常需基于'默认配置'修改个性化配置,若直接赋值会污染默认配置,需用深拷贝。
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")
场景 3:列表去重(单层对象,浅拷贝足够)
列表去重无需修改内层数据,用浅拷贝即可,性能更高。
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]")
场景 4:性能敏感场景的'手动部分拷贝'
若数据量大且仅需修改某一层级,手动复制该层级比深拷贝更高效(避免递归复制所有数据)。
big_data = [{"id": i, "value": i*10} for i in range(1000)]
modified_data = big_data.copy()
modified_data[0] = {"id": 0, "value": 999}
print(f"原数据第 1 个元素:{big_data[0]} → 未修改")
print(f"修改后第 1 个元素:{modified_data[0]} → 已修改")
场景 5:避免'默认参数陷阱'
函数默认参数若为可变对象(如 def func(lst=[])),会导致多次调用共享同一对象,需用 None+ 深拷贝规避。
import copy
def add_item_wrong(item, lst=[]):
lst.append(item)
return lst
print(add_item_wrong(1))
print(add_item_wrong(2))
def add_item_correct(item, lst=None):
if lst is None:
lst = []
lst_copy = copy.deepcopy(lst)
lst_copy.append(item)
return lst_copy
print(add_item_correct(1))
print(add_item_correct(2))
总结:3 步选择正确的拷贝方式
遇到'是否需要拷贝'的问题时,按以下 3 步决策,可避免 99% 的陷阱:
- 判断是否需要修改数据:
- 仅读取数据,不修改:直接赋值(无开销);
- 需要修改数据,且不影响原对象:必须拷贝。
- 判断对象是否嵌套:
- 单层对象(无内层可变对象):浅拷贝(
copy()/切片,性能高);
- 多层嵌套对象(含内层可变对象):深拷贝(
deepcopy(),完全独立)。
- 判断性能是否敏感:
- 数据量小/嵌套浅:深拷贝(方便);
- 数据量大/嵌套深:手动部分拷贝(仅复制需要修改的层级,性能高)。
最终口诀:
'只读不拷,单层浅拷,嵌套深拷,量大手拷'
通过理解变量的'引用本质'和拷贝的'层级差异',你就能精准控制数据的独立性,避开'改一个影响另一个'的隐性 bug,写出更健壮、更高效的 Python 代码。
相关免费在线工具
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online