Python 的赋值把我坑惨了,明明改了副本,结果原数据也变了,原来 copy 和 deepcopy 差这么多

Python 的赋值把我坑惨了,明明改了副本,结果原数据也变了,原来 copy 和 deepcopy 差这么多 一个让我怀疑人生的下午项目上线前一天产品经理跑过来说用户画像的数据结构要调整一下。原来的数据长这样user { name: 张三, age: 28, tags: [VIP, 高消费, 喜欢数码], profile: { city: 北京, occupation: 工程师 } }现在要把tags里的 喜欢数码 改成 科技爱好者还要在profile里加一个level: gold。我在脑子里想了一下逻辑不能直接改原始数据因为其他地方还在用。于是决定先复制一份在副本上改改完再上报。代码写得很顺手def transform_user(original): new_user original # 这不是复制这是起别名 new_user[tags][2] 科技爱好者 new_user[profile][level] gold return new_user result transform_user(user) print(user[tags][2]) # 输出科技爱好者 print(user[profile][level]) # 输出gold我盯着输出看了十秒钟。明明我改的是new_user为什么user也变了我又检查了一遍代码确认自己没有直接改user。但结果就是变了。user里的tags和profile都被改掉了。这意味着我从接口上报出去的数据是对的但本地缓存的原始数据已经被污染了。如果后面还有其他逻辑依赖这个原始数据整个流程都会乱掉。我当时的第一反应是Python 的赋值是不是出 Bug 了后来我才明白——出 Bug 的不是 Python是我。我根本不了解到底在干什么。先弄清楚赋值到底干了什么在 Python 里赋值做的事情很简单就一句话把变量名指向对象而不是复制对象。画个图就明白了。a [1, 2, 3] b a这段代码里发生的事情是这样的Python 先在内存里创建一个列表对象[1, 2, 3]然后把名字a贴在这个对象上执行b a的时候把名字b也贴在同一个对象上内存里的情况是a ──┐ ├── [1, 2, 3] b ──┘a和b指向的是同一个盒子。你往盒子里放东西不管用a还是b操作结果都一样——因为盒子里就那一份数据。这就是为什么b[0] 99之后a[0]也变成了 99。这不是 Python 的问题。所有编程语言的赋值都是这个逻辑——变量名是引用不是容器。那怎么才能复制一份呢浅拷贝只拷贝最外面一层Python 里有个copy模块提供了copy()方法。import copy a [1, 2, 3] b copy.copy(a) # 浅拷贝 b[0] 99 print(a) # [1, 2, 3] 没变 print(b) # [99, 2, 3] 变了这次a没变因为copy.copy()创建了一个新的列表对象然后把原来列表里的元素引用复制了一份。内存示意图a ── [1, 2, 3] b ── [1, 2, 3] ← 新的列表但元素还是原来的元素a和b是两个不同的盒子了。改b[0]不会影响a[0]。但是——如果列表里的元素本身是可变对象呢a [[1, 2], [3, 4]] b copy.copy(a) b[0][0] 99 print(a) # [[99, 2], [3, 4]] 变了 print(b) # [[99, 2], [3, 4]]a又变了。为什么因为copy.copy()只复制了最外面那层列表里面的子列表[1, 2]和[3, 4]还是原来的那两个。内存图是这样的a ── [ 指向子列表A的引用, 指向子列表B的引用 ] b ── [ 指向子列表A的引用, 指向子列表B的引用 ] ↑ ↑ └── 两个列表共享同一组子列表 ──┘b[0]和a[0]指向的是同一个[1, 2]对象。所以你改了b[0][0]其实改的是那个共享的子列表。这就是浅拷贝的问题——只拷贝一层。对于嵌套的数据结构里面的东西还是共享的。回到我那个用户画像的场景现在你明白我为什么翻车了吧new_user original # 这根本不是拷贝是别名shared new_user copy.copy(original) # 浅拷贝外层字典是新的用浅拷贝之后new_user[tags][2] 科技爱好者 # tags 是共享的原数据变了 new_user[profile][level] gold # profile 是共享的原数据变了tags和profile都是可变对象列表和字典浅拷贝只复制了外层的字典里面的列表和字典还是原来那些。所以改new_user的嵌套数据照样污染original。深拷贝真正的完全复制解决问题的方法很简单——用深拷贝import copy def transform_user(original): new_user copy.deepcopy(original) # 深拷贝 new_user[tags][2] 科技爱好者 new_user[profile][level] gold return new_userdeepcopy()会递归地复制所有层级的对象。不管是几层嵌套全部给你复制一份全新的。内存图original ── { tags: ── [VIP, ...], profile: ── {...} } new_user ── { tags: ── [VIP, ...], profile: ── {...} } ↑ 全新的列表 ↑ 全新的字典 └── 没有任何共享的数据 ──────┘改了new_user里面的任何东西original纹丝不动。深拷贝的代价是什么速度慢内存大。deepcopy要遍历整个数据结构把所有东西都复制一遍。如果数据结构很深、很大这个操作会很耗时内存占用也会翻倍。但如果数据正确性比性能更重要——比如你的数据要被多个环节反复使用——那这点代价值得花。三种方式的完整对比操作写法复制了几层嵌套数据是否共享适用场景赋值b a0层是完全共享只读、不需要独立数据浅拷贝copy.copy(a)1层是内层数据共享只有一层的数据结构深拷贝copy.deepcopy(a)所有层否完全独立嵌套结构、需要完全隔离再看一张更直观的对比图原始数据a [1, [2, 3], 4] 赋值 b a ── a 和 b 指向同一个列表 浅拷贝 b copy.copy(a) ── 新列表但内部的 [2,3] 还是同一个 深拷贝 b copy.deepcopy(a) ── 全部都是新的哪些情况需要特别注意情况一字典里的值是列表config { servers: [192.168.1.1, 192.168.1.2], timeout: 30 } # 错误的写法 backup config backup[servers].append(192.168.1.3) # config[servers] 也跟着变了配置被污染 # 正确的写法 backup copy.deepcopy(config) backup[servers].append(192.168.1.3) # config 不变情况二类实例的属性嵌套class Team: def __init__(self): self.members [] self.leader {name: 李四} team_a Team() team_b copy.copy(team_a) # 浅拷贝 team_b.members.append(王五) # 会影响 team_a team_b.leader[name] 赵六 # 会影响 team_a team_c copy.deepcopy(team_a) # 深拷贝 team_c.members.append(孙七) # team_a 不受影响情况三函数参数的默认值这也是一个经典的 Python 坑def add_user(user, tags[]): # 默认列表是共享的 tags.append(user) return tags print(add_user(张三)) # [张三] print(add_user(李四)) # [张三, 李四] ← 不是预期的 [李四]这里tags[]在函数定义时只创建一次所有调用共享同一个列表。正确的写法def add_user(user, tagsNone): if tags is None: tags [] tags.append(user) return tags什么时候该用浅拷贝什么时候用深拷贝浅拷贝的适用场景数据结构只有一层比如列表里的元素都是不可变对象你只修改最外层的结构不改里面的元素你要性能而且确定不会污染原始数据# 合适只有一层的列表 a [1, 2, 3, 4] b copy.copy(a) b.append(5) # a 不变没问题 # 不合适嵌套列表 a [[1, 2], [3, 4]] b copy.copy(a) b[0].append(3) # a 变了出问题深拷贝的适用场景数据结构任意嵌套你需要完全独立的数据副本数据要传给多个下游处理互相不能干扰# 用户画像、配置信息、状态快照——这些都用深拷贝 snapshot copy.deepcopy(current_state)有没有更快的深拷贝deepcopy()有个问题碰到重复引用的对象它会记录已经复制过的对象避免无限递归。这个记录过程本身也有开销。如果你的数据结构很简单且确定可以手写复制逻辑# 对于确定结构的字典手动复制比 deepcopy 快 def copy_user(user): return { name: user[name], age: user[age], tags: user[tags].copy(), # 手动浅拷贝列表 profile: user[profile].copy() # 手动浅拷贝字典 }但这只适合结构固定的数据。通用的场景还是用deepcopy最省心。那天后来怎么样了后来我把transform_user里的copy.copy改成了copy.deepcopy问题解决了。本地缓存的原始数据完好无损上报的数据结构正确项目顺利上线。那个下午让我彻底记住了三件事赋值不复制——它只是贴标签浅拷贝只保一层——里面的东西还是共享的深拷贝才彻底——但代价是时间和内存从那以后每次写复制相关的代码我都会停下来想一想我的数据结构几层我要改哪一层我真的需要完全隔离吗想清楚了再写省得改完代码发现数据全乱了回头还得重来。一句话总结b a只贴了个标签东西还是同一份copy.copy(a)新盒子但盒子里的东西还是原来的copy.deepcopy(a)新的盒子新的东西谁也不碍谁选哪个看你的数据有几层嵌套看你要不要完全隔离。