和deepcopy()的区别这么大)
一个让我加了两天班的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)逻辑看起来没问题对吧default_permission.copy()复制了一份Alice和Bob各用各的。直到测试同事跑来告诉我“我给Alice关了通知提醒Bob的通知提醒也跟着关了”我一脸懵“怎么可能Alice和Bob是两份独立的权限配置啊”查了半天才发现问题出在这行代码上default_permission.copy()。这个copy()只复制了最外面那层字典。里面的settings字典还是同一个——Alice和Bob的settings指向的是同一块内存地址。我改了Alice的settingsBob的settings也跟着变了。那天下午我把Python拷贝的三种方式彻底研究了一遍。今天把这些坑讲清楚希望你别重蹈我的覆辙。第一步赋值不是拷贝很多人搞不清楚“赋值”和“拷贝”的区别。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。你可以用id()验证一下print(id(a)) # 比如 140234567890 print(id(b)) # 同样是 140234567890两个变量指向同一个内存地址本质上就是一个东西。要得到一份“独立”的副本需要用到拷贝操作——copy()或copy.deepcopy()。但要注意不是所有拷贝都能得到完全独立的对象。第二步浅拷贝——只复制第一层Python里的“拷贝”分两种浅拷贝和深拷贝。import copy original [[1, 2], [3, 4]] shallow original.copy() # 浅拷贝 # 或者 shallow copy.copy(original)copy()只复制了最外面那层容器里面的元素还是原来那些对象的引用。画个图理解一下original指向一个列表对象Ashallow指向一个新列表对象BB里的两个元素指向的依然是A里的那两个子列表所以当你修改B的外层结构比如shallow.append(5)A不受影响。但当你修改B里的内层列表比如shallow[0].append(99)A里对应的内层列表也会变因为它们是同一个对象。original [[1, 2], [3, 4]] shallow original.copy() shallow[0].append(99) print(original[0]) # [1, 2, 99] —— 原件被改了 print(shallow[0]) # [1, 2, 99]这就是我那个权限系统翻车的原因——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]深拷贝的规则是遇到一个对象就复制一个遇到嵌套的对象就继续递归复制直到所有东西都是全新的。官方文档的解释更精准浅拷贝构造一个新的复合对象然后把原始对象中找到的对象的引用插进去深拷贝构造一个新的复合对象然后递归地把原始对象里找到的对象的副本插进去。用我那个权限系统的例子import copy default_permission { admin: False, settings: {theme: light, notifications: True} } # 用深拷贝代替浅拷贝 user_perm copy.deepcopy(default_permission) user_perm[settings][notifications] False print(default_permission[settings][notifications]) # True —— 不受影响深拷贝出来的对象就是完完全全的新对象不管是对象本身还是对象中包含的子对象都和原始对象不一样。浅拷贝出来的对象则是“外新内旧”——对象本身是新的但子对象还是原来的。第四步什么时候用哪个用浅拷贝的场景数据结构是单层的没有嵌套比如[1, 2, 3]你明确知道嵌套对象不需要独立共享反而更好对性能敏感浅拷贝比深拷贝快得多用深拷贝的场景数据结构有嵌套列表套列表、字典套字典你需要在副本上修改嵌套对象但不想影响原始数据多线程环境下传递数据需要确保线程间数据隔离用户配置、订单数据、模板生成这类场景性能方面要注意深拷贝比浅拷贝慢得多。有测试显示深拷贝比浅拷贝慢约15倍。对于深层嵌套的结构甚至可能慢100倍。所以别盲目用深拷贝该用浅拷贝的时候就用浅拷贝。第五步还有几个坑坑一循环引用如果对象自己引用自己深拷贝可能会出问题a [] a.append(a) # 自己引用自己 # copy.deepcopy(a) # 可能报 RecursionError不过Python的deepcopy()内部用了memo字典来记录已复制的对象能处理大部分循环引用的情况。坑二不可变对象对于不可变对象数字、字符串、元组浅拷贝和深拷贝没啥区别a 5 b copy.copy(a) c copy.deepcopy(a) # b和c就是5跟a没关系坑三自定义对象如果你的类里有嵌套结构记得实现__copy__和__deepcopy__方法来控制拷贝行为。总结一句话记住区别浅拷贝只复制表面里面的东西还是共享的深拷贝从头到尾全部复制完全独立。用我翻车的那个权限系统打个比方赋值b a给同一本书贴两个标签撕掉一个另一个还在浅拷贝copy()复印了书的封面但里面的每一页还是原来那本深拷贝deepcopy()把整本书从头到尾复印了一遍崭新的一本现在我写代码但凡遇到嵌套数据结构要复制都会停下来想一想我到底要的是“复印封面”还是“复印全书”想清楚再动手少加两天班。