Python 中浅拷贝和深拷贝有什么区别?
简化版
浅拷贝只复制最外层容器,内部元素对象仍然共享;深拷贝会递归复制内部对象,尽量让新对象和原对象互不影响。Python 中可以用 copy.copy() 做浅拷贝,用 copy.deepcopy() 做深拷贝。
详细版
例子:
import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)
b[0].append(99)
print(a) # [[1, 2, 99], [3, 4]]
c[1].append(88)
print(a) # [[1, 2, 99], [3, 4]]
原因:
b是浅拷贝:b和a是不同列表,但里面的子列表还是同一个对象。c是深拷贝:外层列表和内层列表都复制了,所以修改c的内层不会影响a。
常见浅拷贝方式:
b = a[:]
b = list(a)
b = a.copy()
b = copy.copy(a)
是否需要深拷贝,要看对象是否有嵌套可变结构。没有嵌套可变对象时,浅拷贝通常足够。
完整版教学
一、为什么会有拷贝问题
Python 变量保存的是对象引用。直接赋值不会复制对象:
a = [1, 2]
b = a
b.append(3)
print(a) # [1, 2, 3]
如果你希望得到一个“新的列表”,就需要拷贝:
b = a.copy()
但问题来了:如果列表里面还有列表,只复制外层够不够?这就是浅拷贝和深拷贝的区别。
a = [[1], [2]]
a ──> outer_list
├──> inner_list_1
└──> inner_list_2
拷贝问题的核心不是“对象看起来有几层括号”,而是对象图里哪些节点需要共享、哪些节点需要隔离。浅拷贝只新建最外层节点;深拷贝会沿着引用关系继续复制内部节点。
二、浅拷贝复制外层,内部引用仍共享
a = [[1], [2]]
b = a.copy()
print(a is b) # False
print(a[0] is b[0]) # True
外层列表不同,但第一个内层列表是同一个对象。
所以:
b.append([3])
print(a) # [[1], [2]]
修改外层结构不会影响原列表。
但:
b[0].append(99)
print(a) # [[1, 99], [2]]
修改共享的内层对象,会影响原列表。
可以用一张图看清楚:
浅拷贝后:
a ──> outer_A ──┬──> inner_1
└──> inner_2
b ──> outer_B ──┬──> inner_1
└──> inner_2
外层 outer_A 和 outer_B 是两个对象,所以 b.append([3]) 不影响 a 的外层结构;但 inner_1 被共享,所以 b[0].append(99) 会影响 a[0]。
三、深拷贝递归复制内部对象
深拷贝会递归复制对象图:
import copy
a = [[1], [2]]
b = copy.deepcopy(a)
print(a is b) # False
print(a[0] is b[0]) # False
此时修改内层对象也互不影响:
b[0].append(99)
print(a) # [[1], [2]]
print(b) # [[1, 99], [2]]
深拷贝后的对象图更像这样:
深拷贝后:
a ──> outer_A ──┬──> inner_A1
└──> inner_A2
b ──> outer_B ──┬──> inner_B1
└──> inner_B2
数字例子:a 有 1 个外层列表和 2 个内层列表。浅拷贝只新增 1 个外层列表;深拷贝通常会新增 1 个外层列表和 2 个内层列表。嵌套越深、对象越大,深拷贝成本越明显。
记忆钩子:浅拷贝只换“外壳”,深拷贝才会沿着对象图继续往里复制;判断影响范围时画出外层和内层对象最稳。
四、深拷贝不是越多越好
深拷贝有成本:
- 需要递归复制,性能更重;
- 占用更多内存;
- 对包含文件句柄、数据库连接、线程锁等资源的对象,不一定能按预期复制;
- 对复杂对象图要处理循环引用。
Python 的 deepcopy 内部会用 memo 字典记录已经复制过的对象,避免循环引用导致无限递归。
a = []
a.append(a)
b = copy.deepcopy(a)
print(b[0] is b) # True
deepcopy 的 memo 可以理解成“旧对象 id -> 新对象”的映射。遇到循环引用时,如果发现某个旧对象已经复制过,就直接复用对应的新对象,避免无限递归。它也能保持共享结构:原对象图里两个位置指向同一个内部对象,深拷贝后通常也会让两个位置指向同一个复制后的内部对象,而不是盲目复制两份。
五、常见拷贝方式对比
| 写法 | 类型 | 说明 |
|---|---|---|
b = a | 不拷贝 | 只是多一个名字 |
a[:] | 浅拷贝 | 常用于 list |
list(a) | 浅拷贝 | 从可迭代对象构造列表 |
a.copy() | 浅拷贝 | list/dict/set 常见 |
copy.copy(a) | 浅拷贝 | 通用浅拷贝协议 |
copy.deepcopy(a) | 深拷贝 | 递归复制对象图 |
对于不可变对象,拷贝可能直接返回原对象或共享内部对象,这通常没问题,因为不可变对象不能原地改。对于自定义对象,浅拷贝和深拷贝会遵循对象协议;必要时可以实现 __copy__ 和 __deepcopy__ 控制行为。
六、实际开发怎么选
选择方式:
- 只是想避免外层列表增删影响原对象:浅拷贝足够。
- 内部有嵌套可变对象,并且也要隔离:用深拷贝。
- 数据很大、嵌套很深:优先考虑重新构造需要的部分,而不是无脑深拷贝。
- 对自定义对象:必要时实现
__copy__或__deepcopy__控制拷贝行为。
面试时可以用“外层是否复制、内层是否共享”来回答,比只背 API 更清楚。
还要问一个业务问题:你到底想隔离什么?如果只是不想函数修改传入列表的长度,浅拷贝足够;如果列表里有用户字典,且你要修改字典字段,就需要深拷贝或逐项构造新字典。很多时候“按需重建”比无脑 deepcopy 更可控。
new_users = [{**user, "active": True} for user in users]
这种写法只复制你关心的字典层级,成本和意图都更明确。
七、常见误区与追问
- 误区:赋值就是拷贝。
b = a只是名字绑定,两个名字仍指向同一对象。 - 误区:浅拷贝后两个对象完全独立。 浅拷贝只隔离外层容器,内部可变对象仍可能共享。
- 误区:深拷贝永远更安全。 深拷贝更慢、更占内存,对文件句柄、连接、锁等资源对象也未必符合业务预期。
- 追问:
deepcopy如何处理循环引用? 它用 memo 记录已复制对象,遇到环时复用已创建的新对象,避免无限递归。 - 追问:切片
a[:]是什么拷贝? 对 list 来说通常是浅拷贝,新外层列表共享内部元素引用。 - 追问:什么时候应该自定义
__deepcopy__? 当对象持有外部资源、缓存、共享状态,默认递归复制不符合业务语义时。
八、加强记忆
把拷贝记成“对象图复制到哪一层”:赋值不复制,只多一个名字;浅拷贝复制最外层容器,里面的对象引用仍共享;深拷贝沿对象图递归复制,并用 memo 处理循环和共享结构。真正选型时先判断要隔离外层结构、内层可变对象,还是只需要按业务字段重新构造。