03-Python可变对象与不可变对象(下)-深浅拷贝的底层真相

文章目录

  • [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__ 来定制拷贝行为,但初学阶段知道有这个方法就行。


思考 && 总结

深浅拷贝的核心逻辑就三句话:

  1. 浅拷贝创建新的外层容器,但内层元素仍然指向原对象。一维数据够用,嵌套数据有共享风险。
  2. 深拷贝 递归复制每一层,产生完全独立的对象图。memo 字典防止循环引用导致死递归。
  3. 判断是否需要深拷贝 的方法很简单:看你的数据结构有没有嵌套可变对象(列表套列表、字典套列表、类属性是列表等)。有嵌套 → deepcopy。只有一层 → copy()a[:] 就行。

结尾

各位小伙伴,本文的内容到这里就全部结束了,源码骑士在这里再次感谢您的阅读!

源码骑士 --- Python 全栈 & 系统架构

👀 关注:跟博主一起从源码视角深耕底层原理,见证每一次成长

❤️ 点赞:让优质内容被更多人看见,让知识传递更有力量

收藏:把核心知识点存好,在需要时随时查、随时用

💬 评论:分享你的经验或疑问,评论区一起交流避坑

🔄 一键四连:不要忘记给博主"一键四连"哦!今日源码拆解达成!

🗡️ 寄语:技术之路难免有困惑,但同行的人会让前进更有方向

结语:深浅拷贝的问题不在于语法难,在于你对内存里对象是怎么连起来的有没有一张图。下次 Code Review 看到 .copy() 出现在嵌套结构上,多看一眼------可能是个未来的线上事故。不要忘记给博主"一键四连"哦!

相关推荐
与代码不die不休1 小时前
RTX5060显卡torch和torch_radon库安装避坑指南(仅linux系统)
linux·图像处理·python·深度学习
砍材农夫1 小时前
python环境|pip|uv|venv|Conda区别
后端·python·conda·pip·uv
向量引擎1 小时前
AI API 正在进入“请求生命周期治理”阶段:从模型迁移、Agent 接入到成本与安全排错的工程化方法
java·人工智能·python·aigc·ai编程·ai写作·gpu算力
sycmancia1 小时前
Qt——自定义模型类
开发语言·qt
梦想不只是梦与想1 小时前
Python 中的线程(Thread)
python·线程·thread
热心不起来的市民小周1 小时前
100种动物语义分割数据集(A100-Seg)
python·深度学习·计算机视觉
DrMaker1 小时前
【无标题】
软件测试·python·测试工具·pyqt
MATLAB代码顾问1 小时前
Python数据分析项目实战:销售数据仪表盘
开发语言·python·数据分析
码云骑士1 小时前
07-Python装饰器从入门到源码(下)-带参数装饰器与wraps
开发语言·python