Python 的切片把我坑惨了,原来 `[:]` 是浅拷贝,而 `copy.deepcopy` 才是我的救命稻草

数据"备份"引发的血案

先从我上周差点被开除的事儿说起。

我们有个系统,每天半夜要处理一份大客户名单。名单是个列表,每个客户又是一个字典,装着各种信息。领导要求加个功能:在处理之前,先备份一下原始数据,万一处理出错还能恢复。

我一想,这还不简单?Python 里复制列表嘛,list.copy() 或者直接用切片 [:] 不就完事了?

于是我潇洒地写下了这行代码:

ini 复制代码
original_data = [
    {"name": "张三", "balance": 1000},
    {"name": "李四", "balance": 2000},
    {"name": "王五", "balance": 500}
]

# 优雅地备份一下,多么经典的切片操作
backup_data = original_data[:]

然后开始处理。处理逻辑很简单:把所有余额大于 1000 的客户,余额打九折。我一边处理 original_data,一边美滋滋地想------就算改出 bug 也不怕,反正有 backup_data 兜底。

结果第二天一来,发现数据全乱了。不止 original_data 乱了,连 backup_data 也跟着乱了。

我当场就懵了。明明备份了,怎么两个一起乱?

我检查了代码,确认自己没有写类似 backup_data = original_data 这样的引用赋值。我用的可是切片 [:] 啊,都说切片是复制,怎么复制了个寂寞?

后来我才知道,切片 [:] 确实复制了,但它只复制了最外面那一层。至于里面的字典、列表这些可变对象,它压根儿没动,只是把引用抄了一份。

这就好比你要复印一份文件,结果只复印了封面目录,里面的每一页还是指着原来那份。最后目录是新的,但里面的内容一改,大家都跟着变。

这件事之后,我终于把"浅拷贝"和"深拷贝"这六个字刻进了脑子里。

引用 vs 拷贝:别再傻傻分不清

要搞懂切片为什么"背刺"你,得先明白 Python 里"赋值"和"拷贝"到底在干嘛。

先看最基础的:

css 复制代码
a = [1, 2, 3]
b = a
b[0] = 999
print(a)  # 输出 [999, 2, 3]

这个大家应该都懂。b = a 不是复制,是贴标签。a 和 b 指向同一个盒子,你改盒子里的东西,另一个当然看得见。

那切片呢?

css 复制代码
a = [1, 2, 3]
b = a[:]
b[0] = 999
print(a)  # 输出 [1, 2, 3],a 没变

这回对了。因为切片会新建一个列表,然后把原列表里的元素塞进去。所以 a 和 b 是两个不同的盒子。

问题在于------如果盒子里装的不是数字、字符串这种"不可变"的东西,而是列表、字典这种"可变"的东西呢?

css 复制代码
a = [[1, 2], [3, 4]]
b = a[:]
b[0][0] = 999
print(a)  # 输出 [[999, 2], [3, 4]]------a 被改了!

看到了吗?切片只复制了外层的列表,但外层列表里的两个子列表,还是原来那两个 。b[0] 和 a[0] 指向的是同一个子列表。所以你改 b[0] 里面的东西,a[0] 当然也跟着变。

这就是传说中的 "浅拷贝" ------只拷贝最外层,里面的东西还是共用。打个比方,浅拷贝就像是复印了公司的通讯录,通讯录的封面是新复印的,但里面的每一个电话号码还是原来那张纸上的,没人帮你重新抄一遍。

切片是浅拷贝,但不只是切片

踩坑之后我才发现,Python 里"浅拷贝"到处都是。

  • 列表的 copy() 方法 :a.copy(),和 a[:] 一毛一样
  • 字典的 copy() 方法 :d.copy(),只复制键值对的引用
  • list() 构造函数 :list(a),也是浅拷贝
  • 解包操作 :b = [*a],还是浅拷贝
  • 集合推导式 :{x for x in a},依然浅拷贝

基本上,所有你不假思索拿来"复制"东西的操作,十有八九都是浅拷贝。

这些操作在日常写代码的时候,你根本感觉不到问题------除非你往里装的是可变对象。一旦装了列表、字典、自定义对象,浅拷贝就变成了定时炸弹,你不知道什么时候一改就炸。

救命稻草:copy.deepcopy

那到底怎么才能"完整地、彻底地"复制一份数据,让新数据和旧数据没有半毛钱关系?

答案是 copy.deepcopy,Python 标准库 copy 模块里的深拷贝函数。

css 复制代码
import copy

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0][0] = 999
print(a)  # 输出 [[1, 2], [3, 4]]------a 纹丝不动!

这就是深拷贝的威力。它不只是复制最外层,而是递归地把里面的每一层都复制一份。不管你的数据结构有多复杂------列表套字典、字典套列表、再套个自定义对象------它都能给你复制出一份完全独立的新数据。

