资讯动态

Python 复制、浅拷贝、深拷贝:改副本为什么原对象也变了

发布时间:2026/10/27 10:31:38 来源:尧图企业网站定制
Python 里复制一个对象比想象中容易出错赋值不是拷贝浅拷贝只复制一半深拷贝才彻底。很多人踩过改了副本原对象也跟着变的坑根源就在没分清这三者。这篇文章不只讲清三者的区别还深入到深拷贝的memo机制、循环引用的处理、不可变对象的优化、以及自定义对象怎么控制拷贝行为——让你遇到任何复制场景都能一眼判断该用哪个。读完你会发现复制这个词在 Python 里至少有三个层次选错层次就是 bug本文就是要帮你在动手前先分清自己到底需要哪一层选对层级、一次写对。文章目录1. 引子改了副本原对象也变了2. 赋值不是拷贝3. 浅拷贝只复制外层4. 深拷贝递归复制4.1 循环引用靠 memo 不死循环4.2 不可变对象拷贝返回自身5. 自定义对象的拷贝控制6. 三者对比与选择指南7. 实战拷贝在 API 设计里的作用8. 完整示例一个会串数据的配置复制9. 参考链接10. 总结1. 引子改了副本原对象也变了看一段让人摸不着头脑的代码把一个嵌套列表复制一份改副本的内层元素原对象居然跟着变了。importcopy orig[[1,2],[3,4]]dupcopy.copy(orig)# 浅拷贝dup[0].append(99)# 改副本的内层print(orig -,orig)# [[1, 2, 99], [3, 4]] ← 原对象也变了orig - [[1, 2, 99], [3, 4]]为什么dup的修改会波及orig要解释清楚得先分清赋值、浅拷贝、深拷贝这三个完全不同的动作。它们的差异正是名字绑定对象模型见上一篇《变量与参数传递》在复制这件事上的直接延伸。2. 赋值不是拷贝赋值b a只是让b也指向a指向的同一个对象一个字节都不复制。要判断是不是同一个对象用is身份比较而不是值比较。a[1,2,3]ba# 赋值同一对象clist(a)# 这才是复制新对象print(b is a -,bisa)# True同一对象print(c is a -,cisa)# False不同对象print(c a -,ca)# True值相等b is a - True c is a - False c a - Trueb is a为True说明b和a是同一个对象改b就等于改a。想要真正复制得用拷贝。这里有个关键认知赋值是共享引用拷贝是创建新对象。很多 bug 的根源就是以为b a在复制其实它只是多贴了一张标签。判断到底复制了没永远用is看身份别用看值值相等不代表是同一个对象。另一个隐藏的伪拷贝是*运算符——它对可变对象复制的是引用不是对象本身row[0]*3# 一维没问题都是不可变的 0print(一维 ok:,row)# [0, 0, 0]grid[[0]*3]*3# 二维有坑三个 [0,0,0] 是同一个列表grid[0][0]1print(二维被串了:,grid)# [[1,0,0],[1,0,0],[1,0,0]]一维 ok: [0, 0, 0] 二维被串了: [[1, 0, 0], [1, 0, 0], [1, 0, 0]][[0]*3]*3里的*3复制的是同一个内层列表的引用改一行、三行都变。创建二维结构要用推导式grid2[[0]*3for_inrange(3)]# 每行独立grid2[0][0]1print(推导式独立:,grid2)# [[1,0,0],[0,0,0],[0,0,0]]推导式独立: [[1, 0, 0], [0, 0, 0], [0, 0, 0]]*对不可变元素如[0]*3是安全的因为0不可变、复制引用无所谓但对可变元素列表套列表*复制的引用会导致串行。这是继b a之后第二个以为复制了其实没复制的陷阱。3. 浅拷贝只复制外层浅拷贝shallow copy创建一个新的外层容器但里面的元素还是原对象的引用。对于只有一层元素的列表浅拷贝看起来和深拷贝没区别一旦元素本身是可变对象如嵌套列表问题就来了。importcopy orig[[1,2],[3,4]]shallowcopy.copy(orig)print(外层不同 -,shallowisnotorig)# True外层是新容器print(内层共享 -,shallow[0]isorig[0])# True内层还是同一个shallow[0].append(99)# 改内层元素 - 原对象也变print(orig -,orig)# [[1, 2, 99], [3, 4]]shallow.append([5,6])# 改外层 - 原对象不变print(orig 不变 -,orig)# [[1, 2, 99], [3, 4]]外层不同 - True 内层共享 - True orig - [[1, 2, 99], [3, 4]] orig 不变 - [[1, 2, 99], [3, 4]]结论浅拷贝后改外层互不影响改内层会互相影响。常见的浅拷贝方式有几种效果都一样x[1,2,3]print(list(x)isx,x[:]isx,x.copy()isx)# False False FalseFalse False Falselist(x)、x[:]切片、x.copy()都返回一个新列表是浅拷贝。dict有.copy()set也有.copy()——它们通通是浅拷贝不是深拷贝。很多人误以为dict.copy()是深拷贝改嵌套的d[list].append(...)照样串数据d{a:[1,2],b:[3,4]}d2d.copy()# dict 的 copy 也是浅拷贝d2[a].append(99)print(d:,d)# {a: [1, 2, 99], b: [3, 4]}内层被串d: {a: [1, 2, 99], b: [3, 4]}d.copy()只复制了字典外层a对应的列表还是同一个对象append就串了。set.copy()同理是浅拷贝不过 set 的元素都是不可变的一般感觉不到差异。记住一句话所有内置容器的.copy()、切片、list()/dict()/set()构造函数统一都是浅拷贝没有例外。各内置容器的复制方式一览容器浅拷贝写法深拷贝写法listlist(x)/x[:]/x.copy()copy.deepcopy(x)dictdict(x)/x.copy()copy.deepcopy(x)setset(x)/x.copy()copy.deepcopy(x)所有内置容器的快捷复制切片、.copy()、构造函数都是浅拷贝要深拷贝唯一入口是copy.deepcopy没有捷径。这也意味着只要涉及嵌套可变结构就别指望容器自带的.copy()帮你彻底隔离。官方文档copy模块 · 可变序列操作4. 深拷贝递归复制深拷贝deep copy用copy.deepcopy()会递归地复制所有嵌套对象副本和原对象完全独立改任何一层都不互相影响。importcopy orig[[1,2],[3,4]]deepcopy.deepcopy(orig)print(内层独立 -,deep[0]isnotorig[0])# True内层也是新对象deep[0].append(99)print(orig 不变 -,orig)# [[1, 2], [3, 4]]内层独立 - True orig 不变 - [[1, 2], [3, 4]]4.1 循环引用靠 memo 不死循环深拷贝还能正确处理循环引用对象引用自己因为它用一张备忘录memo记住已经复制过的对象不会陷入死循环importcopy lst[1,2]lst.append(lst)# lst 引用了自己dcopy.deepcopy(lst)# 不会死循环print(副本长度 -,len(d))print(副本自引用是副本自己 -,d[2]isd)# True副本长度 - 3 副本自引用是副本自己 - Truememo是一个id(原对象) - 副本的映射。深拷贝每复制一个对象就记进memo再遇到同一个对象通过循环引用或共享引用绕回来时直接返回memo里的副本而不是重新复制。这既防了死循环又保持了引用关系——原对象里两处指向同一个对象副本里这两处也指向同一个副本importcopy shared[1,2,3]orig[shared,shared]# 两个位置引用同一个对象dcopy.deepcopy(orig)print(深拷贝后仍共享:,d[0]isd[1])# True引用关系被保留深拷贝后仍共享: True浅拷贝没这个顾虑它本来就不复制内层但深拷贝必须用memo维护谁已经复制过了这正是copy.py源码里deepcopy函数带一个memo参数的原因。4.2 不可变对象拷贝返回自身注意对不可变对象tuple、str、int 等做浅拷贝或深拷贝通常直接返回对象本身——因为不可变复制没有意义importcopy t(1,2,3)sabcprint(copy.copy(t)ist,copy.copy(s)iss)# True TrueTrue True但这里有个反直觉的例外不可变的外层 可变的内层。tuple本身不可变浅拷贝直接返回自身但深拷贝会穿透进去把里面的可变对象也复制importcopy t(1,[2,3])# 不可变外层 可变内层print(浅拷贝返回自身:,copy.copy(t)ist)# Truedcopy.deepcopy(t)print(深拷贝后内层独立:,d[1]isnott[1])# True浅拷贝返回自身: True 深拷贝后内层独立: True所以深拷贝的精确含义是递归复制所有可变对象不可变对象则复用。元组不可变就复用但它里面藏着的列表可变就被复制了。理解这个穿透规则就不会对深拷贝元组居然内层独立了感到意外。深拷贝的实现原理可以用一句话概括深度优先遍历对象图遇到可变对象就复制并记进 memo遇到不可变对象就复用。这个对象图遍历的视角很有用——它解释了深拷贝为什么能处理任意复杂的嵌套结构因为它就是在遍历图、为什么能处理循环引用因为 memo 记住了访问过的节点、为什么比浅拷贝慢因为要遍历整张图。把对象看成节点、引用看成边、深拷贝看成遍历整张图深拷贝就彻底透明了。官方文档__copy__与__deepcopy__5. 自定义对象的拷贝控制自定义类的实例默认拷贝是浅拷贝复制实例但属性仍共享。要控制拷贝行为可以实现__copy__和__deepcopy__importcopyclassConfig:def__init__(self,name,data):self.namename self.datadatadef__deepcopy__(self,memo):# 自定义深拷贝name 复用不可变data 递归复制newConfig(self.name,copy.deepcopy(self.data,memo))returnnew cConfig(prod,{ports:[80,443]})dcopy.deepcopy(c)print(深拷贝后 data 独立:,d.dataisnotc.data)# Trueprint(d.data:,d.data)深拷贝后 data 独立: True d.data: {ports: [80, 443]}__deepcopy__让你精确控制哪些属性要深复制、哪些要复用。比如上面的name是不可变字符串直接复用data是嵌套字典递归复制。memo参数必须原样传给copy.deepcopy否则处理循环引用会失效。什么时候需要自定义类里持有不能被深拷贝的对象如文件句柄、线程锁、数据库连接时直接deepcopy会报错或复制出坏对象——这时就该写__deepcopy__跳过它们。绝大多数场景不需要交给默认的deepcopy就行。同理浅拷贝也能用__copy__控制importcopyclassConfig:def__init__(self,data):self.datadatadef__copy__(self):returnConfig(list(self.data))# 浅拷贝时把 data 复制一层cConfig([1,2,3])dcopy.copy(c)print(浅拷贝后 data 独立:,d.dataisnotc.data)# Trueprint(d.data:,d.data)浅拷贝后 data 独立: True d.data: [1, 2, 3]默认的浅拷贝只是复制实例、属性仍共享引用不写__copy__时d.data is c.data是 True。写了__copy__后就能控制哪些属性复制一层、哪些复用。__copy__控制浅拷贝__deepcopy__控制深拷贝两者配对使用。深拷贝还有一个边界有些对象根本无法被深拷贝比如线程锁、文件句柄——它们持有的是操作系统资源没有值可复制importcopy,threading lockthreading.Lock()try:copy.deepcopy(lock)exceptTypeErrorase:print(锁无法深拷贝:,type(e).__name__)# TypeError锁无法深拷贝: TypeErrorthreading.Lock没有实现深拷贝deepcopy直接抛TypeError。如果你的类里持有锁、连接这类不可复制的资源就必须自定义__deepcopy__跳过它们比如复用同一个锁。这也是为什么默认深拷贝不是万能的——遇到持有系统资源的对象要主动控制拷贝行为。6. 三者对比与选择指南操作新外层容器内层元素独立改副本影响原对象赋值b a否同一对象—是根本就是同一个浅拷贝copy.copy是否共享改内层会深拷贝copy.deepcopy是是否选择标准一句话元素全不可变int/str/tuple→ 浅拷贝就够深拷贝纯属浪费。有嵌套可变结构列表套列表、字典套列表且要独立修改 → 用deepcopy。只是想拿个快照读一读、不改→ 浅拷贝甚至赋值都行。性能敏感→ 深拷贝有递归开销能用浅拷贝就别用深拷贝。深拷贝的代价是性能它要递归遍历并复制每一层对象还要维护一张 memo 字典防循环引用所以比浅拷贝慢得多。对只有不可变元素的大列表list(x)这种浅拷贝就够用了用deepcopy纯属浪费——这也是选择时要把元素可不可变放在第一位的原因。几个容易踩的误区一是以为切片x[:]是深拷贝其实它和copy.copy一样是浅拷贝二是以为dict.copy()、set.copy()是深拷贝同样只是浅拷贝三是自定义类的实例默认拷贝也是浅拷贝——类里有嵌套可变属性又想真正隔离时要么实现__deepcopy__要么直接交给copy.deepcopy。深拷贝的性能开销值得量化认知。它要递归遍历每一层、每复制一个对象就查一次 memo、分配一次内存所以比浅拷贝慢得多——对一个嵌套层级深、对象数量多的大结构deepcopy可能比copy慢一个数量级。这正是能用浅拷贝就别用深拷贝的实证依据深拷贝不是免费午餐它用时间换来了彻底独立。所以选型的第一问永远是我的数据结构里有没有嵌套的可变对象而不是无脑上deepcopy。选型时问自己三个问题答案就是结论第一问需要复制吗只是想多一个名字访问同一份数据赋值b a就行别复制。第二问数据结构里有没有嵌套的可变对象没有全int/str/tuple→ 浅拷贝够有列表套列表、字典套列表→ 继续第三问。第三问副本要不要独立修改要 →deepcopy不要只读快照→ 浅拷贝。三个问题走下来赋值 / 浅拷贝 / 深拷贝的选择就定了。核心判断依据始终是元素可不可变和要不要独立修改而不是哪个更高级——浅拷贝不是深拷贝的低配版它们是应对不同场景的不同工具。再补一个实用的记忆锚点深拷贝 浅拷贝 递归复制内层。理解了深拷贝是浅拷贝的递归版两者就不再是平级的两个概念而是复制一层和复制所有层的关系——想清这个递进选型时就不会纠结。7. 实战拷贝在 API 设计里的作用拷贝不只是复制数据还是封装边界的一部分。一个类持有内部可变状态对外暴露时如果直接返回引用外部就能绕过类的方法篡改内部状态classBadStore:def__init__(self):self._items[1,2,3]defget_items(self):returnself._items# 返回内部引用危险bsBadStore()bs.get_items().append(99)print(内部状态被破坏:,bs._items)# [1, 2, 3, 99]内部状态被破坏: [1, 2, 3, 99]正确做法是返回副本把内部状态保护起来classStore:def__init__(self):self._items[1,2,3]defget_items(self):returnself._items[:]# 返回副本外部改不到内部storeStore()itemsstore.get_items()items.append(99)print(内部状态未被破坏:,store._items)# [1, 2, 3]内部状态未被破坏: [1, 2, 3]这就是防御式拷贝defensive copy类的 getter 返回内部可变对象的副本防止外部修改破坏封装。反之如果类的设计意图就是共享同一份数据比如缓存、视图那返回引用反而是对的。返回副本还是引用取决于这个状态归谁管——归类管就返回副本归调用方管就返回引用。这是拷贝在真实工程里最实用的场景之一。和拷贝相关的还有一个高频坑——类属性共享。可变对象写在类体里类属性会被所有实例共享classTeam:members[]# 类属性所有实例共享同一个列表t1Team()t2Team()t1.members.append(zhangsan)print(t2.members:,t2.members)# [zhangsan]被串了t2.members: [zhangsan]members []写在类体里是所有Team实例共享的同一个列表t1改、t2也变。正确写法是放进__init__让每个实例有独立的列表classGoodTeam:def__init__(self):self.members[]# 实例属性每个实例独立g1GoodTeam()g2GoodTeam()g1.members.append(lisi)print(g2.members:,g2.members)# []互不影响g2.members: []类属性共享、可变默认参数、浅拷贝串内层三个坑其实是同一个根源——可变对象被多个地方共享引用。理解了名字绑定对象和拷贝复制到哪层这三个坑就都能一眼看穿。8. 完整示例一个会串数据的配置复制模拟一个真实的坑复制一份带嵌套结构的配置结果改副本把原配置也改了。importcopyfromtypingimportOptionaldefclone_config(config:dict,independent:boolFalse)-dict:复制配置。independentTrue 用深拷贝否则浅拷贝。returncopy.deepcopy(config)ifindependentelsecopy.copy(config)base{host:127.0.0.1,ports:[80,443],# 嵌套的可变列表tags:{env:prod},# 嵌套的可变字典}# 场景 1浅拷贝改嵌套字段会串shallowclone_config(base)shallow[ports].append(8080)print(浅拷贝后 base ports -,base[ports])# [80, 443, 8080]被串了# 场景 2深拷贝完全隔离deepclone_config(base,independentTrue)deep[tags][env]devprint(深拷贝后 base tags -,base[tags])# {env: prod}不受影响浅拷贝后 base ports - [80, 443, 8080] 深拷贝后 base tags - {env: prod}场景 1 里ports是嵌套列表浅拷贝只复制了外层dictports仍是同一个列表append就串到了base。场景 2 深拷贝把tags也复制了改副本的tags不动原配置。回看这个真实 bug 的修法就一句话只要数据结构里嵌套了可变对象、且副本要独立修改就用deepcopy。反之扁平结构全不可变元素用浅拷贝就够省掉递归开销。回顾整篇复制三兄弟的区别本质是复制到哪一层赋值b a复制 0 层——根本就是同一个对象。浅拷贝copy.copy复制 1 层——外层是新容器内层还是引用。深拷贝deepcopy复制所有层——递归复制每个可变对象。记住这个层数视角任何复制场景都能一眼定位该用哪个。而判断到底复制到哪了标准动作永远是is比较身份——a is b为 True 就是共享为 False 才是真复制。这个动作简单却能在一秒内戳穿以为复制了其实没复制的绝大多数 bug。最后提醒一句克制原则别把复制当默认动作。大多数时候赋值b a就对了——Python 的对象模型本来就靠共享引用省内存无脑复制反而浪费。只有当你明确要改副本、不影响原对象时才需要浅拷贝明确要彻底独立嵌套结构时才需要深拷贝。判断标准始终是第 6 节那三问而不是复制更安全的刻板印象。把什么时候不复制想清楚和什么时候复制一样重要。9. 参考链接copy模块——浅拷贝与深拷贝的官方定义可变序列操作——list.copy等方法object.__copy__/object.__deepcopy__——自定义对象如何控制拷贝行为copy模块源码——深拷贝用 memo 处理循环引用的实现10. 总结Python 复制三兄弟速查卡 赋值 b a : 同一对象不复制改谁都变 浅拷贝 copy.copy : 新外层容器内层元素共享改内层会串 深拷贝 deepcopy : 递归复制可变对象完全独立 memo 机制 : 记 id→副本防循环引用、保引用关系 不可变对象 : copy 返回自身但深拷贝穿透到内层可变对象 自定义拷贝 : __copy__ / __deepcopy__ 控制复制行为 选型 : 扁平/不可变用浅拷贝嵌套可变要独立用 deepcopy赋值不是拷贝只是让两个名字指向同一对象用is判断身份别用。浅拷贝复制外层、共享内层深拷贝递归复制可变对象、完全独立。改副本改了原对象的坑几乎都是浅拷贝共享内层导致的。深拷贝用memo处理循环引用并保持引用关系不可变对象拷贝返回自身。自定义类默认浅拷贝可用__copy__/__deepcopy__精确控制。按元素是否嵌套、是否需要独立修改来选浅拷贝还是深拷贝。*运算符复制的是引用[[0]*3]*3会串行二维结构要用推导式创建。深拷贝有边界锁、文件句柄等系统资源对象无法复制需自定义__deepcopy__。防御式拷贝getter 返回副本保护内部状态返回引用还是副本取决于状态归谁管。层数视角记心法赋值复制 0 层、浅拷贝 1 层、深拷贝所有层用is验证身份。深拷贝本质是深度优先遍历对象图可变对象复制、不可变对象复用靠 memo 防循环引用。克制原则复制不是默认动作只有明确要独立修改时才复制否则赋值就足够。类属性共享、可变默认参数、浅拷贝串内层同源——都是可变对象被多处共享引用。内置容器的切片、.copy()、构造函数全是浅拷贝深拷贝唯一入口是copy.deepcopy。延伸阅读《Python 变量与参数传递》 讲名字绑定对象的地基模型《Python 垃圾回收》 讲引用计数与循环引用如何被回收两篇结合起来能彻底看懂复制背后的内存语义。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