首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的浅拷贝把我坑惨了,原来copy和deepcopy区别这么大

Python的浅拷贝把我坑惨了,原来copy和deepcopy区别这么大

原创
作者头像
风一样的男子
发布2026-08-20 14:17:31
发布2026-08-20 14:17:31
1530
举报
文章被收录于专栏:编程教程编程教程

先跟你讲个我亲身经历的事,那会儿我刚工作第二年。

公司有个客户管理系统,每个客户有个标签列表,存着“VIP”“高潜”“已转化”这类信息。运营同事要定期给部分客户批量更新标签,但更新前需要先复制一份原始数据做备份。

我的代码大概长这样:

代码语言:javascript
复制
original_customers = [
    {"name": "张三", "tags": ["VIP", "高潜"]},
    {"name": "李四", "tags": ["新客"]},
    {"name": "王五", "tags": ["已转化", "复购"]},
]

# 备份一份
backup = original_customers.copy()

# 运营修改第一个客户的标签
backup[0]["tags"].append("待回访")

print(original_customers[0]["tags"])

你猜怎么着?原始数据里的标签也被改了

我看着屏幕上打印出的['VIP', '高潜', '待回访'],整个人都懵了。明明备份的是backup,改的也是backup,为什么original_customers遭了殃?

那天下午,我把copydeepcopy的文档翻了十几遍,才终于明白:原来copy()只复制了表面一层,里面藏着的引用根本没动

今天我就把这个坑彻底给你讲清楚。

从“赋值”说起——你以为的拷贝,其实就是个外号

先得从最基础的赋值讲起。

在Python里,b = a并不是复制了一个新东西,而是给a取了个小名。a和b指向的是内存里同一个对象。

代码语言:javascript
复制
a = [1, 2, [3, 4]]
b = a
b.append(5)
print(a)  # [1, 2, [3, 4], 5] —— a也跟着变了

这就好比你叫“张三”,你的好朋友叫你“三儿”。不管是叫“张三”还是“三儿”,喊的都是你这个人。你脸上长了个痘,两个名字描述的都是同一张脸。

所以赋值不叫拷贝,叫引用绑定。你操作b,其实就是操作a。

copy模块登场——浅拷贝长什么样

为了能复制出独立的对象,我们用copy模块。

代码语言:javascript
复制
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)  # 浅拷贝
b.append(5)
print(a)  # [1, 2, [3, 4]] —— 没变,好!
print(b)  # [1, 2, [3, 4], 5]

看起来不错,外层列表确实分开了。但问题出在嵌套对象上。

代码语言:javascript
复制
b[2].append(5)
print(a)  # [1, 2, [3, 4, 5]] —— 啊哦,里面的列表被改了!

看到了吧?外层列表是新的,但内部的子列表还是同一个。浅拷贝只复制了最外层的容器,里面的元素如果是个可变对象,它只复制了引用,没有复制对象本身。

用我们刚才的比喻:浅拷贝就像你用复印机复印了一本书的封面,但里面的每一页还是原来那本书的页——你撕掉复印封面里的某一页,原来那本书的那页也跟着没了。

deepcopy——真正的“克隆”

深拷贝就不同了。它像克隆人技术——不光复制了外形,里面所有的器官、细胞都是全新的。

代码语言:javascript
复制
import copy
a = [1, 2, [3, 4]]
b = copy.deepcopy(a)  # 深拷贝
b[2].append(5)
print(a)  # [1, 2, [3, 4]] —— 纹丝不动
print(b)  # [1, 2, [3, 4, 5]] —— 独立修改

深拷贝会递归地复制对象内部的所有层次,直到遇到不可变对象(数字、字符串、元组等)才停止。因为不可变对象本身就不能改变,共享也无所谓。

实战排雷:这些“拷贝”其实都是浅拷贝

很多人不知道,Python里很多常见操作其实都是浅拷贝。

1. 列表的copy()方法和切片

代码语言:javascript
复制
a = [[1, 2], [3, 4]]
b = a.copy()        # 浅拷贝
c = a[:]            # 切片也是浅拷贝
d = list(a)         # 构造器也是浅拷贝

这三者等价于copy.copy(a)

2. 字典的copy()方法

代码语言:javascript
复制
d = {"a": [1, 2], "b": [3, 4]}
e = d.copy()
e["a"].append(5)
print(d["a"])  # [1, 2, 5] —— 原字典也被改了

3. 集合的拷贝

代码语言:javascript
复制
s = {1, 2, [3, 4]}  # 注意,集合不能包含列表,用frozenset举例
# 但集合的copy()也是浅拷贝

4. 嵌套元组(元组本身不可变,但内部元素可变)

代码语言:javascript
复制
t = (1, 2, [3, 4])
u = copy.copy(t)
u[2].append(5)
print(t)  # (1, 2, [3, 4, 5]) —— 还是被改了

所以,只要你的数据结构是嵌套的,浅拷贝就有隐患

到底什么时候用浅拷贝,什么时候用深拷贝?

知道了区别,关键是怎么选。我给你的原则很简单:

