跳到主要内容

深拷贝和浅拷贝

可变对象和不可变对象 一文中提到过,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)
适用场景扁平结构嵌套结构、数据隔离