文章目录
- [Python 可变对象与不可变对象(下):深浅拷贝的底层真相------为什么你的 copy 不好使](#Python 可变对象与不可变对象(下):深浅拷贝的底层真相——为什么你的 copy 不好使)
-
- 导入语
- [1 ~> 先回忆:赋值 `=` 不是拷贝](#1 ~> 先回忆:赋值
=不是拷贝) - [2 ~> 浅拷贝------只拷贝第一层](#2 ~> 浅拷贝——只拷贝第一层)
-
- [2.1 浅拷贝长什么样](#2.1 浅拷贝长什么样)
- [2.2 那为什么说它"浅"](#2.2 那为什么说它"浅")
- [2.3 内存图解释](#2.3 内存图解释)
- [2.4 什么时候浅拷贝就够了](#2.4 什么时候浅拷贝就够了)
- [3 ~> 深拷贝------递归拷贝每一层](#3 ~> 深拷贝——递归拷贝每一层)
-
- [3.1 深拷贝长什么样](#3.1 深拷贝长什么样)
- [3.2 内存图解释](#3.2 内存图解释)
- [3.3 深拷贝如何应对循环引用](#3.3 深拷贝如何应对循环引用)
- [4 ~> 一个真实的生产事故](#4 ~> 一个真实的生产事故)
- [5 ~> copy vs deepcopy 速查表](#5 ~> copy vs deepcopy 速查表)
- [6 ~> 自定义类的深拷贝](#6 ~> 自定义类的深拷贝)
- [思考 && 总结](#思考 && 总结)
- 结尾
Python 可变对象与不可变对象(下):深浅拷贝的底层真相------为什么你的 copy 不好使
最新推荐文章于 2026-06-16 12:00:00 发布 | 阅读 1.0k 阅读 | 分类:Python基础入门
📖 文章简介: 上篇讲了赋值陷阱和函数传参,本篇深入到深浅拷贝。copy() 和 deepcopy() 到底差在哪?为什么嵌套列表用 copy() 改内层元素还是会影响原对象?本文用内存模型图逐层拆解浅拷贝的"只拷贝第一层"机制、深拷贝的递归复制原理、以及循环引用时 deepcopy 如何通过 memo 字典避免死循环。穿插真实踩坑经历:一个配置表因为浅拷贝导致原始模板被污染的生产事故。读完你不仅知道用哪个,还能说出内存层原因。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
上篇讲到了可变对象和不可变对象的赋值陷阱------你知道了 b = a 只是多贴一张标签。所以当你想"真正复制一份独立的数据"时,自然会想到 copy()。
但现实远没这么简单。我 2021 年做配置中心项目时,有一个深层嵌套的 YAML 配置模板,开发同事用 .copy() 复制了一份去修改,结果原始模板被污染了,线上三个服务同时挂掉。回溯了三个小时才发现------嵌套结构用 copy() 等于没拷。
问题的根源在于:浅拷贝只拷贝第一层。深拷贝才递归拷贝每一层。 这句话说着简单,但画成内存图你会发现完全不一样。这篇文章把这两个的区别用图讲清楚。
1 ~> 先回忆:赋值 = 不是拷贝
python
a = [1, 2, 3]
b = a # 赋值------b 和 a 指向同一个对象
b.append(4)
print(a) # [1, 2, 3, 4] ← 不是拷贝,是共享
赋值只是贴标签,没有创建新对象。要真正创建一份独立的数据,你需要拷贝。
2 ~> 浅拷贝------只拷贝第一层
2.1 浅拷贝长什么样
python
import copy
a = [1, 2, 3]
b = a.copy() # 方式一:list.copy()
b = a[:] # 方式二:切片------也等于浅拷贝
b = list(a) # 方式三:构造器
b = copy.copy(a) # 方式四:copy 模块
# 验证:b 是新创建的独立对象
print(a is b) # False ← 确实不是同一个对象了
2.2 那为什么说它"浅"
python
a = [[1, 2], [3, 4]] # 外层列表装了两个内层列表
b = a.copy()
# 修改外层------b 不受影响
a.append([5, 6])
print(b) # [[1, 2], [3, 4]] ← b 外层没变,符合预期
# 但修改内层呢?
a[0].append(999)
print(b[0]) # [1, 2, 999] ← b 的内层变了!
2.3 内存图解释
bash
浅拷贝前:
a → [ 指针1 , 指针2 ]
↓ ↓
[1, 2] [3, 4]
浅拷贝后:
a → [ 指针1 , 指针2 ]
↓ ↓
[1, 2] [3, 4]
↑ ↑
b → [ 指针1 , 指针2 ] ← 外层容器是新对象
← 但内部指针指向的还是原来的内层对象!
浅拷贝创建了一个新的外层容器,但外层容器里的引用(指针)指向的还是原来的内层对象。 这就是"只拷贝第一层"的含义。
2.4 什么时候浅拷贝就够了
如果你的列表是一维的(没有嵌套结构),浅拷贝完全够用:
python
a = [1, 2, 3]
b = a.copy()
b[0] = 100
print(a) # [1, 2, 3] ← a 没变,浅拷贝成功
一维列表中所有元素都是不可变的(int/str 等),不可能被原地修改,所以改不了 b 同时影响 a。
3 ~> 深拷贝------递归拷贝每一层
3.1 深拷贝长什么样
python
import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
a[0].append(999)
print(a[0]) # [1, 2, 999]
print(b[0]) # [1, 2] ← b 不受影响!内层也独立了
3.2 内存图解释
bash
深拷贝后:
a → [ 指针1 , 指针2 ]
↓ ↓
[1, 2] [3, 4] ← a 的内层对象
b → [ 指针3 , 指针4 ]
↓ ↓
[1, 2] [3, 4] ← b 的内层对象------全新的!
深拷贝递归地把每一层都复制了,外层和内层全是独立的新对象。a 和 b 在内存里没有任何重叠。
3.3 深拷贝如何应对循环引用
python
a = [1, 2]
a.append(a) # a 引用自己 → 循环引用
b = copy.deepcopy(a) # 能跑!不会死循环
print(b[2] is b) # True(b 也引用自己,保持了一致的拓扑结构)
deepcopy 内部维护了一个 memo 字典,记录"已拷贝过的对象 → 它的拷贝"。 遇到循环引用时,memo 里已经有了,直接复用------避免死递归。这是 deepcopy 设计的精妙之处,也是为什么你要用 copy.deepcopy() 而不是自己写递归。
4 ~> 一个真实的生产事故
2021 年的配置中心项目。简化后代码大概长这样:
python
# 配置模板------深层嵌套
DEFAULT_CONFIG = {
"server": {
"host": "0.0.0.0",
"port": 8000,
"features": ["auth", "logging"] # 注意这个列表
}
}
# 每个客户有这个函数,复制模板后改配置
def get_customer_config(customer_name):
config = DEFAULT_CONFIG.copy() # 浅拷贝!!!
config["server"]["features"].append(f"custom_{customer_name}")
return config
# 第1个客户
config_a = get_customer_config("A")
# 第2个客户
config_b = get_customer_config("B")
# 第1个客户的配置再次读取
config_a2 = get_customer_config("A")
print(config_a2["server"]["features"])
# ["auth", "logging", "custom_A", "custom_B", "custom_A"] ← 彻底废了
根因分析: .copy() 是浅拷贝------DEFAULT_CONFIG 的顶层 dict 复制了一份,但嵌套的 dict 和 list 没有复制。config["server"]["features"] 修改的仍然是 DEFAULT_CONFIG 内部的同一个 list。每次调用都在污染全局模板,各客户之间的配置串了。
修复: 把 .copy() 换成 copy.deepcopy()。
教训: 这是个代价不算小的教训。从那以后团队 Code Review 看到 .copy() 操作嵌套结构,一律要求加注释说明"为什么这里浅拷贝够用",否则改成 deepcopy。
5 ~> copy vs deepcopy 速查表
| 操作 | 外层容器 | 内层对象 | 适合什么场景 |
|---|---|---|---|
b = a(赋值) |
共享 | 共享 | 不需要拷贝,贴标签就行 |
a.copy()(浅复制) |
✅ 新对象 | ❌ 共享 | 一维列表、只读内层 |
copy.deepcopy(a)(深复制) |
✅ 新对象 | ✅ 全新对象 | 嵌套结构、不想有任何共享 |
切片 a[:] |
✅ 新对象 | ❌ 共享 | 同浅拷贝 |
6 ~> 自定义类的深拷贝
python
import copy
class Person:
def __init__(self, name, hobbies):
self.name = name
self.hobbies = hobbies # hobbies 是 list
p1 = Person("张三", ["篮球", "编程"])
p2 = copy.浅拷贝(p1) # Person 新对象,但 .hobbies 共享
p3 = copy.deepcopy(p1) # Person 新对象,.hobbies 也是新列表
p1.hobbies.append("吃饭")
print(p2.hobbies) # ['篮球', '编程', '吃饭'] ← 浅拷贝的 hobbies 被污染
print(p3.hobbies) # ['篮球', '编程'] ← 深拷贝独立
类中如果包含可变属性的嵌套,同样需要用 deepcopy。有些情况可以覆写 __copy__ 和 __deepcopy__ 来定制拷贝行为,但初学阶段知道有这个方法就行。
思考 && 总结
深浅拷贝的核心逻辑就三句话:
- 浅拷贝创建新的外层容器,但内层元素仍然指向原对象。一维数据够用,嵌套数据有共享风险。
- 深拷贝 递归复制每一层,产生完全独立的对象图。
memo字典防止循环引用导致死递归。 - 判断是否需要深拷贝 的方法很简单:看你的数据结构有没有嵌套可变对象(列表套列表、字典套列表、类属性是列表等)。有嵌套 →
deepcopy。只有一层 →copy()或a[:]就行。
结尾
各位小伙伴,本文的内容到这里就全部结束了,源码骑士在这里再次感谢您的阅读!
源码骑士 --- Python 全栈 & 系统架构
👀 关注:跟博主一起从源码视角深耕底层原理,见证每一次成长
❤️ 点赞:让优质内容被更多人看见,让知识传递更有力量
⭐ 收藏:把核心知识点存好,在需要时随时查、随时用
💬 评论:分享你的经验或疑问,评论区一起交流避坑
🔄 一键四连:不要忘记给博主"一键四连"哦!今日源码拆解达成!
🗡️ 寄语:技术之路难免有困惑,但同行的人会让前进更有方向
结语:深浅拷贝的问题不在于语法难,在于你对内存里对象是怎么连起来的有没有一张图。下次 Code Review 看到 .copy() 出现在嵌套结构上,多看一眼------可能是个未来的线上事故。不要忘记给博主"一键四连"哦!