用浅拷贝的情况:

  • 你的数据结构是一维的,只包含不可变对象(数字、字符串、布尔值等)
  • 或者你明确知道嵌套的内部对象不会被修改
  • 性能敏感的场景(深拷贝递归开销大)
  • 你只是想复制一个容器,但里面的元素本来就不打算动

必须用深拷贝的情况:

  • 数据结构是嵌套的可变对象(列表套列表、字典套列表、类对象等)
  • 你要对副本做修改,但绝对不想影响原数据
  • 数据是配置、快照、备份等需要完全独立的场景
  • 你不确定内部结构有多深,保险起见用深拷贝

话说回来,深拷贝也不是万能的。它有两个小毛病:

  1. 性能开销大:递归复制所有层级,大数据量时会慢。
  2. 循环引用问题:如果对象自己引用自己(比如列表a包含a),深拷贝会无限递归。不过Python的deepcopy已经处理了这种情况,它会维护一个“已复制”字典,遇到循环引用会复用已复制的对象,不会死循环。

deepcopy默认不复制一些特殊对象(比如文件对象、socket、线程锁等),复制会抛出异常。这些对象不能被拷贝,你需要手动处理。

实战案例:配置文件的灾难

再讲一个真实案例,我那会儿维护一个游戏服务器配置。

配置是一个大字典,里面有各种嵌套结构,比如:

代码语言:javascript
复制
config = {
    "level": 10,
    "players": [
        {"name": "alice", "items": ["sword", "shield"]},
        {"name": "bob", "items": ["bow"]}
    ],
    "settings": {"difficulty": "hard", "cheats": False}
}

有一天,我们要给某个玩家临时调整道具,先复制一份配置做试验:

代码语言:javascript
复制
test_config = config.copy()
test_config["players"][0]["items"].append("potion")

结果你懂的,原配置里的alice也多了个药水,导致所有正式服玩家都错了。那天下线修复花了两小时,还被领导质问。

后来我们把所有配置拷贝都改成了deepcopy,这类问题再也没出现过。

一个有趣的细节:不可变对象不需要深拷贝

你可能会问:深拷贝会复制不可变对象吗(比如整数、字符串)?

答案是不会。因为不可变对象没法修改,共享它们没有任何风险。deepcopy在遇到不可变对象时,不会复制一份新的,而是直接共享引用,节省内存。

代码语言:javascript
复制
a = [1, 2, "hello"]
b = copy.deepcopy(a)
print(id(a[0]) == id(b[0]))  # True,是同一个整数对象

但如果元组里包含可变对象,深拷贝会复制那个可变对象,因为可变对象可以改。deepcopy会逐层检查。

怎么判断一个对象是浅拷贝还是深拷贝?

你可以用id()is来判断最外层对象是否不同,但要判断内部嵌套对象,得递归检查。最省事的方法是:改一下里面的内容,看看原数据变没变——这是最直接的测试方法。

写代码的时候,如果拿不准,就先用deepcopy,然后观察性能,如果没问题就保留。性能优化是后期的事,先保证正确性。

总结一张速查表

操作

类型

风险

b = a

赋值(引用)

修改b就是修改a

b = a.copy()

浅拷贝

外层独立,内层共享

b = a[:]

浅拷贝

同上

b = list(a)

浅拷贝

同上

b = dict(a)

浅拷贝

同上

b = copy.copy(a)

浅拷贝

同上

b = copy.deepcopy(a)

深拷贝

完全独立,但可能慢

记住这几句话,够你应付大部分场景:

  • 赋值不是拷贝,是贴标签。
  • 浅拷贝只救急不救穷——管得了第一层,管不了里面。
  • 深拷贝是救火队——无论多深,全都给你复制一份。
  • 没有银弹——深拷贝虽好,但别滥用,注意性能和特殊对象。

从那以后,我养成了一个习惯:只要数据结构里有listdictset等可变容器嵌套,一律用deepcopy做备份。宁可慢一点,也绝不深夜加班改bug。

希望你别重蹈我的覆辙。下次写代码前,先问问自己:我要的是“复印封面”还是“克隆整个人”?选对了,能省下好几个小时的调试时间。

好了,不说了,我现在得去把以前项目里所有的copy()都检查一遍。你也赶紧去吧。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 从“赋值”说起——你以为的拷贝,其实就是个外号
  • copy模块登场——浅拷贝长什么样
  • deepcopy——真正的“克隆”
  • 实战排雷:这些“拷贝”其实都是浅拷贝
    • 1. 列表的copy()方法和切片
    • 2. 字典的copy()方法
    • 3. 集合的拷贝
    • 4. 嵌套元组(元组本身不可变,但内部元素可变)
  • 到底什么时候用浅拷贝,什么时候用深拷贝?
    • 用浅拷贝的情况:
    • 必须用深拷贝的情况:
  • 实战案例:配置文件的灾难
  • 一个有趣的细节:不可变对象不需要深拷贝
  • 怎么判断一个对象是浅拷贝还是深拷贝?
  • 总结一张速查表
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档