首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python 的赋值把我坑惨了,明明改了副本,结果原数据也变了,原来 copy 和 deepcopy 差这么多

Python 的赋值把我坑惨了,明明改了副本,结果原数据也变了,原来 copy 和 deepcopy 差这么多

原创
作者头像
风一样的男子
发布2026-07-30 14:30:42
发布2026-07-30 14:30:42
1500
举报
文章被收录于专栏:编程教程编程教程

一个让我怀疑人生的下午

项目上线前一天,产品经理跑过来,说用户画像的数据结构要调整一下。

原来的数据长这样:

代码语言:javascript
复制
user = {
    "name": "张三",
    "age": 28,
    "tags": ["VIP", "高消费", "喜欢数码"],
    "profile": {
        "city": "北京",
        "occupation": "工程师"
    }
}

现在要把 tags 里的 "喜欢数码" 改成 "科技爱好者",还要在 profile 里加一个 "level": "gold"

我在脑子里想了一下逻辑:不能直接改原始数据,因为其他地方还在用。于是决定先复制一份,在副本上改,改完再上报。

代码写得很顺手:

代码语言:javascript
复制
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 里的 tagsprofile 都被改掉了。

这意味着:我从接口上报出去的数据是对的,但本地缓存的原始数据已经被污染了。 如果后面还有其他逻辑依赖这个原始数据,整个流程都会乱掉。

我当时的第一反应是:Python 的赋值是不是出 Bug 了?

后来我才明白——出 Bug 的不是 Python,是我。

我根本不了解 = 到底在干什么。

先弄清楚赋值到底干了什么

在 Python 里,赋值 = 做的事情很简单,就一句话:

把变量名指向对象,而不是复制对象。

画个图就明白了。

代码语言:javascript
复制
a = [1, 2, 3]
b = a

这段代码里发生的事情是这样的:

  1. Python 先在内存里创建一个列表对象 [1, 2, 3]
  2. 然后把名字 a 贴在这个对象上
  3. 执行 b = a 的时候,把名字 b 也贴在同一个对象

内存里的情况是:

代码语言:javascript
复制
a ──┐
     ├──> [1, 2, 3]
b ──┘

ab 指向的是同一个盒子。你往盒子里放东西,不管用 a 还是 b 操作,结果都一样——因为盒子里就那一份数据。

这就是为什么 b[0] = 99 之后,a[0] 也变成了 99。

这不是 Python 的问题。所有编程语言的赋值都是这个逻辑——变量名是引用,不是容器。

那怎么才能复制一份呢?

浅拷贝:只拷贝最外面一层

Python 里有个 copy 模块,提供了 copy() 方法。

代码语言:javascript
复制
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() 创建了一个新的列表对象,然后把原来列表里的元素引用复制了一份。

内存示意图:

代码语言:javascript
复制
a ──> [1, 2, 3]
b ──> [1, 2, 3]  ← 新的列表,但元素还是原来的元素

ab 是两个不同的盒子了。改 b[0] 不会影响 a[0]

但是——如果列表里的元素本身是可变对象呢?

代码语言:javascript
复制
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] 还是原来的那两个。

内存图是这样的:

代码语言:javascript
复制
a ──> [ 指向子列表A的引用, 指向子列表B的引用 ]
b ──> [ 指向子列表A的引用, 指向子列表B的引用 ]
                ↑                    ↑
                └── 两个列表共享同一组子列表 ──┘

b[0]a[0] 指向的是同一个 [1, 2] 对象。所以你改了 b[0][0],其实改的是那个共享的子列表。

这就是浅拷贝的问题——只拷贝一层。对于嵌套的数据结构,里面的东西还是共享的。

回到我那个用户画像的场景

现在你明白我为什么翻车了吧?

代码语言:javascript
复制
new_user = original  # 这根本不是拷贝,是别名,shared
new_user = copy.copy(original)  # 浅拷贝,外层字典是新的

用浅拷贝之后:

代码语言:javascript
复制
new_user["tags"][2] = "科技爱好者"  # tags 是共享的,原数据变了
new_user["profile"]["level"] = "gold"  # profile 是共享的,原数据变了

tagsprofile 都是可变对象(列表和字典),浅拷贝只复制了外层的字典,里面的列表和字典还是原来那些。

所以改 new_user 的嵌套数据,照样污染 original

深拷贝:真正的完全复制

解决问题的方法很简单——用深拷贝:

代码语言:javascript
复制
import copy

def transform_user(original):
    new_user = copy.deepcopy(original)  # 深拷贝
    new_user["tags"][2] = "科技爱好者"
    new_user["profile"]["level"] = "gold"
    return new_user

