一个让我怀疑人生的下午
项目上线前一天,产品经理跑过来,说用户画像的数据结构要调整一下。
原来的数据长这样:
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 = a | 0层 | 是,完全共享 | 只读、不需要独立数据 |
| 浅拷贝 | 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,问题解决了。
本地缓存的原始数据完好无损,上报的数据结构正确,项目顺利上线。
那个下午让我彻底记住了三件事:
赋值不复制——它只是贴标签
浅拷贝只保一层——里面的东西还是共享的
深拷贝才彻底——但代价是时间和内存
从那以后,每次写复制相关的代码,我都会停下来想一想:
我的数据结构几层?我要改哪一层?我真的需要完全隔离吗?
想清楚了再写,省得改完代码发现数据全乱了,回头还得重来。
一句话总结:
b = a:只贴了个标签,东西还是同一份copy.copy(a):新盒子,但盒子里的东西还是原来的copy.deepcopy(a):新的盒子,新的东西,谁也不碍谁
选哪个?看你的数据有几层嵌套,看你要不要完全隔离。