深拷贝和浅拷贝
在 可变对象和不可变对象 一文中提到过,Python 中变量存储的是对象的引用,而非值本身。这就引出了一个重要的问题:
当我们需要得到一个对象的副本时,应该如何操作?
这就是拷贝的概念。而在面对嵌套数据结构时,拷贝的深度又分为两种:浅拷贝和深拷贝。
浅拷贝:复制容器结构,但共享内部对象
浅拷贝会创建一个新的外层对象,但内部嵌套对象依然是共享引用。用一个直观例子方便理解:
a ──→ [ ref1, ref2 ] ← 新对象
│ │
b ──→ [ ref1, ref2 ] ← 也是新对象,但 ref1/ref2 相同
↓
[1, 2] (同一个嵌套对象)
浅拷贝后:
- 列表 a 和 b 是不同的对象,修改 a 或 b 的外层结构(如添加/删除元素)不会互相影响。
- 而列表 a 和 b 内部的嵌套对象(如
[1, 2])是同一个对象,修改这个嵌套对象会同时影响列表 a 和 b。
如果列表 a 和 b 内部元素都是不可变对象,此时不会出现上述问题。
为什么需要浅拷贝
既然内部对象修改会互相影响,为什么还需要浅拷贝?首先因为 Python 的变量是引用,有时需要:
- 复制结构
- 不复制内容
下面通过一个修改配置模板顶层元素的例子来说明:假设系统有一个默认任务配置,现在需要基于它创建多个任务(如 task1、task2……)
default_config = {
"retry": 3,
"timeout": 10,
"headers": {
"User-Agent": "test-client"
}
}
在没有浅拷贝的情况下,如果要将 task2 的配置修改为 retry=5,由于它们共享同一个默认配置对象,此时会影响到 task1 和 task3:
import copy
task1 = default_config.copy()
task2 = task1
task3 = task1
task2["retry"] = 5
print(
default_config["retry"],
task1["retry"],
task2["retry"],
task3["retry"]
) # 3 5 5 5
使用浅拷贝后,每个任务都有自己的配置对象,修改 task2 的配置不会影响其他任务:
import copy
task1 = default_config.copy()
task2 = default_config.copy()
task3 = default_config.copy()
task2["retry"] = 5
print(
default_config["retry"],
task1["retry"],
task2["retry"],
task3["retry"]
) # 3 3 5 3
优缺点
通过对浅拷贝的深入探究,可以总结浅拷贝的优缺点如下:
优点:
- 性能高,复制开销小
- 对于不含嵌套对象的扁平数据,效果与深拷贝相同
缺点:
- 对嵌套对象无效,内层数据仍然共享
- 容易产生意外的数据污染
常见使用
b = a.copy() # list/dict 内置方法
b = list(a) # 类型构造器
b = a[:] # 切片
深拷贝:递归复制所有层
深拷贝会递归遍历对象的所有层级,为每一层都创建新对象,使得新旧对象之间完全独立。用一个直观例子方便理解:
a ──→ [ ref1, ref2 ] ← 新对象
↓ ↓
[1, 2] [3, 4] ← 全部是新对象,与 a 完全无关
b ──→ [ ref3, ref4 ] ← 也是新对象
↓ ↓
[1, 2] [3, 4] ← 独立副本
深拷贝后:
- 列表 a 和 b 是不同的对象,修改 a 或 b 的外层结构(如添加/删除元素)不会互相影响。
- 而列表 a 和 b 内部的嵌套对象(如
[1, 2])是不同的对象,修改 a 里的嵌套对象不会影响 b 里的嵌套对象,反之亦然。
深拷贝的性能开销
深拷贝通过递归复制所有层级,避免了浅拷贝中内层对象共享带来的隐患。不过,这种彻底性的代价是更高的内存和时间开销,当对象结构越复杂时,这一开销就越明显。
import copy
import time
# 构造一个更复杂的结构:100个书架,每个书架100本书
print("\n" + "=" * 50)
print("构建超大规模嵌套对象...")
big_library = {"name": "超大图书馆", "shelves": []}
# 生成 1000 个书架
for i in range(1000):
shelf = {"id": f"Shelf-{i}", "books": []}
# 每个书架放 1000 本书
for j in range(1000):
shelf["books"].append({"title": f"Book-{i}-{j}", "price": j})
big_library["shelves"].append(shelf)
print(f"总书籍数量: {1000 * 1000} 本")
# 浅拷贝
start = time.time()
shallow_big = copy.copy(big_library)
shallow_big_time = time.time() - start
# 深拷贝
start = time.time()
deep_big = copy.deepcopy(big_library)
deep_big_time = time.time() - start
print(f"大规模数据 - 浅拷贝耗时: {shallow_big_time:.6f} 秒")
print(f"大规模数据 - 深拷贝耗时: {deep_big_time:.6f} 秒")
# print(f"深拷贝是浅拷贝的 {deep_big_time/shallow_big_time:.1f} 倍")
"""
==================================================
构建超大规模嵌套对象...
总书籍数量: 1000000 本
大规模数据 - 浅拷贝耗时: 0.000000 秒
大规模数据 - 深拷贝耗时: 3.175306 秒
"""
优缺点
通过对深拷贝的深入探究,可以总结深拷贝的优缺点如下:
优点:
- 完全独立,修改副本不影响原对象
- 适合任意层次的嵌套结构
缺点:
- 性能开销大,对复杂对象递归复制耗时
- 含有循环引用的对象需要额外处理(
copy.deepcopy内置支持) - 某些特殊对象(如文件句柄、数据库连接)无法深拷贝
适用场景
| 场景 | 推荐方式 | 说明 |
|---|---|---|
| 扁平数据结构(无嵌套) | 浅拷贝 | 性能好,足够安全 |
| 嵌套数据结构需要完全隔离 | 深拷贝 | 防止内层数据被意外修改 |
| 多线程/异步环境中的数据隔离 | 深拷贝 | 避免竞态条件导致的数据污染 |
| 递归算法中保护原始数据 | 深拷贝 | 回溯算法、状态保存等场景 |
| 仅需隔离外层结构 | 浅拷贝 | 如列表分组、分页等操作 |
常见问题
Q:copy.copy() 和 .copy() 有什么区别?
效果一样,都是浅拷贝。.copy() 是 list/dict 等内置类型的方法;copy.copy() 是通用方法,适用于任意对象。
Q:深拷贝能处理循环引用吗?
循环引用见xxx
可以。copy.deepcopy 内部维护了一个备忘录(memo),用于检测并处理循环引用,不会陷入无限递归。
总结
| 对比 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 是否新对象 | ✅ | ✅ |
| 嵌套是否复制 | ❌ | ✅ |
| 性能 | 较快 | 较慢 |
| 方法 | a.copy() / a[:] | copy.deepcopy(a) |
| 适用场景 | 扁平结构 | 嵌套结构、数据隔离 |