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

15-浅拷贝深拷贝在C层面的真相(上)-copy模块源码解读

文章目录

  • 浅拷贝深拷贝在 C 层面的真相(上):copy 模块源码解读——为什么 `.copy()` 只复制一层
    • 导入语
    • 1 ~> `copy` 模块源码入口——`copy()` 函数的整体结构
      • 1.1 `copy` 模块的核心流程
      • 1.2 三种拷贝路径
    • 2 ~> 列表的 `.copy()` 在 C 层做了什么
      • 2.1 C 实现
      • 2.2 C 层的——内存图
    • 3 ~> 字典的 `.copy()` 类似——引用传递
    • 4 ~> 如果不想引用计数+1——自定义 `__copy__`
    • 思考 && 总结
    • 结尾

浅拷贝深拷贝在 C 层面的真相(上):copy 模块源码解读——为什么.copy()只复制一层

📖文章简介:前面在第一板块我们讲了深浅拷贝的 Python 行为——.copy()只复制一层、deepcopy()递归复制每一层。现在进入源码阶段:copy模块到底做了什么?本文逐行解读copy.py的核心源码——copy()函数通过查找类型的__copy__魔术方法或copyreg分发器来决定"这个类型怎么复制"。对于列表,__copy__就是list.copy()——创建新的 C 数组,但内部指针指向的还是原对象。用图文解释"拷贝第一层"在 C 数据结构里到底长什么样——ob_item数组是新分配的,但数组里的指针指向的是同一个堆对象。


🎬 个人主页:源码骑士

专栏传送门:《Android开发基础》《python基础课程》

⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂


🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"


导入语

前面讲 Python 变量和可变对象的时候提到过:.copy()是浅拷贝——只复制第一层。当时我们画了内存图、跑了id()验证。现在进入第二板块——源码拆解——我们把这个问题从 Python 层推到 C 层:浅拷贝到底在 C 的数据结构里做了什么?为什么底层的ob_item指针数组是新的,但数组里的指针却指向同一个堆对象?

这篇拆copy.py的核心逻辑,下篇拆deepcopy的递归与memo字典。


1 ~>copy模块源码入口——copy()函数的整体结构

1.1copy模块的核心流程

打开 CPython 源码中的Lib/copy.py,核心函数copy(x)的整体流程如下:

# 简化后的 copy.copy() 源码defcopy(x):cls=type(x)# 步骤1:检查类型是否有自定义的 __copy__ 方法copyfunc=getattr(cls,"__copy__",None)ifcopyfuncisnotNone:returncopyfunc(x)# 步骤2:检查 copyreg 分发器中是否有注册reductor=dispatch_table.get(cls)ifreductorisnotNone:rv=reductor(x)# 步骤3:按类型做默认处理ifisinstance(x,list):returnx.copy()# → PyList_GetSlice → 浅拷贝elifisinstance(x,dict):returnx.copy()# → 字典的浅拷贝elifisinstance(x,set):returnx.copy()# ... 更多内置类型

1.2 三种拷贝路径

路径说明
__copy__魔术方法自定义类可以覆写这个方法来定义"怎么浅拷贝自己"
copyreg.dispatch_table注册表——类似 Java 的 SPI 发现,覆盖默认行为
内置类型默认处理列表、字典、集合都有 C 层面的内置.copy()方法

这三条路径保证了copy.copy()能适用于几乎所有 Python 对象——包括你自定义的类。


2 ~> 列表的.copy()在 C 层做了什么

2.1 C 实现

列表的.copy()方法在 C 层实现为PyList_GetSlice(一个通用的切片取子列表函数,lst[:]等价于lst.copy())。

