从内存结构到 copy 模块源码:把 Python 深浅拷贝扒得底朝天
浅拷贝与深拷贝
Python 中给变量做赋值操作时,很多人会默认"赋值就是复制一份"。这个直觉在一部分场景下是对的,但在涉及可变对象(mutable object)时就不可靠了。浅拷贝和深拷贝这两个概念,归根结底是在回答一个问题:复制一个对象时,它的内部内容到底复制了几层。
要把这个问题说清楚,需要从 Python 的变量模型开始。
一、变量名不是盒子,是标签
在 C 语言中,变量可以被理解为一个"盒子"——你创建一个 int a = 5,内存里分配了 4 字节的空间,5 这个值被放进了这个盒子里。赋值 int b = a 会再分配一个新盒子,把 5 复制一份放进去。两个变量各自独立,改 a 不影响 b。
Python 的变量模型与此不同。Python 中的变量名是一个引用(reference),指向内存中的一个对象。你可以把它理解为"贴在对象上的标签",而不是"装对象的盒子"。
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4]
print(b) # [1, 2, 3, 4]
print(a is b) # True
a = [1, 2, 3] 做的事情是:在内存中创建一个列表对象 [1, 2, 3],然后把标签 a 贴在这个对象上。b = a 并没有创建新对象,它只是又拿了一张标签 b 贴在了同一个对象上。所以通过 b 去修改列表内容,a 看到的也是修改后的结果,因为它们指向的是同一个东西。
a is b 返回 True,说明两个变量名指向同一个对象(即内存地址相同)。
对于不可变对象(immutable object),这种共享不会产生问题:
a = 100
b = a
b = 200
print(a) # 100
print(b) # 200
b = 200 并没有修改原来 100 所在的内存,而是让标签 b 指向了一个新的整数对象 200。标签 a 仍然贴在 100 上。Python 中的整数、字符串、元组(tuple)都是不可变对象,一旦创建就不能被修改内容。
但列表、字典、集合是可变对象,它们的内容可以被原地修改。当两个变量名引用同一个可变对象时,通过任何一个名字做的修改,对另一个名字都可见。这就是浅拷贝和深拷贝要处理的核心问题。
二、浅拷贝:只复制最外层
浅拷贝(shallow copy)会创建一个新的容器对象,但不会递归地复制容器内部的元素。新容器中的元素和原容器中的元素指向同一组对象。
用代码来说明:
import copy
original = [1, 2, [3, 4]]
shallow = copy.copy(original)
print(shallow) # [1, 2, [3, 4]]
print(original is shallow) # False — 外层是新对象
print(original[2] is shallow[2]) # True — 内层的 [3, 4] 仍是同一个
original is shallow 返回 False,说明浅拷贝确实创建了一个新的列表对象。但 original[2] is shallow[2] 返回 True,意味着内部嵌套的列表 [3, 4] 没有被复制——新列表和旧列表共享了同一个子列表对象。
这时候修改子列表就会出现"预期之外"的行为:
shallow[2].append(5)
print(original) # [1, 2, [3, 4, 5]] ← 被改了
print(shallow) # [1, 2, [3, 4, 5]]
shallow 是一个独立的外层列表(修改 shallow[0] 不影响 original[0]),但它内部引用的可变对象和 original 共享。
浅拷贝有几种等价的触发方式:
# 方式一:copy.copy()
import copy
new_list = copy.copy(original)
# 方式二:list() 构造器
new_list = list(original)
# 方式三:切片
new_list = original[:]
# 方式四:列表推导式
new_list = [x for x in original]
这四种方式对一维列表(内部不嵌套可变对象)的语义完全一致。但对于嵌套结构,它们都是浅拷贝——只复制了一层。
字典的浅拷贝也类似:
original = {"a": 1, "b": [2, 3]}
shallow = original.copy() # dict.copy() 是浅拷贝
shallow = dict(original) # 等价
shallow = copy.copy(original) # 等价
shallow["b"].append(4)
print(original["b"]) # [2, 3, 4] ← 被改了
三、深拷贝:递归复制所有层级
深拷贝(deep copy)会递归地复制对象及其内部包含的所有嵌套对象,直到所有层级都是独立的副本。
import copy
original = [1, 2, [3, 4]]
deep = copy.deepcopy(original)
print(original is deep) # False
print(original[2] is deep[2]) # False — 内层列表也是新对象
deep[2].append(5)
print(original) # [1, 2, [3, 4]] ← 没被改
print(deep) # [1, 2, [3, 4, 5]]
deepcopy 递归遍历了原对象的所有嵌套层,每一层的可变对象都被复制了一份。修改 deep 中任何层级的元素,都不会影响到 original。
对于多层嵌套的结构:
original = {"users": [{"name": "Alice", "scores": [90, 85]}, {"name": "Bob", "scores": [78, 92]}]}
deep = copy.deepcopy(original)
deep["users"][0]["scores"].append(88)
print(original["users"][0]["scores"]) # [90, 85] ← 没被改
print(deep["users"][0]["scores"]) # [90, 85, 88]
四、浅拷贝和深拷贝的内存结构
用图来表示 original = [1, 2, [3, 4]] 经历赋值、浅拷贝、深拷贝后的内存状态。
直接赋值 b = a:
变量名 对象
a ──────────→ [1, 2, ●───→ [3, 4]]
b ──────────↗
a 和 b 是同一个对象的两个标签。只有一块列表对象,内层的 [3, 4] 也只有一份。
浅拷贝 b = copy.copy(a):
变量名 对象
a ──────────→ [1, 2, ●───→ [3, 4]] ← 内层共享
b ──────────→ [1, 2, ●──↗
外层列表是两个独立的对象,但它们内部索引 [2] 指向同一个子列表 [3, 4]。修改子列表的内容,a 和 b 都能看到变化。
深拷贝 b = copy.deepcopy(a):
变量名 对象
a ──────────→ [1, 2, ●───→ [3, 4]]
b ──────────→ [1, 2, ●───→ [3, 4]] ← 完全独立
外层列表是独立的,内层子列表也是独立的。两层之间没有任何共享。
五、copy 模块的实现细节
Python 标准库的 copy 模块源码值得读一读,尤其是理解它的类型分派与协议回退两层机制。
copy.copy() 和 copy.deepcopy() 的内部逻辑分为两步走:先查内部分派表(dispatch table),查不到才走对象协议。
内部分派表(快速路径)
为了保证基础数据结构的复制性能,copy 模块内部维护了两张字典——_copy_dispatch 和 _deepcopy_dispatch——对 list、dict、tuple、set、frozenset、bytearray 等内置类型硬编码了专用的复制函数。调用 copy.copy() 或 copy.deepcopy() 时,模块会先用 type(x) 作为键去查分派表,命中就直接调用对应的函数,不走任何协议检查。
# copy.py 源码(CPython,简化示意)
_copy_dispatch = {
list: list.copy, # list 的浅拷贝直接用 list.copy()
dict: dict.copy, # dict 的浅拷贝直接用 dict.copy()
set: set.copy, # set 的浅拷贝直接用 set.copy()
frozenset: lambda x: x, # frozenset 不可变,直接返回自身
# ... 其他内置类型
}
这意味着对于最常见的内置容器,copy.copy([1, 2, 3]) 和直接调 [1, 2, 3].copy() 的开销几乎一样——省去了协议查找的类型检查开销。
协议回退(通用路径)
当分派表中没有匹配的类型(通常是自定义类),copy 模块才会依次尝试以下协议:
__copy__协议:如果对象定义了__copy__()方法,copy.copy()会调用它来获取浅拷贝结果__deepcopy__协议:如果对象定义了__deepcopy__(memo)方法,copy.deepcopy()会调用它来获取深拷贝结果- pickling 协议:如果对象没有定义上述方法,
copy模块会退而使用__reduce_ex__、__reduce__、__getstate__/__setstate__等 pickle 协议来重建对象 - 类型注册机制:
copyreg模块允许为特定类型注册自定义的复制函数,注册后也会被分派表查询到
整个流程可以概括为:分派表命中 → 直接复制;分派表未命中 → 协议回退 → pickle 回退 → copyreg 注册。
memo 字典是 deepcopy 的核心机制。它记录了"原对象 → 副本对象"的映射关系。在递归复制过程中,如果遇到一个已经被复制过的对象(比如两个列表引用了同一个子列表),deepcopy 不会重复创建新副本,而是复用之前已经创建的那个。这保证了深拷贝正确处理循环引用和共享引用:
import copy
# 共享引用
inner = [1, 2]
original = [inner, inner] # 两个元素指向同一个子列表
deep = copy.deepcopy(original)
print(original[0] is original[1]) # True
print(deep[0] is deep[1]) # True — 共享关系被保留了
deep[0].append(3)
print(original[0]) # [1, 2] ← 没被改
print(deep[0]) # [1, 2, 3]
print(deep[1]) # [1, 2, 3] ← 因为共享引用,deep[0] 和 deep[1] 仍是同一个对象
如果 deepcopy 不维护 memo 字典,deep[0] 和 deep[1] 会变成两个独立对象,共享引用的语义就丢失了。
循环引用也能正确处理:
a = [1, 2]
a.append(a) # a 引用了自身
deep = copy.deepcopy(a)
# deepcopy 不会无限递归,memo 字典会检测到 a 已经在复制过程中
六、不同类型对象的拷贝行为
Python 内置类型的拷贝行为并不完全一致,有些需要特别注意。
6.1 元组(tuple)
元组是不可变对象,但元组内部可以包含可变对象。浅拷贝一个包含列表的元组,外层元组不会被复制(因为不可变对象本身无需复制),但 copy.copy() 仍然会返回同一个元组对象(因为它是不可变的,复制没有意义):
import copy
t = (1, 2, [3, 4])
shallow = copy.copy(t)
print(t is shallow) # True — 不可变对象,浅拷贝直接返回自身
# 但如果用 deepcopy
deep = copy.deepcopy(t)
print(t is deep) # False — 因为内部有可变对象,deepcopy 会创建新元组
print(t[2] is deep[2]) # False — 内部的 [3, 4] 被复制了
这反映了一个设计原则:不可变对象如果在所有层级上都只包含不可变对象,它本身就是"安全的",无需复制。但如果不可变对象内部嵌套了可变对象,问题就回来了。
6.2 字典(dict)
字典的浅拷贝通过 dict.copy() 或 copy.copy() 完成。和列表一样,只复制外层字典结构,key 和 value 仍然是原始引用:
original = {"config": {"debug": True}, "data": [1, 2]}
shallow = original.copy()
shallow["config"]["debug"] = False
print(original["config"]["debug"]) # False ← 被改了
6.3 自定义类
自定义类的默认拷贝行为取决于它的 __dict__ 和 __slots__:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p1 = Point(1, 2)
p2 = copy.copy(p1)
print(p1 is p2) # False — 创建了新的 Point 实例
print(p1.x is p2.x) # True — 但 x 的值(整数)是不可变对象,共享不影响
class Node:
def __init__(self, value, children=None):
self.value = value
self.children = children or []
n1 = Node("root", [Node("child_1"), Node("child_2")])
n2 = copy.copy(n1)
print(n1 is n2) # False
print(n1.children is n2.children) # True — 浅拷贝,children 列表被共享
n2.children[0].value = "modified"
print(n1.children[0].value) # "modified" ← 被改了
自定义类的 deepcopy 会递归复制 __dict__ 中所有属性。如果类中包含不可序列化的字段(比如打开的文件句柄、数据库连接),deepcopy 可能会失败或产生无意义的副本。
6.4 特殊类型
| 类型 | 浅拷贝行为 | 深拷贝行为 | 备注 |
|---|---|---|---|
int, float, str |
返回自身 | 返回自身 | 不可变,无需复制 |
tuple |
返回自身(除非内部有可变对象且 __deepcopy__ 被调用) |
递归复制内层可变对象,创建新元组 | 含可变对象的元组在 deepcopy 时会被复制 |
list |
创建新列表,元素引用共享 | 递归复制所有层级 | 标准容器 |
dict |
创建新字典,key/value 引用共享 | 递归复制所有层级 | 标准容器 |
set |
创建新集合,元素引用共享 | 递归复制所有层级 | 元素本身必须是可哈希的 |
frozenset |
返回自身 | 返回自身 | 不可变 |
function |
返回自身 | 返回自身 | 函数对象不支持拷贝 |
module |
返回自身 | 返回自身 | 模块对象不支持拷贝 |
七、is 和 == 的区别
讨论拷贝时经常需要区分这两个操作符。
a is b:判断两个变量是否指向同一个对象(比较的是id(a) == id(b),即内存地址)a == b:判断两个对象的值是否相等(调用a.__eq__(b))
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — 值相等
print(a is b) # False — 不是同一个对象
c = a
print(a is c) # True
在拷贝的语境中:
- 浅拷贝后,
original == copy一定为True(如果内部元素定义了正确的__eq__),但original is copy为False - 深拷贝后同理,但深拷贝保证所有嵌套层级都没有共享引用
八、什么时候需要深拷贝
实际开发中,直接用 deepcopy 的场景其实不多。大部分情况下,要么不需要复制(函数参数传递时用不可变对象),要么浅拷贝就够了。
需要深拷贝的典型场景:
递归数据结构的修改。树、图这类数据结构经常需要在修改某个分支时保留原始状态。如果树的节点用列表或字典表示,浅拷贝会导致分支之间互相污染。
# 不用 deepcopy 的后果
def add_branch(tree, path, value):
"""在树的某条路径上添加值"""
node = tree
for key in path[:-1]:
node = node[key]
node[path[-1]] = value
tree1 = {"a": {"b": {"c": 1}}}
tree2 = copy.copy(tree1) # 浅拷贝
add_branch(tree2, ["a", "b", "d"], 2)
print(tree1) # {"a": {"b": {"c": 1, "d": 2}}} ← tree1 被污染了
用 copy.deepcopy(tree1) 就不会有这个问题。
缓存和快照。系统需要保存某个数据结构的历史状态时,必须确保快照和当前状态完全独立。deepcopy 是最直接的方式(虽然不一定是性能最优的——对于大型对象,序列化到 JSON 再反序列化可能更快,因为 C 实现的 JSON 编解码器比 Python 层的递归遍历快)。
函数参数的防御性复制。如果你的函数接收一个可变对象作为参数,且需要在函数内部修改它但不想影响外部传入的对象:
def process_data(data):
working_copy = copy.deepcopy(data)
working_copy["results"] = compute(working_copy["raw"])
return working_copy
这在编写库代码时比较常见——你不知道调用者会不会在传参之后继续使用原始对象。
九、深拷贝的性能开销
deepcopy 的代价在于它需要递归遍历整个对象图(object graph),为每个可变对象创建新副本,并维护 memo 字典来追踪已复制的对象。对于一个包含 N 个对象的结构,deepcopy 的时间和空间复杂度都是 O(N)。
在性能敏感的场景中,可以考虑替代方案:
- 手动构造新对象:对于结构已知的对象,显式创建新实例并逐字段赋值,比
deepcopy更快,因为跳过了类型检查和 memo 管理的开销 - 序列化/反序列化:
json.loads(json.dumps(obj))对于纯 JSON 兼容的结构(字典、列表、字符串、数字),性能往往优于deepcopy,因为 JSON 编解码器是 C 实现的。缺点是无法处理自定义类、集合等非 JSON 类型 copyreg注册:对于频繁拷贝的自定义类,通过copyreg注册一个高效的复制函数,避免deepcopy的通用遍历逻辑
十、总结
| 操作 | 外层对象 | 内部可变对象 | 典型方式 |
|---|---|---|---|
赋值 b = a |
同一个对象 | 同一个对象 | 直接赋值 |
| 浅拷贝 | 新对象 | 共享原对象 | copy.copy()、切片、list()、dict.copy() |
| 深拷贝 | 新对象 | 也是新对象 | copy.deepcopy() |
选择拷贝方式的原则:
- 如果对象内部没有可变元素(比如纯数字列表
[1, 2, 3]),浅拷贝和深拷贝效果一样,用浅拷贝就行 - 如果对象内部有可变元素,且你需要在拷贝后独立修改这些内层元素,用深拷贝
- 如果只是想把外层容器独立出来(比如从函数里返回一个新列表,但列表里的元素只是读取、不会修改),浅拷贝就足够了
- 性能敏感场景中,优先考虑手动构造或序列化方案替代
deepcopy
更多推荐



所有评论(0)