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

简介: 本文以一次上线前的“翻车”经历切入,生动剖析Python中赋值、浅拷贝与深拷贝的本质区别:`=`仅创建引用;`copy.copy()`仅复制外层;`copy.deepcopy()`才真正递归隔离所有嵌套数据。适合开发者快速理解并规避数据污染风险。(239字)

一个让我怀疑人生的下午

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

原来的数据长这样:

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

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

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

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (54).png

代码写得很顺手:

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 里,赋值 = 做的事情很简单,就一句话:

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

画个图就明白了。

a = [1, 2, 3]
b = a

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

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

内存里的情况是:

a ──┐
    ├──> [1, 2, 3]
b ──┘

ab 指向的是同一个盒子。你往盒子里放东西,不管用 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]  ← 新的列表,但元素还是原来的元素

ab 是两个不同的盒子了。改 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 是共享的,原数据变了

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

所以改 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,问题解决了。

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

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

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

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

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

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


一句话总结:

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

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

目录
相关文章
|
8天前
|
人工智能 JSON 安全
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
阿里云AI安全产品联动防御Fastjson攻击
2281 12
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
|
8天前
|
云安全 人工智能 安全
|
9天前
|
人工智能 自然语言处理 数据挖掘
Qwen3.8-Max-Preview深度全解析:2.4万亿参数旗舰MoE模型+Token Plan限时优惠完整落地指南
2026年7月,全新旗舰级混合专家大模型Qwen3.8-Max-Preview正式开放抢先体验,作为通义千问Qwen3系列规格最高、综合推理能力顶尖的新一代模型,该模型总参数量达到2.4万亿(2.4T),是当前线上可调用的原生多模态旗舰模型,综合推理水准对标海外顶级Fable 5模型,在复杂工程开发、长文档深度分析、多步骤智能体自治、跨境多语言创作、海量数据挖掘五大高难度业务场景实现跨越式性能提升。
1025 2
|
10天前
|
人工智能
Qwen3.8抢先体验!正式版即将发布并开源!
千问Qwen3.8即将开源,参数达2.4T,进化速度以“天”计,实力媲美Fable 5。预览版Qwen3.8-Max已上线阿里Token Plan等平台,限时优惠:日间Credits低至1折,夜间更优,个人/团队版月付仅35元起!
1027 44
|
8天前
|
人工智能 自然语言处理 数据挖掘
最新版通义千问(Qwen3.8-Max-Preview)功能介绍
2026年,通义千问正式推出全新旗舰级大模型 **Qwen3.8-Max-Preview 预览版**,作为首款突破万亿参数规格的新一代基座模型,该模型总参数量达到**2.4万亿**,采用全新迭代的MoE混合专家架构,综合推理性能、长文本处理、多模态理解、复杂任务规划能力全面超越前代Qwen3.7-Max版本,整体实力跻身全球第一梯队,可对标海外顶级旗舰模型,是当前面向复杂工程开发、多智能体协同、超长文档解析、专业办公自动化场景的最优国产基座模型。
1062 0
|
7天前
|
自然语言处理 测试技术 API
通义千问Qwen3.8-Max-Preview全功能解析:2.4万亿参数旗舰模型深度使用指南
在大模型技术持续迭代的当下,通义千问推出的Qwen3.8-Max-Preview作为新一代旗舰预览版模型,凭借2.4万亿参数的超大规模、多模态融合能力与全场景适配特性,成为开发者与企业用户探索AI应用的核心工具。该模型采用稀疏混合专家(MoE)架构,是通义千问首个突破万亿参数的多模态模型,可同时处理文本、图像、视频与文档等多种数据形态,在全栈代码开发、复杂逻辑推理、长文档分析与多智能体协作等场景实现跨越式升级。本文将全面拆解Qwen3.8-Max-Preview的核心功能,详解API调用流程与配置方法,覆盖多场景实战技巧,帮助用户快速掌握这款旗舰模型的使用方法,充分释放其性能潜力。
510 1
|
10天前
|
人工智能 自然语言处理 数据挖掘
Qwen3.8-Max 预览版全解析:2.4 万亿参数旗舰模型,Token Plan 限时优惠指南
Qwen3.8-Max-Preview是通义千问Qwen3系列旗舰MoE大模型,参数达2.4万亿,综合推理能力居行业第一梯队。支持思考/快速双模式,擅长大模型五大高难场景。现于阿里云百炼Token Plan、Qoder及QoderWork上线体验,个人版低至39元/月。在阿里云百炼官网:https://t.aliyun.com/U/fPVHqY 免费领取千万Tokens
705 1
Qwen3.8-Max 预览版全解析:2.4 万亿参数旗舰模型,Token Plan 限时优惠指南