PyObject*PyList_GetSlice(PyObject*a,Py_ssize_t ilow,Py_ssize_t ihigh){PyListObject*src=(PyListObject*)a;PyListObject*dest;Py_ssize_t i,len;// ...dest=(PyListObject*)PyList_New(len);// ★ 分配一个新的列表对象for(i=0;i<len;i++){PyObject*v=src->ob_item[i];// ★ 取出原列表的这个指针Py_INCREF(v);// ★ 引用计数 +1dest->ob_item[i]=v;// ★ 新列表中存的是同一个指针!}return(PyObject*)dest;}

关键细节在最后三行:

  1. Py_INCREF(v)—— 原列表中的每个元素的引用计数 +1(表示"有另一个变量也指向我了")
  2. dest->ob_item[i] = v—— 新列表中的指针数组只是复制了引用,没有创建新对象

2.2 C 层的——内存图

原列表 lst: 新列表 lst2=lst.copy(): ┌──────────────┐ ┌──────────────┐ │ ob_size:3│ │ ob_size:3│ │ allocated:3│ │ allocated:3│ │ ob_item ────→┼──[ptr0][ptr1][ptr2]│ ob_item ────→┼──[ptr0][ptr1][ptr2]└──────────────┘ │ │ │ └──────────────┘ │ │ │ ▼ ▼ ▼ │ │ │[1,2][3,4][5,6]│ │ │ ▼ ▼ ▼ 同一对象!同一个!

两个ob_item数组是两块独立的内存——PyList_New为新列表分配了独立的 C 数组。但数组里的每个指针指向的是同样的堆对象。所以:

  • lst[0].append(999)——lst2[0]跟着变(因为它们指向同一个对象)
  • lst[0] = [7, 8]——lst2[0]不受影响(只改了指针,不影响 lst2 数组里的另一个指针)

3 ~> 字典的.copy()类似——引用传递

d1={"a":[1,2],"b":[3,4]}d2=d1.copy()d1["a"].append(999)print(d2["a"])# [1, 2, 999] ← d2 的 values 跟着变了

C 层同样采用Py_INCREF+ 指针复制——key 和 value 的指针都复制到新字典,但它们指向的还是原对象。


4 ~> 如果不想引用计数+1——自定义__copy__

importcopyclassDeepList:"""一个在浅拷贝时实际做深拷贝的列表包装器"""def__init__(self,data):self.data=list(data)def__copy__(self):# 自定义浅拷贝行为:我们决定"浅拷贝 = 递归复制所有子元素"returnDeepList(copy.deepcopy(self.data))def__repr__(self):returnf"DeepList({self.data})"dl1=DeepList([[1,2],[3,4]])dl2=copy.copy(dl1)# 调用了我们写的 __copy__dl1.data[0].append(999)print(dl2.data[0])# [1, 2] ← 不受影响,因为我们深度拷贝了!

像 Django 的QuerySet内部就是这样自定义拷贝行为的——它不想让多个变量共享同一个数据库查询结果。


思考 && 总结

浅拷贝的 C 层原理就三句话:

  1. copy.copy()调用类型的__copy__方法,列表的默认实现是PyList_GetSlice
  2. C 层为新列表分配了独立的ob_item数组,但数组中的每个指针是通过Py_INCREF复制而来——指向的还是原来堆里的对象。
  3. 这就是"只拷贝第一层"的根源——元素不是在 C 数组里,而是在数组中引用的外部对象里。

下篇深入deepcopy——递归是怎么实现的、memo字典如何防止循环引用导致死循环、以及 Numpy 数组等 C 类型为什么不能deepcopy


结尾

各位小伙伴,上篇完毕。感谢阅读!

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

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

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

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

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

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

🗡️寄语:知道指针在哪里,才知道内存是怎么共享的。

结语:浅拷贝只复制了指针数组,没复制指针指向的对象。下篇deepcopy递归复制所有层——直到碰到不可变对象和memo,不见不散!一键四连!

http://www.zskr.cn/news/1525747.html

相关文章:

  • 2026年6月最新版内江正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • 16-浅拷贝深拷贝在C层面的真相(下)-deepcopy递归与memo字典
  • WarcraftHelper完整指南:如何让魔兽争霸3焕然一新的终极解决方案
  • BiliRaffle:让B站UP主告别手动抽奖的终极解决方案
  • 告别拍脑袋估算:用RUSLE模型+QGIS,5步搞定土壤侵蚀强度计算(附数据获取渠道)
  • 3种高效方法在macOS上完美安装IINA专业播放器
  • 17-slots为什么有时反而更慢-属性查找的底层路径与描述符协议
  • 5步创新方案彻底解决CAD字体同步难题
  • ChatGPT API实战入门:从401报错到生产级对话服务
  • LLM 验证代码题解:从输出校验到逻辑等价判定的工程实践
  • 核心必背!【中药学】必背100题及解析(卷号:06121219_04)
  • 2026年云端保姆级流程:如何部署OpenClaw?Token Plan配置及大模型API Key接入
  • Claudesidian:打造AI驱动的第二大脑,让知识管理从未如此简单高效
  • Java Web WEB旅游推荐系统系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】
  • 跨平台BongoCat交互式桌宠:从事件捕获到视觉反馈的实时响应机制
  • 2026年6月最新版晋城正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • 2026 Lazada流量转化导师客观测评榜单|商家选型避坑指南 - 品牌2026推荐
  • MPC8309 USB OTG驱动开发:从寄存器解析到实战避坑指南
  • CPython性能优化:如何深度理解Python解释器运行机制
  • 2026年6月最新版淮安正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • Java 开发者怎么用 Spring AI 接 DeepSeek?一个最小 Demo 跑通思路
  • 2026温州GEO优化公司权威评测报告:企业AI搜索选型避坑指南 - 品牌报告
  • 2026青岛奢侈品回收口碑老店 正规商家盘点 - 资讯速览
  • 多节点访问轮询算法:从基础到实战
  • 5000+戴森球计划工厂蓝图:从新手到专家的完整建造指南
  • 2026资源型EMBA客观测评:高管理性择校全指南 - 品牌2026推荐
  • CST中优化器中优化算法介绍
  • Apate文件伪装技术:数字安全时代的数据防护新方案
  • 终极CAJ转PDF跨平台解决方案:一站式解决学术文献格式兼容问题
  • 如何成为Switch文件解析高手:hactool完整入门指南