← 返回题目列表

Python 中浅拷贝和深拷贝有什么区别?

高频 中等 第 14 / 22 题 更新于 2026/07/25
Python浅拷贝深拷贝copy

简化版

浅拷贝只复制最外层容器,内部元素对象仍然共享;深拷贝会递归复制内部对象,尽量让新对象和原对象互不影响。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 是浅拷贝:ba 是不同列表,但里面的子列表还是同一个对象。
  • 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_Aouter_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

deepcopymemo 可以理解成“旧对象 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 处理循环和共享结构。真正选型时先判断要隔离外层结构、内层可变对象,还是只需要按业务字段重新构造。