Python浅拷贝与深拷贝的本质:内存引用机制解析

陆遥
陆遥
发布时间:2026-11-12| 2人浏览过
原创

深入解析Python浅拷贝与深拷贝的底层内存机制,结合变量名标签模型、指针数组搬移、递归克隆与memo防环结构,彻底理清直接赋值、浅拷贝与深拷贝的本质区别。

Python浅拷贝与深拷贝的本质:内存引用机制解析
语言:Python 3 主题:底层内存模型 / 数据结构克隆 阅读预估:10 分钟
在 Python 中,理解浅拷贝与深拷贝的唯一途径是透视内存引用机制。Python 的变量并非直接存储数值的“物理盒子”,而是悬挂在对象上的“名字标签”。直接赋值 b = a 仅是给同一个内存对象增添了一个别名;浅拷贝 copy.copy() 创建了新的顶层容器,但内部所有元素仍沿用原有的内存指针;只有深拷贝 copy.deepcopy() 才会递归遍历整个对象树,并在“备忘录字典”的协同下完整克隆出所有独立的下层分支。

一、变量绑定与直接赋值:名牌与对象的映射关系

在很多静态语言(如 C/C++)的学习经验中,初学者习惯把变量想象成一个具有固定大小和物理内存地址的“盒子”,给变量赋值就相当于把数据放进盒子里。然而在 Python 的对象模型中,这一直觉会带来严重的认知偏差。

在 Python 中,万物皆对象,变量只是引用。当执行 a = [1, 2, [3, 4]] 时,Python 解释器会做两件事:

  1. 在堆内存中开辟空间,创建具体的列表对象(内含元信息与指针数组),并分配唯一的内存地址。
  2. 在当前作用域的命名空间(Namespace 字典)中登记名字 a,并将其指向该列表的内存地址。

当执行 b = a 时,解释器既不会复制列表中的数据,也不会申请任何新的容器内存,而仅仅是在当前的命名空间中增加了一个名字 b,并让它指向与 a 完全相同的内存地址。

# 直接赋值示例
a = [1, 2, [3, 4]]
b = a

print(f"a 的内存地址: {id(a)}")  # 示例输出: 0x10A2B0
print(f"b 的内存地址: {id(b)}")  # 示例输出: 0x10A2B0 (完全一致)
print(f"a 与 b 是否同一对象: {a is b}")  # 输出: True

b[0] = 99
print(f"修改 b 后的 a: {a}")  # 输出: [99, 2, [3, 4]],原列表同步被改动

内置函数 id() 返回对象在生命周期内的唯一标识(在 CPython 实现中直接对应底层 C 结构体的内存指针地址),而 is 操作符正是通过比对两个变量的 id() 是否相等来判断它们是否属于同一个对象。直接赋值的结果是 a is b 恒为 True,两者实质是对同一片堆内存区域的两种称呼。

Python浅拷贝与深拷贝的本质:内存引用机制解析
图 1:Python 命名空间标签与堆内存对象之间的指针绑定关系

二、浅拷贝(Shallow Copy):外壳独立,内核共享

当业务需要修改新集合而不污染旧集合时,直接赋值显然无法满足要求,浅拷贝便应运而生。在 Python 中,浅拷贝可以通过多种常见方式触发:

  • 标准库函数:import copy; c = copy.copy(a)
  • 列表切片操作:c = a[:]
  • 类型工厂函数:c = list(a) 或 c = dict(d)
  • 集合自带的浅拷贝方法:c = a.copy()

1. 浅拷贝在内存中的具体构建步骤

所谓“浅”,指的是拷贝动作仅停留在最外层容器自身。以 c = copy.copy(a) 为例,底层的执行流程可清晰拆分为三个阶段:

  1. 分配新容器:在堆内存中重新申请一块内存空间,初始化一个全新的列表对象(例如地址为 0x10C880),此时 id(c) != id(a),即 c is a 为 False。
  2. 复制槽位指针:依次读取原容器 a 内部数组(CPython 中的 ob_item 指针数组)所保存的所有元素指针,并将这些指针逐一写进新容器 c 的槽位中。注意:此时没有克隆任何元素对象,复制的仅仅是指针本身的数值。
  3. 完成绑定:将变量名 c 绑定到这个新的外层容器上。
