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

📅 2026/7/31 14:44:41 👁️ 阅读次数 📝 编程学习
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里的tagsprofile都被改掉了。

这意味着:我从接口上报出去的数据是对的,但本地缓存的原始数据已经被污染了。如果后面还有其他逻辑依赖这个原始数据,整个流程都会乱掉。

我当时的第一反应是:Python 的赋值是不是出 Bug 了?

后来我才明白——出 Bug 的不是 Python,是我。

我根本不了解=到底在干什么。

先弄清楚赋值到底干了什么

在 Python 里,赋值=做的事情很简单,就一句话:

把变量名指向对象,而不是复制对象。

画个图就明白了。

a = [1, 2, 3] b = a

这段代码里发生的事情是这样的:

  1. Python 先在内存里创建一个列表对象[1, 2, 3]

  2. 然后把名字a贴在这个对象上

  3. 执行b = a的时候,把名字b也贴在同一个对象

内存里的情况是:

a ──┐ ├──> [1, 2, 3] b ──┘

ab指向的是同一个盒子。你往盒子里放东西,不管用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] ← 新的列表,但元素还是原来的元素

ab是两个不同的盒子了。改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 是共享的,原数据变了

tagsprofile都是可变对象(列表和字典),浅拷贝只复制了外层的字典,里面的列表和字典还是原来那些。

所以改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_user

deepcopy()递归地复制所有层级的对象。不管是几层嵌套,全部给你复制一份全新的。

内存图:

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, tags=None): 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,问题解决了。

本地缓存的原始数据完好无损,上报的数据结构正确,项目顺利上线。

那个下午让我彻底记住了三件事:

  1. 赋值不复制——它只是贴标签

  2. 浅拷贝只保一层——里面的东西还是共享的

  3. 深拷贝才彻底——但代价是时间和内存

从那以后,每次写复制相关的代码,我都会停下来想一想:

我的数据结构几层?我要改哪一层?我真的需要完全隔离吗?

想清楚了再写,省得改完代码发现数据全乱了,回头还得重来。


一句话总结:

  • b = a:只贴了个标签,东西还是同一份

  • copy.copy(a):新盒子,但盒子里的东西还是原来的

  • copy.deepcopy(a):新的盒子,新的东西,谁也不碍谁

选哪个?看你的数据有几层嵌套,看你要不要完全隔离