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;}关键细节在最后三行:
Py_INCREF(v)—— 原列表中的每个元素的引用计数 +1(表示"有另一个变量也指向我了")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 层原理就三句话:
copy.copy()调用类型的__copy__方法,列表的默认实现是PyList_GetSlice。- C 层为新列表分配了独立的
ob_item数组,但数组中的每个指针是通过Py_INCREF复制而来——指向的还是原来堆里的对象。 - 这就是"只拷贝第一层"的根源——元素不是在 C 数组里,而是在数组中引用的外部对象里。
下篇深入deepcopy——递归是怎么实现的、memo字典如何防止循环引用导致死循环、以及 Numpy 数组等 C 类型为什么不能deepcopy。
结尾
各位小伙伴,上篇完毕。感谢阅读!
源码骑士 — 源码级拆解,从底层看透技术
👀关注:跟博主一起从源码视角深耕底层原理
❤️点赞:让优质内容被更多人看见
⭐收藏:核心知识点存好,随用随查
💬评论:分享你的经验或疑问,一起交流
🔄一键四连:别忘了给博主一键四连!
🗡️寄语:知道指针在哪里,才知道内存是怎么共享的。
结语:浅拷贝只复制了指针数组,没复制指针指向的对象。下篇deepcopy递归复制所有层——直到碰到不可变对象和memo,不见不散!一键四连!
