文章目录
- [浅拷贝深拷贝在 C 层面的真相(下):deepcopy 递归与 memo 字典------为什么循环引用不会死循环](#浅拷贝深拷贝在 C 层面的真相(下):deepcopy 递归与 memo 字典——为什么循环引用不会死循环)
-
- 导入语
- [1 ~> `deepcopy` 核心源码------三步走](#1 ~>
deepcopy核心源码——三步走) -
- [1.1 简化后结构](#1.1 简化后结构)
- [1.2 `memo` 字典------防止死递归的核心](#1.2
memo字典——防止死递归的核心)
- [2 ~> 循环引用的处理------`memo` 实战演示](#2 ~> 循环引用的处理——
memo实战演示) -
- [2.1 代码演示](#2.1 代码演示)
- [2.2 内部处理过程](#2.2 内部处理过程)
- [3 ~> `_reconstruct`------不调 `init` 怎么创建对象](#3 ~>
_reconstruct——不调__init__怎么创建对象) -
- [3.1 问题](#3.1 问题)
- [3.2 `_reconstruct` 的工作原理](#3.2
_reconstruct的工作原理) - [3.3 如果不想用默认行为------覆写 `deepcopy`](#3.3 如果不想用默认行为——覆写
__deepcopy__)
- [4 ~> 不能深拷贝的类型](#4 ~> 不能深拷贝的类型)
- [思考 && 总结](#思考 && 总结)
- 结尾
浅拷贝深拷贝在 C 层面的真相(下):deepcopy 递归与 memo 字典------为什么循环引用不会死循环
📖 文章简介: 上篇拆了 copy.copy() 的 C 层实现------浅拷贝只复制 ob_item 指针数组,不复制指针指向的对象。本篇进入 deepcopy 源码核心:递归复制每一层的实现机制、memo 字典如何防止循环引用导致无限递归、_reconstruct 函数怎么"从零重建一个对象"。最后讨论特殊类型的深拷贝限制------为什么 file 对象、socket 对象不能深拷贝,以及 try/except 中 deepcopy 失败时的处理策略。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
上篇拆了 copy.copy() 的源码。现在进入 deepcopy------Python 深度拷贝系统的核心。它的逻辑看起来简单:递归遍历每个对象、然后逐层复制。但这里有两个棘手的问题:
- 循环引用怎么办?------a 引用 b,b 引用 a。递归下去不就死循环了吗?
- 怎么"从零重建"一个对象? ------ 不调用
__init__怎么拿到一个空壳子?
这两个问题 copy.deepcopy 都解决了。解决方式很精妙------memo 字典 + _reconstruct 函数。本文把它们拆开讲。
1 ~> deepcopy 核心源码------三步走
1.1 简化后结构
python
def deepcopy(x, memo=None, _nil=[]):
if memo is None:
memo = {} # ① 创建一个"已拷贝"记录表
# ② 如果这个对象已经拷贝过了(在 memo 里),直接返回它的拷贝
d = id(x)
if d in memo:
return memo[d]
# ③ 根据类型分发到对应的拷贝逻辑
cls = type(x)
# ...按类型处理:list、dict、set、自定义类等
# 处理完后把 (原对象ID → 新对象) 存入 memo
memo[d] = y # ← 关键!
return y
1.2 memo 字典------防止死递归的核心
memo 是一个字典,key 是原始对象的 id ,value 是它的深拷贝副本。
bash
deepcopy 开始后:
memo = {}
遇到 obj1: memo[123456] = obj1_copy
遇到 obj2: memo[789012] = obj2_copy
如果 obj1 又引用了 obj2 → 检查 memo → 已经拷贝过了 → 直接用 obj2_copy!
2 ~> 循环引用的处理------memo 实战演示
2.1 代码演示
python
import copy
# 构造循环引用
a = [1, 2]
a.append(a) # a[2] 就是 a 自己!
print(a) # [1, 2, [...]]
b = copy.deepcopy(a)
print(b) # [1, 2, [...]]
print(b is b[2]) # True ← b 里引用自己的结构也被保留了
print(b is a) # False ← 是一份独立复制
2.2 内部处理过程
bash
步骤1: deepcopy(a) 被调用,memo={}
步骤2: 发现 a 是列表,开始复制
步骤3: y = [](分配新列表对象) → memo[id(a)] = y
步骤4: 处理 a[0] → 1 是不可变对象 → 复制值 1
步骤5: 处理 a[1] → 2 是不可变对象 → 复制值 2
步骤6: 处理 a[2] → 它指向 a!
→ 检查 memo[id(a)] → 已经存储在步骤3里 → 返回 y(新列表对象)
→ y.append(y) → 新列表自己引用自己 ✓
如果不是 memo 字典,步骤 6 会无限重复步骤 1-6------死循环。memo 把循环引用问题解决了,同时保留了原对象图一样的拓扑结构。
3 ~> _reconstruct------不调 __init__ 怎么创建对象
3.1 问题
python
class Logger:
def __init__(self, name):
self.name = name
self.file = open(f"/var/log/{name}.log", "w") # 初始化时打开文件
logger = Logger("app")
logger_copy = copy.deepcopy(logger) # 我们不能调用 __init__!
如果 deepcopy 调用了 Logger.__init__,它会打开另一个日志文件------这不符合拷贝的语义。deepcopy 的做法是:不解构对象的初始化------直接恢复它目前的状态。
3.2 _reconstruct 的工作原理
python
# 简化版 _reconstruct 逻辑
def _reconstruct(cls, base, state):
# 创建一个"空壳子",不调用 __init__
obj = base.__new__(cls)
# 把 __getstate__ 返回的状态直接设置到对象上
if state is not None:
obj.__dict__.update(state)
return obj
其核心是调用 __new__ 而非 __init__------创建一个空壳对象,然后直接用 __dict__ 灌入原始状态。
3.3 如果不想用默认行为------覆写 __deepcopy__
python
import copy
class Config:
def __init__(self, data):
self.data = data
self._cache = None
def __deepcopy__(self, memo):
# 自定义深拷贝:不复制缓存(cache 可以重建)
new = Config(copy.deepcopy(self.data, memo))
new._cache = None # 缓存重置,不用从旧对象拷贝
return new
4 ~> 不能深拷贝的类型
| 类型 | 为什么不能 | 说明 |
|---|---|---|
文件对象 (open) |
关联了外部资源(文件句柄) | deepcopy 无法复制文件句柄 |
| 网络 socket | 关联了外部资源(连接) | 拷贝一个 socket 没有意义 |
| 线程、锁 | 关联了调度器状态 | 拷贝锁会导致未知行为 |
| C 扩展中的自定义对象 | C 层数据无法被 __dict__ 读取 |
需要实现 __deepcopy__ 或 __copy__ |
python
import copy
try:
f = open("test.txt", "w")
f2 = copy.deepcopy(f) # TypeError
except TypeError as e:
print(f"不能深拷贝文件对象: {e}")
思考 && 总结
deepcopy 的源码核心三点:
memo字典 记录"原对象 ID → 副本对象"------遇到循环引用时直接返回已有副本,而不是陷入死递归。这是deepcopy设计中最精妙的一环。_reconstruct用__new__创建空壳 +__dict__.update(state)恢复状态------绕过了__init__,避免了重复创建文件、连接等副作用。- 不可深拷贝的类型 直接抛
TypeError------这个限制是为了防止错误的资源复制。
结尾
深浅拷贝 C 层上下篇到此完结。感谢阅读!
源码骑士 --- 源码级拆解,从底层看透技术
👀 关注:跟博主一起从源码视角深耕底层原理
❤️ 点赞:让优质内容被更多人看见
⭐ 收藏:核心知识点存好,随用随查
💬 评论:分享你的经验或疑问,一起交流
🔄 一键四连:别忘了给博主一键四连!
🗡️ 寄语:源码是技术的最后一道答案。
结语:deepcopy 的核心是 memo 字典 + _reconstruct------死循环问题被一个字典优雅地解决了。下篇拆一个更冷门的话题------__slots__ 为什么有时加了反而更慢。一键四连!