别再被“等号”骗了!Python 浅拷贝与深拷贝
摘要:还在被 Python 的赋值搞晕吗?本文将为您讲解赋值、浅拷贝与深拷贝的区别,揭秘“引用”背后的内存真相,帮你避开数据被意外篡改的“深坑”,彻底搞懂 copy 模块的正确用法。
📚 核心知识点:引用、浅拷贝与深拷贝
在 Python 中,万物皆对象。当我们处理列表、字典等可变对象时,经常会遇到一个让人抓狂的问题:为什么我修改了副本,原数据也跟着变了?
这就要从 Python 的内存管理机制说起。我们需要区分三个概念:
- 赋值(=) :这根本不是拷贝!这只是给对象贴了个新标签(起了个别名)。
- 浅拷贝(Shallow Copy) :只拷贝了“外壳”,里面的内容还是共用的。
- 深拷贝(Deep Copy) :彻底的克隆,从外到内完全独立。
📝 场景重现:被“篡改”的数据
想象一下,你正在写一个购物车程序。你想基于原始购物车 original_cart 创建一个副本 new_cart 来模拟打折计算,但不想影响原始数据。
错误示范(赋值陷阱) :
original_cart = ["手机", "耳机"]
new_cart = original_cart # 你以为你复制了它?
new_cart.append("充电宝")
print(original_cart)
# 输出:['手机', '耳机', '充电宝']
# 完蛋!原始数据被污染了!
为什么会这样?让我们深入内存世界看一看。
💡 原理解析:从“贴标签”到“俄罗斯套娃”
为了讲清楚这三者的区别,我们可以用“俄罗斯套娃”(或者列表套列表)来打比方。
假设我们有一个嵌套列表:a = [1, [2, 3], 4]。
- 外层列表是“大套娃”。
- 里面的
[2, 3]是藏在里面的“小套娃”。
1. 赋值(=):只是起了个“外号”
当你执行 b = a 时,Python 并没有创建新对象。它只是让变量 b 指向了和 a 同一个内存地址。
- 比喻:你给小明起了个外号叫“二狗”。叫“二狗”或者叫“小明”,指的都是同一个人。
- 结果:改
b就是改a,完全同步。
2. 浅拷贝(Shallow Copy):只复制“外壳”
当你执行 import copy; b = copy.copy(a) 时,Python 创建了一个新的外层列表,但是!对于列表里面的元素,它只是复制了引用(内存地址) 。
-
比喻:你买了一个新的大套娃(
b),但你没买里面的小套娃,而是把a里面的小套娃拿出来放进了b里。 -
结果:
- 如果你修改外层的数字(比如把
1改成9),互不影响(因为数字是不可变对象,修改等于新建)。 - 如果你修改内层的列表(比如把
[2, 3]改成[2, 3, 4]),两个大套娃都会变! 因为它们肚子里装的是同一个小套娃。
- 如果你修改外层的数字(比如把
3. 深拷贝(Deep Copy):彻底的“克隆”
当你执行 import copy; b = copy.deepcopy(a) 时,Python 会递归地拷贝所有层级的对象。
- 比喻:你不仅买了一个新的大套娃,还找人把里面的每一个小套娃都原样复制了一份放进去。
- 结果:
a和b是完全独立的两个人,无论怎么改,互不干扰。
💻 代码实战(Python)
这里有一份完整的代码示例,你可以直接运行看看效果:
import copy
# 原始数据:一个包含嵌套列表的“套娃”
original = [1, [2, 3], 4]
# 1. 赋值(=):只是起别名
assign_ref = original
# 2. 浅拷贝(Shallow Copy):只拷贝外壳
shallow_copy = copy.copy(original)
# 或者用列表切片:shallow_copy = original[:]
# 3. 深拷贝(Deep Copy):完全克隆
deep_copy = copy.deepcopy(original)
# ===================== 开始搞破坏 =====================
# 修改外层元素(数字是不可变对象,修改即替换)
original[0] = 999
# 修改内层元素(列表是可变对象,原地修改)
original[1][0] = 'X'
# ===================== 查看结果 =====================
print(f"原始数据: {original}")
# 输出: [999, ['X', 3], 4]
print(f"赋值引用: {assign_ref}")
# 输出: [999, ['X', 3], 4]
# (完全一样,因为是同一个对象)
print(f"浅拷贝 : {shallow_copy}")
# 输出: [1, ['X', 3], 4]
# (外层 1 没变,但内层变成了 'X'!因为内层是共用的)
print(f"深拷贝 : {deep_copy}")
# 输出: [1, [2, 3], 4]
# (完全没变,它是独立的!)
📌 总结与避坑指南
为了让你一目了然,我做了一个对比表:
| 操作方式 | 代码示例 | 内存关系 | 修改外层 | 修改内层(嵌套对象) | 适用场景 |
|---|---|---|---|---|---|
| 赋值 | b = a | 同一地址 | 影响 | 影响 | 只是想给变量起个别名 |
| 浅拷贝 | copy.copy(a) | 外层独立,内层共用 | 不影响 | 影响 | 确定列表里没有嵌套可变对象,或为了省内存 |
| 深拷贝 | copy.deepcopy(a) | 完全独立 | 不影响 | 不影响 | 复杂数据结构(如 JSON、嵌套列表),需要绝对安全时 |
小编建议:
在处理复杂的业务数据(比如从数据库查出来的嵌套字典、列表)时,如果不确定数据结构有多深,无脑用 copy.deepcopy() 最安全!虽然性能稍微差一点点,但能帮你省去无数排查“数据为什么被改了”的 Bug 时间。
希望今天的知识能帮你在下次面试中从容应对!🐟