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 不是复制,是贴标签。ab 指向同一个盒子,你改盒子里的东西,另一个当然看得见。

那切片呢?

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

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

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

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 copycopy.deepcopy

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

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

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

相关推荐
神奇小汤圆2 小时前
Java 万字长文:从零基础到高级应用的完整教程——把面向对象讲透
后端
孓最求完美2 小时前
实战经验:JT808/JT809 车联网高并发服务端性能优化指南
后端
shengjk12 小时前
深度拆解:从 LC-3 汇编到 Java/Rust,数组越界与硬件中断的底层真相
后端
Gorway2 小时前
理解 Spring 依赖注入:从构造器注入到集合与条件 Bean
java·后端
颜进强2 小时前
Claude Code - 26 效率三件套:cc-switch 切模型 · codeburn 算成本 · claude-hud 看状态
前端·后端·ai编程
掘金者阿豪2 小时前
时序数据库选型被写入峰值带偏了?金仓超表让我少加了不少班
后端
大黄评测3 小时前
jQuery 遍历方法 each 实战:循环处理列表数据
后端
大勇前进3 小时前
jQuery 事件委托原理:彻底解决动态生成元素绑定失效问题
后端