还是用通讯录打比方:浅拷贝是复印封面,深拷贝是把通讯录里每一页的每一个电话号码都重新抄一遍。两份通讯录从此再无瓜葛,你改你的,我改我的。

回到文章开头的例子,正确的写法应该是这样的:

ini 复制代码
import copy

original_data = [
    {"name": "张三", "balance": 1000},
    {"name": "李四", "balance": 2000}
]

# 这才是真正的救命稻草
backup_data = copy.deepcopy(original_data)

# 随便改 original_data,backup_data 纹丝不动
original_data[0]["balance"] = 900
print(backup_data[0]["balance"])  # 还是 1000

这下你可以放心大胆地改数据了,备份就是真正的备份。

深拷贝不是万能的(那些藏得更深的坑)

听到这儿,你是不是觉得以后所有复制都无脑用 deepcopy 就行了?

先别急。深拷贝也不是银弹,它有几个麻烦的地方。

第一,慢。

deepcopy 要递归遍历整个数据结构,数据越大越慢。如果你的数据结构有几千层嵌套、几百万个元素,deepcopy 可能会让你等到怀疑人生。在性能敏感的场合,浅拷贝和深拷贝之间需要权衡一下。

第二,有些东西拷不了。

比如文件对象、网络连接、线程锁这些东西,根本没法制成一个独立的副本。你强行 deepcopy,Python 会直接报错。

go 复制代码
import copy
f = open("test.txt", "r")
copy.deepcopy(f)  # 报错:无法 pickle 文件对象

第三,循环引用可能出问题。

如果你的数据结构里 A 引用了 B,B 又引用了 A(循环引用),deepcopy 虽然能处理,但如果不小心写错了自定义的 __deepcopy__ 方法,可能会无限递归。

所以深拷贝虽然强,但不是无脑用的。

那到底该怎么选?

踩完这些坑之后,我给自己理了一套决策流程,每次要"复制"东西就按这个来:

如果你要复制的数据里全是不可变类型 ------数字、字符串、布尔值、None、元组(且元组里也都是不可变类型)------用浅拷贝就够了 。这时候切片、copy() 随便用,完全没问题,效率高。

如果数据里有可变类型------列表、字典、集合,或者自定义对象------那就要想清楚:

  • 你只是要复制"容器本身",里面的内容打算只读不写 → 浅拷贝就行
  • 你要的是完全独立的副本,修改副本不能影响原数据 → 用 deepcopy

如果是自定义对象 ,可以给类实现 __copy__ 和 __deepcopy__ 方法,精确控制拷贝行为,但这属于进阶用法了。

写在最后

那次数据事故之后,我养成了一个习惯:每次写切片或者 copy() 之前,都先看一眼数据结构里面装的是什么。

如果只是 [1, 2, 3] 这样的简单列表,切片大法好。如果是 [{"a": 1}, {"b": 2}] 这样的复杂结构,我一定老老实实写上 import copy 和 copy.deepcopy。

其实回头想想,Python 把浅拷贝作为默认行为是有道理的------速度快、内存省,大多数情况下也够用。但问题在于,很多人(包括当时的我)根本不知道浅拷贝的存在,想当然地认为"复制"就是"完完整整复制一份"。

这两个词在 Python 里完全是两码事。

希望我的这次教训,能帮你少熬一个通宵。下次再用切片复制数据的时候,记得多看一眼------里面装的是不是会"牵连"你的东西。

相关推荐
行百里er7 小时前
Redis 完结篇——Redis 热点问题在 Spring Boot 项目中实战
redis·后端
高频因子挖掘机8 小时前
只取最近 60 个交易日,却每次拉全历史:量化脚本变慢的原因与改法
后端·github·api
孟健9 小时前
从5000亿Token到83%字节匹配:AI逆向工程与重构的工程闭环实践
后端·架构·ai编程
码事漫谈10 小时前
丢得静悄悄,谈一谈 Agent 的记忆是怎么没的
后端
迪飞特科技10 小时前
智慧物业平台实战:Spring Boot 整合小程序实现报修巡检费用全流程
spring boot·后端·小程序
考虑考虑10 小时前
@JsonInclude注解使用
spring boot·后端·spring
IT_陈寒10 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端
网易云信10 小时前
大模型能上岗做销售吗?我们让 12 款国产模型交了 420 份作业
前端·后端
CCblandy10 小时前
HTTP 长连接 3 类故障实测:[WinError 10053]、99 个 TIME_WAIT、69 次多余握手,4 种对策怎么选
后端·python·网络协议·http·云原生·性能优化·架构
蜗牛互联网10 小时前
Python Agent工具调用的白名单、参数校验与审批门禁
java·服务器·前端·人工智能·后端·python