Python深拷贝与浅拷贝全解析:从内存模型到实战避坑指南

发布时间:2026/9/24 20:54:44
Python深拷贝与浅拷贝全解析:从内存模型到实战避坑指南 先说个我自己的经历。有次在维护一个配置合并模块从文件里读出一个嵌套字典往里面塞了几个默认值然后传给后续的数据清洗流程。结果诡异的事情发生了原始配置对象在多处被引用某一处改了嵌套子字典的某个值其他所有引用方拿到的配置全变了线上日志出现了完全不符合预期的数据组合。花了一整晚排查最终定位到问题就出在用户在某处调用了copy.copy()——他以为拷贝了实际上只是复制了最外层引用内层字典仍然是同一块内存。从那天起但凡有同事跟我聊Python拷贝我都会先讲清楚一个核心概念可变对象与不可变对象的区别才是真正理解浅拷贝、深拷贝、乃至整个Python数据传递机制的地基。这篇文章不是Python入门教程而是面向已经写过一段时间、被各种改了A影响B折磨过的同学。目标是让你彻底搞清楚、copy.copy()、copy.deepcopy()到底分别做了什么为什么函数传参会改到外面的变量怎么判断自己该用哪种拷贝以及真实项目里那些最阴间的拷贝坑长什么样。内容会涉及一些CPython实现层面的行为但不深入C源码也会对比C、Java里的拷贝概念帮你们这些跨语言过来的同学建立映射关系。1. 从变量就是名字开始Python的内存模型跟你想的不一样1.1 C的盒子思维 vs Python的标签思维很多从C、Java转过来的同学对变量的理解是变量就是一个盒子里面装着值。这种思维在Python里会带来持续的误解。C里你写int a 10;就是在栈上分配了一块能容纳int的内存起名叫a把10这个值放进去。你再写int b a;又开了一个新盒子把10复制一份放进去。之后你改b 20a完全不受影响——这是盒子模型也是浅拷贝概念里值语义的基础。但Python完全不同。你可以把Python的变量理解成贴在对象上的标签。一个对象在堆内存里存在它有自己的类型、自己的引用计数而变量名只是一个指向它的标签。执行a [1, 2, 3]时实际上是先创建一个列表对象然后你拿标签a贴到它身上。执行b a时并没有创建新列表只是又拿了一个标签b贴到同一个列表对象上。所以a is b返回Truea[0] 99之后你打印b看到的是同一个列表的变化。这一点不理解后面全是糊涂账。Python的变量赋值从设计上就是引用语义和Java里对象类型的行为一致但和C默认的值语义完全相反。Java程序员可能觉得这不就跟对象引用一样吗是的但这只是第一步。麻烦在于Python里有些类型是值语义的比如整数、字符串、元组——你修改不了它们所以标签贴几个都无所谓。而列表、字典、集合这些是引用语义的能原地修改于是标签多了就出乱子。1.2 可变对象与不可变对象的本质区别这是本文最关键的分类。不可变对象包括int、float、str、tuple、frozenset、bytes它们一旦创建内部结构就不可被修改。注意我说的是不可被修改不是不能重新赋值。a 10; a 1看着是改了a实际是创建了一个新的整数对象11让a指向它原来的10如果没有其他引用就会被垃圾回收。可变对象包括list、dict、set以及你自己定义的类实例它们有内部结构可以通过方法原地修改list.append()、dict[key] value、set.add()。关键在于原地修改不会改变对象在内存中的地址所有引用这个对象的变量都会看到修改。为什么这个分类对拷贝如此重要因为不可变对象根本不需要深拷贝——你拷贝它们大家拿到的其实是同一个对象但由于不可变谁也不能原地改动它所以共享没有副作用。真正需要操心的全部集中在可变对象上。而且需要注意的是tuple这东西有点特殊它是一个不可变容器但里面可以装可变对象。t ([1, 2], a)这种元组你没法对t本身做append但可以通过t[0].append(3)修改里面那个列表。这就是为什么深拷贝面前元组也不能掉以轻心——判断一个对象是否需要深拷贝要看它内部是否包含可变对象的引用而不是只看它本身是否可变。1.3 为什么id()是你调Bug时最好的朋友id()函数返回对象的唯一标识在CPython里就是内存地址。排查拷贝问题最直接的手段就是用id()和is关键字看两个变量是不是指向同一个对象。平时可能不觉得这个函数有用遇到拷贝相关的诡异行为时它是第一把手术刀。比如你怀疑两个字典是不是共享了内层结构直接打import copy a {data: [1, 2, 3]} b copy.copy(a) print(id(a), id(b)) # 不同说明b是新的外层字典 print(id(a[data]), id(b[data])) # 相同说明内层list还是同一个id()相同就说明是同一个对象id()不同说明至少在这一层是分开的。加上is操作符能快速判断引用关系。调试拷贝问题不比什么高深技巧就是一层一层扒开看每一层里两个变量的id()一样不一样找出从哪一层开始共享了问题就定位了。2. 浅拷贝到底拷贝了啥只复制外壳内层引用全共享2.1 三种拷贝方式的演进逻辑先从最简单的说起。b a压根不算拷贝它就是多贴了个标签两个名字指向同一个对象。这是零拷贝。接下来是浅拷贝Python里的实现是copy.copy()但很多人不知道很多类型自己也内置了浅拷贝方法比如list.copy()、dict.copy()、set.copy()切片a[:]也是浅拷贝。浅拷贝会创建一个新的容器对象然后把原容器里的元素引用逐个复制到新容器里。注意是复制引用不是复制元素对象本身。这么说有点抽象画个图大家就懂了。假设原对象是a {data: [1, 2, 3], name: test}浅拷贝出来的b它自己是一个全新的字典对象内存地址和a不同。但b[data]和a[data]指向的是同一个列表对象b[name]和a[name]指向同一个字符串对象对不可变对象来说这无所谓。所以浅拷贝的语义是外层副本是新的内层所有东西仍然共享。修改b[data].append(4)a[data]也会看到[1, 2, 3, 4]但如果给b[name] new_name只是把b里的标签重新指向了一个新字符串a完全不受影响因为这只是改变了b字典内部的一个槽位指向并没有改动共享的那个字符串对象。2.2 为什么切片a[:]不是深拷贝a[:]、list(a)、a.copy()这几个方式网上资料一堆但核心只有一句话它们全是浅拷贝。我见过不少人写new_list old_list[:]然后自信满满地认为我复制了一份随便改。结果rows [[1, 2], [3, 4]] rows_copy rows[:] rows_copy[0][0] 100 print(rows) # [[100, 2], [3, 4]]原来的rows也变了原因就是rows_copy只是新列表里面两个元素还是原来那两个子列表的引用。你改rows_copy[0][0]动的是共享子列表的内容。这个例子我每次课堂都会拿出来讲因为太具有迷惑性了——顶层操作看起来像拷贝换个层级就露馅。这里有个判断技巧你到底改的是容器里的槽位还是槽位指向的对象内部。前者只会影响当前容器后者会顺着共享引用波及所有引用方。浅拷贝只隔断了槽位操作隔不断内部修改。2.3 列表copy()、dict.copy()、set.copy()是浅拷贝这很正常你可能想吐槽Python为什么把.copy()设计成浅拷贝而不是深拷贝为什么总让新手中招其实这是有意为之浅拷贝开销小、速度快而且很多场景根本不需要深拷贝。比如你要遍历一个列表的同时往另一个地方追加当前元素浅拷贝已经能把容器本身备份一份了拷贝容器内部那些不可变元素和不可变键值谁都不会改它们何必要深克隆所有内容所以浅拷贝是Python世界的默认选择。只有明确知道内部嵌套了可变对象、且你要独立修改它们时才需要升级到深拷贝。这也是为什么官方文档里copy模块的定位是shallow and deep copy operations浅拷贝排在前面——因为它更常用。这是一个务实的语言设计决策理解了它你就不会抱怨Python怎么这么坑反而会想清楚自己在哪个场景。2.4 自定义对象的浅拷贝行为__copy__到底默认做了什么先说结论copy.copy()用于自定义类实例时默认行为是创建一个新实例但不调用__init__然后把这个实例的__dict__也就是instance.__dict__属性字典用浅拷贝的方式复制一遍。也就是说顶层对象是新的但实例属性里如果引用了可变对象仍然是共享的。看一个常见场景class Config: def __init__(self): self.hosts [192.168.1.1] self.timeout 30 c1 Config() c2 copy.copy(c1) c2.hosts.append(10.0.0.1) print(c1.hosts) # [192.168.1.1, 10.0.0.1]被改了这个行为合理吗合理。因为默认浅拷贝是通用兜底方案它不知道你的类内部哪些属性是语义上可共享的哪些必须独立。你要精确控制可以在类里定义__copy__()方法自己决定怎么拷贝。比如def __copy__(self): new Config() new.timeout self.timeout new.hosts self.hosts.copy() # 指定要深一层 return new2.5 表格总结浅拷贝后各种操作的影响范围操作是否影响原对象原因修改浅拷贝容器自身的槽位如b[0] x、b[key] x不影响只是改变新容器的引用槽位修改浅拷贝容器里的可变元素内部如b[0].append(x)、b[data].add(x)影响内层可变对象还是同一个删除浅拷贝容器里的元素del b[0]不影响只是修改新容器结构对不可变元素赋值如b[0] 5原元素是int不影响不可变对象无法原地修改赋值只是换引用记住这张表浅拷贝的行为边界就清晰了。本质上就是浅拷贝拦得住第一层操作拦不住第二层及以下。3. 深拷贝不止复制它维护了一张记忆表3.1 深拷贝的核心逻辑遇到可变对象就递归复制copy.deepcopy()的逻辑可以这样理解从根对象开始遍历整个对象图遇到一个可变容器就新建一个容器然后对里面的每个元素递归执行同样的判断如果是不可变对象直接复用因为没必要复制如果是可变对象再创建一个副本如果元素是更深层的容器继续递归。最终产物是原对象的一个结构相同但内容完全独立的新对象图。为什么不可变对象在深拷贝里也是直接复用因为深拷贝的目的是消除共享带来的副作用而不可变对象根本没有副作用你复制它反而是浪费内存和时间。CPython里对str、int、float等类型做了缓存优化深拷贝时直接返回原对象id()都不变。复杂的是如何处理那些不可变容器里的可变元素。前面说过元组t ([1, 2], a)它本身不可变但里面的列表是变的。深拷贝的语义要求如果深拷贝一个元组理想情况下应该返回一个新的元组但CPython对深拷贝元组有一种优化——先看元组里的元素是否全不可变如果是直接返回原元组如果里面存在可变对象才会递归复制这些可变部分生成一个新元组。所以你深拷贝t之后如果里面那个列表内容变了原元组里的列表不变它们彻底分家。3.2 记忆表memo机制为什么深拷贝不会死循环这是深拷贝里最精妙的地方。对象图里可能存在循环引用a []; a.append(a)这是完全合法的Python。如果深拷贝遇到它时不做任何处理会陷入无限递归。但deepcopy能正常返回靠的就是一个内部memo字典它记录原对象id - 拷贝对象的映射。每次深拷贝一个对象之前先查memo如果这个对象已经拷贝过了直接返回之前拷贝的结果。遇到循环引用第一次深拷贝a时先创建一个空列表a_copy并立刻注册到memoid(a)映射到a_copy。然后再递归深拷贝a的元素也就是a自己此时查memo发现已经拷贝过了直接返回a_copy。这样循环引用被优雅地解开拷贝出来的a_copy也保持着自引用关系。这也是为什么copy.deepcopy(a) is a返回False但copy.deepcopy(a)[0] is copy.deepcopy(a)这种自行深拷贝时结果不保证一致——两次独立的深拷贝各自维护自己的memo不会共享。理解memo机制也有实际用途如果出事儿的对象图特别大深拷贝报RecursionError或者你想优化重复深拷贝的开销可以手动复用deepcopy的memo参数memo {} obj_copy1 copy.deepcopy(obj, memo) obj_copy2 copy.deepcopy(obj, memo) # 第二次很多对象会直接命中memo速度极快这在某些对象图复杂的场景下能明显减少拷贝耗时代价是两次拷贝之间共享了某些对象如果你需要完全独立得权衡。3.3 深拷贝不是万能的文件句柄、socket、单例对象的深拷贝会出问题你以为深拷贝是完全复制但现实里有些东西是复制不了也复制不得的。文件对象open()返回的对象深拷贝会抛异常因为它内部持有操作系统级别的文件描述符Python不知道该不该复制这个描述符、复制了又是什么语义。网络连接socket对象类似。线程对象同理你不能复制一个线程让它变成两个线程。此外还有一些伪可变对象比如logging.Logger它们内部有状态有handlers深拷贝一个logger通常得到一个可以工作的新对象但某些单例模式设计的类全局就应当只有一个实例深拷贝反而破坏了这种约束。所以copy模块对很多内建类型有专门的处理比如对type对象、模块对象、weakref等直接返回原对象。在实际项目中如果你的类包含不可复制的资源要么在__deepcopy__()里自定义复制行为要么抛出异常提醒调用方不要瞎深拷贝。错误处理也是一种良好的设计总比默默产生一个半坏的对象强。3.4 自己写__deepcopy__掌握控制权的正确姿势如果你定义了业务类内部有一个线程池、一个数据库连接池、或者一个缓存你肯定不希望深拷贝把它们也复制一遍因为那可能意味着创建了一堆新连接资源管理直接崩掉。这时候必须自定义__deepcopy__class Service: def __init__(self, name): self.name name self.cache {} self.session create_db_session() # 不可复制 def __deepcopy__(self, memo): new Service.__new__(Service) memo[id(self)] new new.name self.name new.cache copy.deepcopy(self.cache, memo) # 缓存要深拷贝 new.session self.session # 会话直接共享不复制 return new__deepcopy__方法的签名里带着memo你必须正确地把memo[id(self)] new放在递归复制前保证循环引用机制正常工作。这个模式就像是给深拷贝定制一份复制策略清单——哪些属性要深拷贝、哪些属性要浅拷贝、哪些属性要原样共享全由你说了算。这是处理复杂对象图的终极方案。3.5 深拷贝的性能开销一个真实案例深拷贝听着很美好但有代价。对象图越大、嵌套越深、重复对象越多耗时和内存占用越高。我在一个数据分析任务里处理过一坨配置根字典有几千个键value里头又嵌套了三四层list和dict整个对象图可能有几十万个对象节点。对这样一件配置做一次deepcopy在我当时的笔记本上耗时约0.8秒左右。听起来不慢但这段代码在循环里被调用了上千次耗时直接变成十几分钟。优化方式一是在循环外先深拷贝一次存到多个变量里轮流用小心共享二是用copy.deepcopy的memo参数复用部分拷贝结果三是改造数据设计尽量让结构扁平化减少深层嵌套四是只在必要的分支上深拷贝其他分支浅拷贝就能搞定。归根结底深拷贝是最后的手段不是默认手段。你的业务里大量使用深拷贝先停下来想想设计是不是有问题。4. 函数传参里的拷贝陷阱默认参数、类属性和跨调用污染4.1 函数参数是传对象引用不是传值也不是传引用这是Python面试高频题也是实际出Bug重灾区。Python函数传参既不是C的值传递也不是C的引用传递准确说法是传对象引用。函数内部拿到的形参就是实参对象的另一个标签。如果实参是可变对象在函数内部执行param.append(x)、param[key] val这类原地修改外部变量会看到变化如果执行param new_value只是在函数局部把标签重新贴到别处外面的变量毫发无损。这个特性让很多人困惑我明明在函数里改了列表为什么外面跟着变反过来我明明给形参赋了新值为什么外面没变关键区别就在这里。记住一句话参数传递复制的是引用不是对象函数内部能不能影响外部取决于你是修改了引用的目标还是修改了引用本身。def add_item(lst, item): lst.append(item) # 修改传入列表外部可见 def try_reset(lst): lst [] # 局部重新绑定外部不可见 data [1] add_item(data, 2) print(data) # [1, 2] try_reset(data) print(data) # [1, 2]没变4.2 可变默认参数所有调用共享同一个对象这是Python世界最经典的坑之一def append_to(item, container[]): container.append(item) return container print(append_to(1)) # [1] print(append_to(2)) # [1, 2]第二个调用者拿到了上次的结果原因很简单默认参数[]在函数定义时只创建一次之后每次调用如果没有传入该参数用的都是同一个列表对象。这个列表被所有调用共享于是历史数据残留。解决方案有两个一是默认值设为None函数内部判断后新建列表二是用不可变默认值如()并在内部转成list。从根上说这就是可变对象共享导致的经典事故和你用浅拷贝导致内层共享是同一个本质问题。我曾经在一个配置文件解析函数里踩过这个坑因为那个函数同时被多个线程调用结果共享的默认列表在多线程下出现了数据错乱。后来把所有可变默认参数全部改成None模式问题彻底消失。检查自己的代码里有没有可变默认参数是一件五分钟但非常划算的事。4.3 类属性与实例属性的拷贝困惑另一个高频坑是类属性里的可变对象。self.data []写在__init__里每个实例都有独立的列表但如果你把列表直接挂在类上class Manager: tasks [] # 所有实例共享这个列表 m1 Manager() m2 Manager() m1.tasks.append(task1) print(m2.tasks) # [task1]m2也看到了初学者会非常困惑以为每个实例的tasks互不相干。实际上访问m1.tasks时由于实例没有自己的tasks属性Python沿着m1 - Manager的继承链找到了类属性。这个类属性列表是全局唯一的一份所有实例都引用它。要修复只能在__init__里定义实例属性self.tasks []覆盖类属性。这个坑和拷贝的关系微妙但紧密它本质也是共享可变对象的引用只不过共享的路径是类的__dict__而不是你主动调了copy.copy()。排查时思路完全相同——用id()看看到底是不是同一个对象。4.4 嵌套结构跨函数传播的污染案例把上面这些组合起来一个真实场景可以这样呈现配置模块负责加载一份嵌套字典校验模块和导出模块分别拿到它各自都可能往里追加一些中间结果。因为大家拿的都是同一个字典对象A模块加的运行状态字段B模块也能看到。某天你发现导出的数据里多了不该有的字段排查半天才意识到不是谁动了我的数据而是大家根本就在用同一份数据。这类问题的解决思路第一是理清数据的所有权——哪个模块该持有原始配置、哪个模块只允许读取、哪个模块需要拿到自己的副本去加工。第二是明确边界只读方无论如何不要写数据加工方如果需要改动嵌套结构要在入口处深拷贝出一份工作副本保证原始配置不被污染。不要等到出了Bug再到处找而是从一开始就约定好谁写谁拷贝。5. 跨语言对照C的拷贝构造、Java的克隆与Python拷贝的区别和联系5.1 C的深浅拷贝为什么是C式的C里深浅拷贝的概念源自值语义和指针的混合。类里如果有裸指针成员默认的拷贝构造函数是浅拷贝——它复制指针值本身导致两个对象指向同一块堆内存析构时double free或者悬挂指针。所以C程序员要自己写拷贝构造函数、重载赋值运算符实现深拷贝。这是C最痛苦的内存管理问题之一。Python的浅拷贝看起来和C的浅拷贝概念类似但触发机制完全不同C里obj2 obj1就可能触发拷贝构造如果类型是值语义而Python里obj2 obj1永远不会触发任何拷贝逻辑它只是引用。Python的拷贝必须显式调copy.copy()或copy.deepcopy()。所以如果你从C过来第一件事就是忘掉赋值就拷贝的直觉Python的赋值永远是贴标签。这个区别决定了你在C里要小心别多拷贝在Python里要小心别多共享。5.2 Java的clone()为什么总被诟病Python怎么看得更开Java里clone()是Object的protected方法需要实现Cloneable接口才能用默认行为是逐字段复制浅拷贝而且需要向上转型加SuppressWarnings(unchecked)写起来极其繁琐。想深拷贝只能自己递归复制或者用序列化这种奇技淫巧。相比之下Python的copy模块直接给你深浅两套方案还带记忆表处理循环引用同时允许在__copy__和__deepcopy__里自定义设计上确实干净不少。这也是Python适合快速开发的原因之一——它对拷贝这种通用需求提供了开箱即用的标准工具而不是让你每次都在类里手写一堆clone逻辑。但开箱即用也意味着你要真正理解深浅拷贝的语义否则工具的便利反而会放大错误。5.3 从零拷贝到需要时才复制性能与正确性的平衡热词里出现了零拷贝、ROS2零拷贝说明拷贝性能是一个普遍关注点。在追求极致性能的系统里人们想尽办法减少复制比如Linux的sendfile、mmap、C的move语义、Rust的所有权转移本质都是在说数据只保留一份别复制来复制去。Python的引用传递天然就是零拷贝——b a不复制任何东西。但零拷贝的代价就是共享带来的副作用。所以你的目标不是消灭拷贝也不是到处深拷贝而是在正确性和性能之间找平衡。我的经验法则是只读共享零拷贝加工必须独立副本小对象随便深拷贝大对象想清楚再拷贝对不可变对象天然零拷贝不纠结对可变对象先想清有没有人在同时写再决定用浅拷贝还是深拷贝。这个法则在Python、Java、C里都通用因为拷贝问题的本质和语言无关只和数据的所有权与生命周期有关。6. 实战排错几个让我印象深刻的拷贝Bug复盘6.1 矩阵初始化的星号陷阱[[0] * 3] * 3这个代码在Python圈子里已经快成传说了但它仍然值得拿出来复盘因为它把共享可变对象展示得淋漓尽致。外层* 3把同一个内层列表重复了三次你改matrix[0][0] 1得到的是[[1, 0, 0], [1, 0, 0], [1, 0, 0]]。用id()一查就明白matrix[0] is matrix[1]是True。正确写法是列表推导[[0] * 3 for _ in range(3)]每一次迭代都新建一个内层列表。这个坑我在实际代码评审里见过不下三次每次的场景都不同但本质都一样——大家用*操作符复制了引用忘了引用的复制不是值的复制。6.2 一份全局配置被多个模块各改各的引发的数据污染有一次系统里有一份全局配置字典包含各种服务地址、开关标志。A模块负责爬虫调度B模块负责数据清洗两个模块都会往配置里加自己的运行时状态。因为共享同一份字典A加的当前任务ID会被B的当前批次号覆盖日志里偶尔出现驴唇不对马嘴的任务关联。当时的排查链路先看代码里有没有人直接改配置——有但改的都是自己加的key似乎不影响别人再检查是否有哪一步做了拷贝——没有谁都是直接拿引用。问题很清楚虽然你只动自己的key但key写在同一个dict里你无法确保别人的key不和你冲突更不能保证没有第三个模块乱读。解决办法是每个模块入口深拷贝一份专属工作副本互相隔离。这个案例的关键教训共享是默认状态隔离是需要显式做的。不要假设我只读不写就安全——你无法控制别人的写。也不要假设我只读了配置配置不会变——它可能被别的模块改到面目全非。想隔离就得自己动手去deepcopy。6.3 多线程环境下共享缓存对象导致的超时故障另一个让我印象深的案例一个缓存模块用类属性保存了一个大的字典缓存多个工作线程并发访问。某天缓存的一个值是列表某个线程在更新时用了list.append()另一线程同时在做list.pop(0)数据出现竞态错乱某些任务一直拿到残缺数据。这类问题表面上是需要加锁的并发问题但根子是共享可变对象且边界不清晰。如果这个缓存对象在设计上就是不可变的比如更新时替换整个字典而不是原地修改很多竞态问题根本不会发生。这和拷贝其实是一个问题的两面你一旦决定共享就要承担并发控制你一旦决定隔离就要付出拷贝开销。在共享与隔离之间做选择是架构设计里的经典权衡。6.4 利用__deepcopy__隔离大型模型对象的工程实践有一类特殊场景数据库ORM模型的实例内部关联了一堆懒加载的子对象、会话状态、脏数据标记。对这类对象直接deepcopy要么复制出一大堆你根本不想要的内部状态要么因为会话冲突报错。我在一个迁移服务里处理过这类问题给模型类写了__deepcopy__规定业务字段深拷贝会话状态直接置空关联ID保留懒加载引用共享。这样拷贝出来的新对象就像一个干净的模板迁移逻辑在上面可以随便加工不用担心影响数据库会话。这个经验可以推广遇到复杂对象别硬着头皮deepcopy花十分钟写一个自定义__deepcopy__定义好拷贝边界之后所有调用方都可以放心用。这是一种值得投入的基础设施投资长期收益很高。7. 尾音我实际写代码时的几项经验清单写了这么多分享几条我在实际项目中反复用到的经验算是对这篇文章的一个人味收尾而不是那种罗列式总结。第一默认不拷贝。能读就不复制能共享就共享。Python的设计哲学本来就是引用共享跟这套体系对抗纯属自找麻烦。你只需要在数据被修改前的那一步主动划分好边界。第二用__copy__和__deepcopy__把你的类调教好。如果你写的是会被外部引用的库代码这两个方法一定要实现。否则调用方只能面对默认的浅拷贝行为或者深拷贝报错。把这俩方法实现了你的类就是拷贝友好型别人用着放心你也少接一堆issue。第三调试拷贝问题不要纠结直接用id()加copy模块写个最小复现。把问题数据简化成一个二三层嵌套的小结构跑几行代码看引用的分岔点十分钟基本都能定位。这比靠猜快得多。第四写测试的时候把拷贝行为测进去。凡是你的类有嵌套结构或者你的函数接收外部传入的列表、字典都写一个修改后原对象不变的用例。测试一旦覆盖了这个层面以后重构时拷贝相关的回归很容易被测试抓住。第五记住可变对象与拷贝的问题本质是数据所有权和生命周期的问题。语言特性只是表现形式。想清楚数据属于谁谁可以修改谁需要独立副本代码就不会出大乱子。Python的拷贝不难但它横跨了语言设计、内存模型、数据结构、并发安全好几个维度的知识。把这篇文章里的概念消化掉以后再遇到为什么改了这个那边也变了的灵异事件你心里基本就有底了。