改了副本,原件也跟着变?Python拷贝的坑我帮你踩过了

简介: 本文揭秘Python中浅拷贝与深拷贝的核心差异:`copy()`仅复制外层容器,嵌套对象仍共享引用,导致权限系统中Alice和Bob的settings意外联动;而`deepcopy()`递归复制所有层级,确保完全独立。附赋值、浅拷贝、深拷贝对比及常见踩坑指南。(239字)

一个让我的用户权限系统全线崩溃的Bug

去年我负责一个用户权限管理模块,每个用户进来时,系统会基于一个“默认权限模板”生成一份独立的权限配置。

default_permission = {
   "admin": False,
   "modules": ["dashboard", "profile"],
   "settings": {"theme": "light", "notifications": True}
}

def create_user_permission(username):
   user_perm = default_permission.copy()  # 复制一份
   user_perm["username"] = username
   return user_perm

alice = create_user_permission("alice")
bob = create_user_permission("bob")
print(alice)
print(bob)

看起来完美。直到有一天测试同事跑来告诉我:“我给Alice关了通知提醒,Bob的通知提醒也跟着关了!”

我一脸懵:“怎么可能?Alice和Bob是两份独立的权限配置啊!”

查了半天才发现问题出在这行代码上:user_perm = default_permission.copy()

这个copy(),只复制了最外面那层,里面的settings字典还是同一个

我改了Alice的settings,Bob的settings也跟着变了,因为两个用户的settings指向的是同一个字典对象。

那天下午,我把Python拷贝的三种方式彻底研究了一遍。今天把这些坑讲清楚,希望你别重蹈我的覆辙。

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


第一步:赋值不是拷贝(先把最基础的概念搞明白)

很多人搞不清楚“赋值”和“拷贝”的区别。

a = [1, 2, [3, 4]]
b = a  # 这不是拷贝,是贴标签
b.append(5)
print(a)  # [1, 2, [3, 4], 5] —— a也跟着变了

在Python里,b = a不是复制,而是把标签b也贴到a指向的那个对象上。a和b指向同一个对象,改b就是改a。

要得到一份“独立”的副本,需要用到拷贝操作——copy()copy.deepcopy()

但要注意:不是所有拷贝都能得到完全独立的对象


第二步:浅拷贝(只复制第一层)

Python里的“拷贝”分两种:浅拷贝和深拷贝。

import copy

original = [[1, 2], [3, 4]]
shallow = original.copy()  # 浅拷贝
# 或者 shallow = copy.copy(original)

copy()只复制了最外面那层容器,里面的元素还是原来那些对象的引用。

画个图理解一下:

  • original指向一个列表对象A
  • shallow指向一个新列表对象B
  • B里的两个元素,指向的依然是A里的那两个子列表

所以当你修改B的外层结构(比如shallow.append(5)),A不受影响。但当你修改B里的内层列表(比如shallow[0].append(99)),A里对应的内层列表也会变,因为它们是同一个对象。

这就是我那个权限系统的翻车原因——default_permission.copy()只复制了最外层字典,里面的settings字典还是同一个。

default = {"settings": {"theme": "light"}}
copy = default.copy()
copy["settings"]["theme"] = "dark"
print(default["settings"]["theme"])  # "dark" —— 原件被改了!


第三步:深拷贝(彻底独立)

深拷贝会递归地复制对象里的所有层级,每一层都是全新的独立对象。

import copy

original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0].append(99)
print(original[0])  # [1, 2] —— 不受影响
print(deep[0])     # [1, 2, 99]

深拷贝的规则是:遇到一个对象就复制一个,遇到嵌套的对象就继续递归复制,直到所有东西都是全新的

copy.deepcopy()会正确处理各种复杂情况,包括嵌套的列表、字典、自定义类实例等。

所以我的权限系统,应该用deepcopy而不是copy

default_permission = {...}
user_perm = copy.deepcopy(default_permission)  # 完全独立


第四步:自定义对象的拷贝

当你的对象是自定义类的实例时,copydeepcopy的行为可以通过特殊方法控制。

import copy

class Config:
   def __init__(self, theme, modules):
       self.theme = theme
       self.modules = modules

   # 控制浅拷贝的行为
   def __copy__(self):
       return Config(self.theme, self.modules.copy())

   # 控制深拷贝的行为
   def __deepcopy__(self, memo):
       return Config(
           copy.deepcopy(self.theme, memo),
           copy.deepcopy(self.modules, memo)
       )

__copy__控制浅拷贝时怎么复制,__deepcopy__控制深拷贝时怎么处理。

如果你不实现这两个方法,copydeepcopy会默认复制对象的所有属性。但可能并不符合你的预期——比如,你可能希望modules列表在浅拷贝时共享,在深拷贝时独立。

最佳实践:如果你不确定,那就用deepcopy。虽然慢一点,但安全。当你发现性能成了瓶颈,再考虑优化成浅拷贝或自定义拷贝逻辑。


第五步:其他数据结构的拷贝