import copy

a = [1, 2, [3, 4]]
c = copy.copy(a)

print(f"外层容器是否相同: {a is c}")          # 输出: False (容器独立)
print(f"第0项引用是否相同: {a[0] is c[0]}")    # 输出: True  (共享整数 1)
print(f"第2项嵌套列表是否相同: {a[2] is c[2]}") # 输出: True  (共享嵌套列表地址)
Python浅拷贝与深拷贝的本质:内存引用机制解析
图 2:浅拷贝的底层执行步骤与内外层元素修改时的分化行为

2. 浅拷贝的行为分化:为什么修改有时独立、有时互相影响?

很多开发者容易对浅拷贝产生误解,原因是执行修改操作时,往往会出现看似矛盾的现象:

  • 对顶层元素进行替换:例如 c[0] = 99。由于整数是不可变对象,这个操作实质是将 c[0] 的指针重定向到新创建的整数 99 上,而 a[0] 依然指向整数 1。此时两者互不干扰,表现出“独立副本”的特征。
  • 对嵌套可变对象进行内部修改:例如 c[2].append(5)。由于 a[2] 和 c[2] 存放的是完全相同的嵌套列表地址 0x10B440,此时 append 是在原地修改这块共享内存,因此 a[2] 也会同步变为 [3, 4, 5]!
避坑重点:浅拷贝仅能安全用于“一维扁平数据结构”。只要数据结构中嵌套了列表、字典、集合或自定义类的可变实例,浅拷贝就绝对不能防止内层状态的串扰。

三、深拷贝(Deep Copy):递归克隆与备忘录防环机制

若要实现无论结构嵌套多少层,新旧对象都彻底解耦、互不影响,就必须采用深拷贝。深拷贝由 Python 标准库的 copy.deepcopy() 提供。

深拷贝的本质是对对象引用图的完整递归遍历与重建。它不仅为顶层容器创建新内存,还会沿着内部指针一路向下扫描,凡是遇到的可变子对象,都会依次回调克隆逻辑,生成全新的独立副本。

import copy

a = [1, 2, [3, 4]]
d = copy.deepcopy(a)

d[2].append(5)
print(f"修改副本后 a[2]: {a[2]}")  # 输出: [3, 4] (原数据未被影响!)
print(f"修改副本后 d[2]: {d[2]}")  # 输出: [3, 4, 5] (副本完全独立)
print(f"嵌套列表地址比较: {a[2] is d[2]}")  # 输出: False (内存地址完全不同)

备忘录字典(memo):化解循环引用的核心法宝

在实际项目中,对象关系往往不是简单的树状层级,而可能存在复杂的交叉引用甚至是循环引用(例如列表 x 的某个元素包含了 x 自身)。如果深拷贝只是天真地不断递归调用自身,解释器将陷入无限递归,最终抛出 RecursionError: maximum recursion depth exceeded。

Python 在 deepcopy 内部引入了关键的 备忘录字典(memo dict) 机制:

  1. deepcopy(x, memo=None) 在启动时会维护一个 memo = {} 字典。
  2. 在克隆每个对象前,首先以其内存地址 id(x) 为键查询 memo。
  3. 若 id(x) 已在 memo 中,说明该对象已被访问或正在被克隆,此时直接返回 memo[id(x)] 所保存的新对象引用,递归立即回溯。
  4. 若未命中,则立即为当前对象分配克隆副本,并在深入递归子元素之前,先将 memo[id(x)] = x_copy 记录在案。这样后续即使子节点再次指向父节点,也能精准拿到已建好的副本引用,从而打破死循环。
# 循环引用与深拷贝防环测试
cyclic_list = [1]
cyclic_list.append(cyclic_list)  # 列表中包含了对自身的引用

