一个让我怀疑人生的下午
项目上线前一天,产品经理跑过来,说用户画像的数据结构要调整一下。
原来的数据长这样:
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_user
deepcopy() 会递归地复制所有层级的对象。不管是几层嵌套,全部给你复制一份全新的。
内存图:
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):新的盒子,新的东西,谁也不碍谁
选哪个?看你的数据有几层嵌套,看你要不要完全隔离。