Python 深拷贝和浅拷贝到底差在哪?6 个让你半夜 debug 的案例 + 对照表
系列:Python 底层设计逻辑|类型:排错清单型|分发:CSDN / 掘金 / 51CTO / 阿里云 / 知乎
字数:约 1500 字
结论先行:Python 里 copy.copy() 是浅拷贝,copy.deepcopy() 是深拷贝。浅拷贝只复制"第一层",嵌套的可变对象(list/dict/set)仍然共享同一块内存;深拷贝会递归复制所有层,新旧对象彻底解耦。90% 的"我改了 A,B 怎么也变了"都是浅拷贝埋的雷。
1. 最直观的反例:改一个,另一个跟着变
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a) # 浅拷贝
b[2].append(5)
print(a) # [1, 2, [3, 4, 5]] ← a 也被改了!
关键:b[2] 这个内层列表没有被复制,a 和 b 指向同一个内层列表对象。
2. 浅拷贝"只保第一层"到底保了什么
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
b[0] = 99
print(a) # [1, 2, [3, 4]] ← 这一层没事
为什么:外层容器(a 这个 list 本身)是新对象;但元素里,不可变对象(int)天然独立,可变对象(内层 list)只复制了"引用"。所以改 b[0] 安全,改 b[2] 危险。
3. 深拷贝:彻底解耦
import copy
a = [1, 2, [3, 4]]
c = copy.deepcopy(a)
c[2].append(5)
print(a) # [1, 2, [3, 4]] ← 完全不受影响
print(c) # [1, 2, [3, 4, 5]]
代价:深拷贝递归复制每一层,对象大/层级深时明显更慢、更占内存。不是无脑上深拷贝,而是"需要隔离时才用"。
4. 函数默认参数的经典陷阱
def add_item(x, items=[]): # 默认参数是模块加载时创建的同一个 list
items.append(x)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] ← 坑!
解法:items=None 后在函数内 items = items or [],本质就是避开"共享可变默认对象"。
5. 切片和 dict 推导 ≠ 深拷贝
a = [[1], [2]]
b = [x for x in a] # 列表推导仍是浅拷贝!
b[0].append(9)
print(a) # [[1, 9], [2]] ← 还是中招
对照:b = a[:]、list(a)、{**d} 全是浅拷贝。想深,必须 deepcopy。
6. 自定义对象:只拷 __dict__ 吗?
copy 模块对自定义类:浅拷贝新建实例并复制 __dict__(仍是引用共享);深拷贝递归处理。若对象持有文件句柄、锁、socket,深拷贝会报错或失去意义——这类对象要自己实现 __copy__ / __deepcopy__。
对照表:一眼分清
| 维度 | 赋值 = |
浅拷贝 copy() |
深拷贝 deepcopy() |
|---|---|---|---|
| 外层容器 | 同一对象 | 新对象 | 新对象 |
| 内层可变元素 | 共享 | 共享 | 独立 |
| 速度 | 最快 | 快 | 慢(递归) |
| 内存 | 最小 | 小 | 大 |
| 适用 | 不想复制 | 扁平结构/只读共享 | 需要完全隔离 |
排错清单(收藏级)
- 怀疑浅拷贝中招:先
id(a[2]) == id(b[2])验证内层是否同一对象。 - 想要隔离:直接
deepcopy,别用推导式/list()/[:]假装拷贝。 - 默认参数:永远用
None占位,函数内初始化。 - 超大嵌套:评估
deepcopy性能,必要时手动控制拷贝深度。 - 含资源句柄的对象:手写
__deepcopy__,别交给默认递归。
一句话记住:浅拷贝是"换个壳子装旧货",深拷贝是"连货带壳全重做"。改了嵌套还串味,八成是浅了。