16-浅拷贝深拷贝在C层面的真相(下)-deepcopy递归与memo字典

文章目录

  • [浅拷贝深拷贝在 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 深度拷贝系统的核心。它的逻辑看起来简单:递归遍历每个对象、然后逐层复制。但这里有两个棘手的问题:

  1. 循环引用怎么办?------a 引用 b,b 引用 a。递归下去不就死循环了吗?
  2. 怎么"从零重建"一个对象? ------ 不调用 __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 的源码核心三点:

  1. memo 字典 记录"原对象 ID → 副本对象"------遇到循环引用时直接返回已有副本,而不是陷入死递归。这是 deepcopy 设计中最精妙的一环。
  2. _reconstruct__new__ 创建空壳 + __dict__.update(state) 恢复状态------绕过了 __init__,避免了重复创建文件、连接等副作用。
  3. 不可深拷贝的类型 直接抛 TypeError------这个限制是为了防止错误的资源复制。

结尾

深浅拷贝 C 层上下篇到此完结。感谢阅读!

源码骑士 --- 源码级拆解,从底层看透技术

👀 关注:跟博主一起从源码视角深耕底层原理

❤️ 点赞:让优质内容被更多人看见

收藏:核心知识点存好,随用随查

💬 评论:分享你的经验或疑问,一起交流

🔄 一键四连:别忘了给博主一键四连!

🗡️ 寄语:源码是技术的最后一道答案。

结语:deepcopy 的核心是 memo 字典 + _reconstruct------死循环问题被一个字典优雅地解决了。下篇拆一个更冷门的话题------__slots__ 为什么有时加了反而更慢。一键四连!