数据结构 浅拷贝方式 是否独立
列表 list.copy(), list[:], list() 外层独立,内层共享
字典 dict.copy(), dict() 外层独立,内层共享
集合 set.copy(), set() 外层独立,内层共享
元组(不可变) tuple() 本身不可变,但内部元素如果是可变对象,依然共享
自定义类 copy.copy() 取决于__copy__实现

# 列表浅拷贝的三种写法
lst = [1, 2, 3]
copy1 = lst.copy()
copy2 = lst[:]
copy3 = list(lst)

# 字典浅拷贝的两种写法
d = {"a": 1}
copy1 = d.copy()
copy2 = dict(d)


第六步:几个容易踩的坑

坑1:用*复制列表

lst = [[1, 2]] * 3
print(lst)  # [[1, 2], [1, 2], [1, 2]]
lst[0][0] = 99
print(lst)  # [[99, 2], [99, 2], [99, 2]] —— 三个子列表是同一个!

[[1, 2]] * 3创建了三个引用,指向同一个内层列表。改一个全变。

坑2:从JSON加载的数据默认是浅拷贝结构

import json
data = json.loads('{"users": [{"name": "alice"}]}')
copy = data.copy()
copy["users"][0]["name"] = "bob"
print(data["users"][0]["name"])  # "bob" —— 被改了!

JSON数据天然是嵌套结构,对它们做浅拷贝要特别小心。

坑3:copy.deepcopy可能会无限递归

如果对象里有自引用(自己指向自己),deepcopy会正确处理(通过memo字典记录已经复制过的对象),但如果你自己实现__deepcopy__方法时没处理好,就会无限递归。


一张表总结

操作 外层容器 内层元素 适用场景
= 赋值 共享 共享 需要引用同一对象时
copy() 浅拷贝 独立 共享 内层数据不可变或不需要独立
deepcopy() 深拷贝 独立 独立 内层数据需要完全独立

回到开头的Bug

那个权限系统最后改成了这样:

import copy

default_permission = {
   "admin": False,
   "modules": ["dashboard", "profile"],
   "settings": {"theme": "light", "notifications": True}
}

def create_user_permission(username):
   user_perm = copy.deepcopy(default_permission)  # 深拷贝,完全独立
   user_perm["username"] = username
   return user_perm

改完之后,Alice关通知再也不会影响到Bob了。那个让我加了一天班的Bug,就这样被deepcopy一行代码解决了。

记住:Python的copy()只复制外层,内层还是共享的。如果你想得到一份完全独立的副本,用copy.deepcopy()

这个坑我帮你踩过了,希望你能少踩一次。

目录
相关文章
|
6天前
|
缓存 人工智能 安全
GPT-5.6 Terra与GPT-5.5性能实测:成本减半后的跑分对比与快速迁移指南
GPT-5.6 Terra 的定价为每百万 token 输入 2.50/输出 15。GPT-5.5 则是 5/ 30。Terra 的每一项费率,包括 $0.25/M 的缓存读取,都恰好是 GPT-5.5 的一半,因此在任何工作负载组合下,Terra 都固定 便宜 2.0x。以每天 10 万次请求、3K token 提示词计算,大约是 Terra 每天 2,000,GPT−5.5每天 4,000,即每月约 60,000对 120,000。问题在于:OpenAI 没有发布任何针对 Terra 的编码基准。那个著名的 91.9% Terminal-Bench 数字是 Sol 在 Ul
|
1天前
|
数据采集 机器学习/深度学习 人工智能
田间杂草定位与检测4200张YOLO智慧农业数据集分享
本数据集含4200张真实农田图像,YOLO格式,单类别(杂草)高质量标注,覆盖多作物、多光照、多生长阶段等复杂场景,专为智慧农业杂草检测与智能除草设备研发设计,支持YOLOv5/v8/v10等主流模型训练。
326 93
|
2天前
|
缓存 UED 开发者
Codex109天重置23次,明天还要再送一次
Codex近109天完成23次额度重置,7月14日将迎来第24次。Tibo高频响应用户反馈:优化GPT-5.6高消耗问题、补发失效福利、调整重置时间——形成“反馈→回应→修复→补偿”正向闭环,彰显以用户为中心的产品哲学。(239字)
487 11
|
6天前
|
人工智能 缓存 JSON
刚刚 GPT-5.6 发布,吊打 Claude 5 和 Grok 4.5?一手实测来啦!
GPT-5.6 刚发布,跑分号称超越 Fable 5,真的假的?附一手实测,让 3 个最强 AI 编程模型 GPT-5.6 Sol、Claude Fable 5、Grok 4.5 在 Cursor 里同时一把梭开发网页游戏,看看最新模型到底谁更能打。
331 0
|
6天前
|
存储 人工智能 JSON
Qwen 本地部署搭配 ComfyUI 生成 AI 漫剧完整实操指南(小白零基础可落地,零成本无限生成+角色一致性天花板)
2026全网最优本地漫剧流水线:零成本、离线运行、角色统一、低配(8G显卡)可跑。融合Qwen本地大模型+ComfyUI双引擎,实现剧本生成→分镜绘图→动态成片全自动,隐私安全、无审核限流,新手30分钟上手,日更无忧。(239字)