☰
为什么拷贝会踩坑?memory_graph图解赋值、浅拷贝与深拷贝的3种行为
2026/10/15 17:29:34 网站建设 项目流程

【免费下载链接】memory_graph

Teaching tool and debugging aid in context of references, mutable data types, and shallow and deep copy.

项目地址:https://gitcode.com/gh_mirrors/me/memory_graph
点击查看免费下载

在 Python 中给一个变量赋值时,数据到底是被复制了,还是只共享了一份引用?b = a、copy.copy(a)、copy.deepcopy(a)三种"拷贝"行为各有差异,稍有不慎就会改出隐藏 Bug。开源工具memory_graph能把变量与数据之间的引用关系画成内存图,帮你一眼看清 Python 赋值、浅拷贝与深拷贝的 3 种行为,是学习 Python 数据模型(引用、可变性)的绝佳图解工具。

🚀 快速上手:两行代码画出内存图

先安装 memory_graph(需要 Graphviz):

pip install --upgrade memory_graph

核心 API 只有三个函数:

  • mg.show(data):把数据的结构可视化出来
  • mg.render(data, "out.png"):渲染成图片文件(也支持 pdf / svg / gv)
  • mg.block(...):执行函数后阻塞等待回车,方便逐步观察
import memory_graph as mg a = [1, 2] b = a # b 和 a 是同一份 list! mg.show(locals()) # 画出所有局部变量的引用关系

locals()会返回当前作用域所有局部变量的字典,是最常用的观察入口。下面用同一个嵌套列表,逐一图解 3 种行为。

🔍 行为一:赋值(Assignment)——什么都没拷贝

a = [4, 3, 2] b = a b.append(1) # 改 b 会同时改 a

这里b = a只是让b这个名字也指向a所引用的那个 list,两个变量共享同一份数据。print看不出来问题,但内存图一目了然:a和b两根箭头指向同一个 list 节点。

mg.render(locals(), 'mutable2.png')生成的图正是 mutable.py 的运行结果——图中dict里并列着a、b,箭头汇合到同一个 list 上:

💡 验证小技巧:id(a) == id(b)或a is b为True时,说明两者共享同一对象。

🧩 行为二:浅拷贝 copy.copy——只复制最外层

对一个嵌套列表做浅拷贝时,只有第一层是新列表,里面嵌套的子列表依然是共享的:

import copy a = [ [1, 2], ['x', 'y'] ] c1 = a # 行为一:赋值,全部共享 c2 = copy.copy(a) # 行为二:浅拷贝,只复制第一层 c3 = copy.deepcopy(a) # 行为三:深拷贝,完全不共享 mg.show(locals())

生成的图中有三个 list 顶层节点:c1与a共用一个,c2有自己的顶层 list,但它的两个子列表与a的子列表是同一批节点;只有c3的整棵子树都独立了。

这个例子来自 src/copies.py,可直接运行复现。

行为三:深拷贝 copy.deepcopy——整棵树都独立

copy.deepcopy(a)会递归复制所有层级,改c3的任何一处都不会波及a。在 Marimo 这类交互式环境中还能结合调用栈一起看四种拷贝(赋值、.copy()、自定义拷贝、deepcopy)的对比:

三种行为速查表:

写法行为顶层嵌套子对象
c1 = a赋值共享共享
c2 = copy.copy(a)(等价a.copy()、a[:]、list(a))浅拷贝新列表共享
c3 = copy.deepcopy(a)深拷贝新列表全部新对象

🎯 进阶:自定义拷贝(Custom Copy)

有时标准三种行为都不满足需求——比如对象里digits想复制、letters想共享。这时可以自己写拷贝方法,src/custom_copy.py 给出了范例:

def custom_copy(self): """ Copies 'digits' but shares 'letters'. """ c = copy.copy(self) c.digits = copy.copy(self.digits) return c

内存图中可以清楚看到:a和b两个对象各自拥有独立的digits列表,但letters指向同一个 list 节点:

🧊 易错点:不可变类型下,拷贝与赋值等价

tuple、int、str等不可变类型无法原地修改,"修改"时 Python 会自动产生新值,所以提前拷贝毫无意义。memory_graph 对此做了优化:copy.copy(a)和copy.deepcopy(a)对不可变类型都会退化为赋值,省时间也省内存:

混合了可变与不可变值的结构也一样——深拷贝会尽量少复制不可变部分,只保证每个可变值都有副本:

🐛 实战调试:在 IDE 里持续观察拷贝过程

拷贝相关的 Bug 往往在运行中才暴露。推荐做法:在调试器的watch里加入mg.render(locals(), "my_graph.pdf"),然后单步调试时 PDF 会实时更新所有局部变量的引用关系——下图就是在 Visual Studio Code 中调试拷贝行为时的效果:

另外两个高频陷阱,建议结合内存图对照记忆:

  • b += [300](原地改)会影响共享者a,但b = [400, 500](重新绑定名字)不会影响a
  • 函数传参时,参数与实参共享可变对象;给函数传c.copy()副本才能隔离修改

更多示例可在仓库的 src/ 目录中找到,例如 src/rebind.py(重绑定)、src/function_call.py(函数调用共享)。

📌 总结:建立正确的 Python 心智模型

问题答案
b = a拷贝了吗?没有,只是共享引用
改b会影响a吗?可变类型且共享时——会
要完全独立怎么办?copy.deepcopy(a)
如何直观确认谁和谁共享?mg.show(locals())画图

memory_graph 通过memory_to_nodes(memory_graph/memory_to_nodes.py)把内存中的对象按id()去重建图,正是"共享"与"拷贝"最可靠的判据。遇到拿不准的数据流,画一张图,胜过十次print。

【免费下载链接】memory_graph

Teaching tool and debugging aid in context of references, mutable data types, and shallow and deep copy.

项目地址:https://gitcode.com/gh_mirrors/me/memory_graph
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询