当前位置: 首页 > news >正文

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

文章目录

  • 浅拷贝深拷贝在 C 层面的真相(下):deepcopy 递归与 memo 字典——为什么循环引用不会死循环
    • 导入语
    • 1 ~> `deepcopy` 核心源码——三步走
      • 1.1 简化后结构
      • 1.2 `memo` 字典——防止死递归的核心
    • 2 ~> 循环引用的处理——`memo` 实战演示
      • 2.1 代码演示
      • 2.2 内部处理过程
    • 3 ~> `_reconstruct`——不调 `__init__` 怎么创建对象
      • 3.1 问题
      • 3.2 `_reconstruct` 的工作原理
      • 3.3 如果不想用默认行为——覆写 `__deepcopy__`
    • 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 简化后结构

defdeepcopy(x,memo=None,_nil=[]):ifmemoisNone:memo={}# ① 创建一个"已拷贝"记录表# ② 如果这个对象已经拷贝过了(在 memo 里),直接返回它的拷贝d=id(x)ifdinmemo:returnmemo[d]# ③ 根据类型分发到对应的拷贝逻辑cls=type(x)# ...按类型处理:list、dict、set、自定义类等# 处理完后把 (原对象ID → 新对象) 存入 memomemo[d]=y# ← 关键!returny

1.2memo字典——防止死递归的核心

memo是一个字典,key 是原始对象的 id,value 是它的深拷贝副本

deepcopy 开始后: memo={}遇到 obj1: memo[123456]=obj1_copy 遇到 obj2: memo[789012]=obj2_copy 如果 obj1 又引用了 obj2 → 检查 memo → 已经拷贝过了 → 直接用 obj2_copy!

2 ~> 循环引用的处理——memo实战演示

2.1 代码演示

importcopy# 构造循环引用a=[1,2]a.append(a)# a[2] 就是 a 自己!print(a)# [1, 2, [...]]b=copy.deepcopy(a)print(b)# [1, 2, [...]]print(bisb[2])# True ← b 里引用自己的结构也被保留了print(bisa)# False ← 是一份独立复制

2.2 内部处理过程

步骤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 问题

classLogger: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的工作原理

# 简化版 _reconstruct 逻辑def_reconstruct(cls,base,state):# 创建一个"空壳子",不调用 __init__obj=base.__new__(cls)# 把 __getstate__ 返回的状态直接设置到对象上ifstateisnotNone:obj.__dict__.update(state)returnobj

其核心是调用__new__而非__init__——创建一个空壳对象,然后直接用__dict__灌入原始状态。

3.3 如果不想用默认行为——覆写__deepcopy__

importcopyclassConfig:def__init__(self,data):self.data=data self._cache=Nonedef__deepcopy__(self,memo):# 自定义深拷贝:不复制缓存(cache 可以重建)new=Config(copy.deepcopy(self.data,memo))new._cache=None# 缓存重置,不用从旧对象拷贝returnnew

4 ~> 不能深拷贝的类型

类型为什么不能说明
文件对象 (open)关联了外部资源(文件句柄)deepcopy无法复制文件句柄
网络 socket关联了外部资源(连接)拷贝一个 socket 没有意义
线程、锁关联了调度器状态拷贝锁会导致未知行为
C 扩展中的自定义对象C 层数据无法被__dict__读取需要实现__deepcopy____copy__
importcopytry:f=open("test.txt","w")f2=copy.deepcopy(f)# TypeErrorexceptTypeErrorase:print(f"不能深拷贝文件对象:{e}")

思考 && 总结

deepcopy的源码核心三点:

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

结尾

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

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

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

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

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

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

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

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

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

http://www.jsqmd.com/news/1013927/

相关文章:

  • 2026年6月最新版来宾正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • OpenGL基础
  • 2026 Lazada流量转化专家/机构中立测评榜单|商家全域选型指南 - 品牌2026推荐
  • MPC8245 DUART深度解析:从异步串口原理到寄存器编程实战
  • WarcraftHelper完整指南:如何让魔兽争霸3焕然一新的终极解决方案
  • 鸿蒙原生应用实战(五)ArkUI 图片拼接/长图生成:多图合并 + Canvas 绘制 + 导出分享
  • 5分钟掌握猫抓Cat-Catch:浏览器资源嗅探工具的完整使用指南
  • BiliRaffle:让B站UP主告别手动抽奖的终极解决方案
  • 告别拍脑袋估算:用RUSLE模型+QGIS,5步搞定土壤侵蚀强度计算(附数据获取渠道)
  • 终极BT下载加速指南:如何用trackerslist项目彻底告别龟速下载
  • 2026年6月最新版莱芜正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • 2026 广州合同诈骗罪专业律师推荐:合同纠纷变刑事?怎么选对辩护律师 - 互联网科技品牌测评
  • 存算一体芯片软件双模式:单字符驱动网络(普通CPU也能跑)
  • 17-slots为什么有时反而更慢-属性查找的底层路径与描述符协议
  • AIOps 智能容量预测与弹性伸缩联动:从经验估算到数据驱动,云资源的成本与性能平衡
  • PyTorch训练避坑实录:在AMD平台(DirectML)上跑代码,为什么我的优化器不工作了?
  • 5步创新方案彻底解决CAD字体同步难题
  • Neura获14亿美元C轮融资,人形机器人赛道从实验室迈向工厂!
  • 3种高效方法在macOS上完美安装IINA专业播放器
  • ChatGPT API实战入门:从401报错到生产级对话服务
  • 核心必背!【中药学】必背100题及解析(卷号:06121219_04)
  • 深入解析MPC8309 eSDHC中断机制:SDIO通信稳定性的关键
  • 5分钟快速上手:免费获取海量小说资源的完整书源配置方案
  • LLM 验证代码题解:从输出校验到逻辑等价判定的工程实践
  • 2026年6月最新版酒泉正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • 2026年云端保姆级流程:如何部署OpenClaw?Token Plan配置及大模型API Key接入
  • 消费级柔性机器人公司SoulX获融资,首款产品MoYa将带来家庭智能关护新体验!
  • 18-生成器不只是省内存(上)-yield的状态机模型与帧暂停
  • 合肥市庐江县 家电维修清洗|维小达|空调、冰箱、洗衣机、热水器、油烟机一站式维保清洗服务 - 维小达科技
  • 广州擅长合同诈骗刑事辩护律师排名参考:2026 年经济犯罪辩护实务观察 - 互联网科技品牌测评