cloned = copy.deepcopy(cyclic_list)
print(f"克隆是否成功: {cloned[0] == 1}")        # 输出: True
print(f"回环引用是否指向克隆体自身: {cloned[1] is cloned}")  # 输出: True
print(f"克隆体与原体是否独立: {cloned is cyclic_list}")      # 输出: False
Python浅拷贝与深拷贝的本质:内存引用机制解析
图 3:深拷贝递归解构与依赖 memo 备忘录消除死循环的运行架构

四、不可变对象的拷贝特殊性:Python 的底层优化

在学习深浅拷贝时,不可变对象(如 int、float、str、tuple、frozenset)的表现往往会让很多开发者产生困惑。

既然不可变对象一旦在内存中创建就永远无法被篡改,那么无论是直接赋值、浅拷贝还是深拷贝,为其复制一份全新内存没有任何业务价值,只会平白浪费系统资源。因此 Python 解释器在 copy 模块中做了极其深度的优化:

  • 对于基本标量(数字、字符串、布尔值):copy.copy(100) 或 copy.deepcopy(100) 均直接返回原对象。
  • 对于只包含不可变元素的元组:例如 t = (1, 2, "hello"),执行 copy.deepcopy(t) is t 结果依然为 True!
  • 特例:包含可变对象的元组。如果元组内部嵌套了列表 t = (1, [2, 3]),此时元组本身虽然不可变,但其包含的元素是可变的。为了保证深层数据隔离,deepcopy(t) 会破例创建并返回一个全新的元组对象。
t1 = (1, 2, 3)
t1_deep = copy.deepcopy(t1)
print(f"纯不可变元组深拷贝: {t1 is t1_deep}")  # 输出: True (直接复用优化)

t2 = (1, [2, 3])
t2_deep = copy.deepcopy(t2)
print(f"含有可变列表的元组深拷贝: {t2 is t2_deep}")  # 输出: False (为隔离而新建)
print(f"元组内部列表是否独立: {t2[1] is t2_deep[1]}") # 输出: False

五、工程全景对比与技术选型指南

在工程实践中,盲目使用 copy.deepcopy() 是初学者最容易犯的性能杀手错误之一。深拷贝不仅需要额外的堆内存开销,其递归反射检查与维护 memo 字典的耗时可能比浅拷贝慢几个数量级。选择何种机制,应当基于确切的数据结构和业务隔离需求。

对比维度 直接赋值 (b = a) 浅拷贝 (copy.copy) 深拷贝 (copy.deepcopy)
本质逻辑 共享同一对象,新增变量名引用 创建顶层新容器,复制元素指针 递归复制整棵对象树与子节点
内存开销 0 字节(仅名牌登记开销) 极小(仅容纳顶层指针的数组) 较大(复制所有遍历到的可变对象)
执行时间 O(1) 瞬时完成(纳秒级) O(N) 与最外层长度成正比 O(M) 与全部子节点总数成正比
外层容器独立性 不独立(同一地址) 独立(增删元素不影响原容器) 独立(完全解耦)
嵌套对象独立性 完全共享 完全共享(修改内层互相污染) 完全独立(深层修改互无感知)
典型应用场景 参数传递、状态共享、重命名别名 一维列表排序、过滤、快照备份 复杂多层嵌套配置字典、图/树结构克隆
Python浅拷贝与深拷贝的本质:内存引用机制解析
图 4:Python 拷贝机制全景决策逻辑与三大模式核心属性图谱
架构建议: 在面向大规模数据处理或高频请求处理时(如 Web 框架中间件、游戏循环):
  1. 优先使用扁平化的不可变数据结构(如 namedtuple 或 dataclass(frozen=True)),天然免除深拷贝需求。
  2. 如果必须针对复杂类实例实施深拷贝,可通过自定义 __copy__() 与 __deepcopy__(self, memo) 魔术方法,精确排除不需要复制的缓存、日志句柄或网络连接对象,避免全量递归带来的巨大开销。
相关专题:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。