deepcopy()递归地复制所有层级的对象。不管是几层嵌套,全部给你复制一份全新的。

内存图:

代码语言:javascript
复制
original ──> { "tags": ──> ["VIP", ...], "profile": ──> {...} }
new_user  ──> { "tags": ──> ["VIP", ...], "profile": ──> {...} }
                    ↑ 全新的列表                ↑ 全新的字典
                    └── 没有任何共享的数据 ──────┘

改了 new_user 里面的任何东西,original 纹丝不动。

深拷贝的代价是什么?速度慢,内存大。

deepcopy 要遍历整个数据结构,把所有东西都复制一遍。如果数据结构很深、很大,这个操作会很耗时,内存占用也会翻倍。

但如果数据正确性比性能更重要——比如你的数据要被多个环节反复使用——那这点代价值得花。

三种方式的完整对比

操作

写法

复制了几层?

嵌套数据是否共享?

适用场景

赋值

b = a

0层

是,完全共享

只读、不需要独立数据

浅拷贝

copy.copy(a)

1层

是,内层数据共享

只有一层的数据结构

深拷贝

copy.deepcopy(a)

所有层

否,完全独立

嵌套结构、需要完全隔离

再看一张更直观的对比图:

代码语言:javascript
复制
原始数据:a = [1, [2, 3], 4]

赋值   b = a        ──> a 和 b 指向同一个列表
浅拷贝 b = copy.copy(a) ──> 新列表,但内部的 [2,3] 还是同一个
深拷贝 b = copy.deepcopy(a) ──> 全部都是新的

哪些情况需要特别注意?

情况一:字典里的值是列表

代码语言:javascript
复制
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 不变

情况二:类实例的属性嵌套

代码语言:javascript
复制
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 坑:

代码语言:javascript
复制
def add_user(user, tags=[]):  # 默认列表是共享的!
    tags.append(user)
    return tags

print(add_user("张三"))  # ["张三"]
print(add_user("李四"))  # ["张三", "李四"]  ← 不是预期的 ["李四"]

这里 tags=[] 在函数定义时只创建一次,所有调用共享同一个列表。

正确的写法:

代码语言:javascript
复制
def add_user(user, tags=None):
    if tags is None:
        tags = []
    tags.append(user)
    return tags

什么时候该用浅拷贝,什么时候用深拷贝?

浅拷贝的适用场景:

  • 数据结构只有一层(比如列表里的元素都是不可变对象)
  • 你只修改最外层的结构,不改里面的元素
  • 你要性能,而且确定不会污染原始数据
代码语言:javascript
复制
# 合适:只有一层的列表
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 变了,出问题

深拷贝的适用场景:

  • 数据结构任意嵌套
  • 你需要完全独立的数据副本
  • 数据要传给多个下游处理,互相不能干扰
代码语言:javascript
复制
# 用户画像、配置信息、状态快照——这些都用深拷贝
snapshot = copy.deepcopy(current_state)

有没有更快的深拷贝?

deepcopy() 有个问题:碰到重复引用的对象,它会记录已经复制过的对象,避免无限递归。这个记录过程本身也有开销。

如果你的数据结构很简单且确定,可以手写复制逻辑:

代码语言:javascript
复制
# 对于确定结构的字典,手动复制比 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,问题解决了。

本地缓存的原始数据完好无损,上报的数据结构正确,项目顺利上线。

那个下午让我彻底记住了三件事:

  1. 赋值不复制——它只是贴标签
  2. 浅拷贝只保一层——里面的东西还是共享的
  3. 深拷贝才彻底——但代价是时间和内存

从那以后,每次写复制相关的代码,我都会停下来想一想:

我的数据结构几层?我要改哪一层?我真的需要完全隔离吗?

想清楚了再写,省得改完代码发现数据全乱了,回头还得重来。


一句话总结:

  • b = a:只贴了个标签,东西还是同一份
  • copy.copy(a):新盒子,但盒子里的东西还是原来的
  • copy.deepcopy(a):新的盒子,新的东西,谁也不碍谁

选哪个?看你的数据有几层嵌套,看你要不要完全隔离

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

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

目录
  • 一个让我怀疑人生的下午
  • 先弄清楚赋值到底干了什么
  • 浅拷贝:只拷贝最外面一层
  • 回到我那个用户画像的场景
  • 深拷贝:真正的完全复制
  • 三种方式的完整对比
  • 哪些情况需要特别注意?
  • 什么时候该用浅拷贝,什么时候用深拷贝?
  • 有没有更快的深拷贝?
  • 那天后来怎么样了?
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档