
浅拷贝与深拷贝Python 中给变量做赋值操作时很多人会默认赋值就是复制一份。这个直觉在一部分场景下是对的但在涉及可变对象mutable object时就不可靠了。浅拷贝和深拷贝这两个概念归根结底是在回答一个问题复制一个对象时它的内部内容到底复制了几层。要把这个问题说清楚需要从 Python 的变量模型开始。一、变量名不是盒子是标签在 C 语言中变量可以被理解为一个盒子——你创建一个int a 5内存里分配了 4 字节的空间5这个值被放进了这个盒子里。赋值int b a会再分配一个新盒子把5复制一份放进去。两个变量各自独立改a不影响b。Python 的变量模型与此不同。Python 中的变量名是一个引用reference指向内存中的一个对象。你可以把它理解为贴在对象上的标签而不是装对象的盒子。a[1,2,3]ba b.append(4)print(a)# [1, 2, 3, 4]print(b)# [1, 2, 3, 4]print(aisb)# Truea [1, 2, 3]做的事情是在内存中创建一个列表对象[1, 2, 3]然后把标签a贴在这个对象上。b a并没有创建新对象它只是又拿了一张标签b贴在了同一个对象上。所以通过b去修改列表内容a看到的也是修改后的结果因为它们指向的是同一个东西。a is b返回True说明两个变量名指向同一个对象即内存地址相同。对于不可变对象immutable object这种共享不会产生问题a100ba b200print(a)# 100print(b)# 200b 200并没有修改原来100所在的内存而是让标签b指向了一个新的整数对象200。标签a仍然贴在100上。Python 中的整数、字符串、元组tuple都是不可变对象一旦创建就不能被修改内容。但列表、字典、集合是可变对象它们的内容可以被原地修改。当两个变量名引用同一个可变对象时通过任何一个名字做的修改对另一个名字都可见。这就是浅拷贝和深拷贝要处理的核心问题。二、浅拷贝只复制最外层浅拷贝shallow copy会创建一个新的容器对象但不会递归地复制容器内部的元素。新容器中的元素和原容器中的元素指向同一组对象。用代码来说明importcopy original[1,2,[3,4]]shallowcopy.copy(original)print(shallow)# [1, 2, [3, 4]]print(originalisshallow)# False — 外层是新对象print(original[2]isshallow[2])# True — 内层的 [3, 4] 仍是同一个original is shallow返回False说明浅拷贝确实创建了一个新的列表对象。但original[2] is shallow[2]返回True意味着内部嵌套的列表[3, 4]没有被复制——新列表和旧列表共享了同一个子列表对象。这时候修改子列表就会出现预期之外的行为shallow[2].append(5)print(original)# [1, 2, [3, 4, 5]] ← 被改了print(shallow)# [1, 2, [3, 4, 5]]shallow是一个独立的外层列表修改shallow[0]不影响original[0]但它内部引用的可变对象和original共享。浅拷贝有几种等价的触发方式# 方式一copy.copy()importcopy new_listcopy.copy(original)# 方式二list() 构造器new_listlist(original)# 方式三切片new_listoriginal[:]# 方式四列表推导式new_list[xforxinoriginal]这四种方式对一维列表内部不嵌套可变对象的语义完全一致。但对于嵌套结构它们都是浅拷贝——只复制了一层。字典的浅拷贝也类似original{a:1,b:[2,3]}shalloworiginal.copy()# dict.copy() 是浅拷贝shallowdict(original)# 等价shallowcopy.copy(original)# 等价shallow[b].append(4)print(original[b])# [2, 3, 4] ← 被改了三、深拷贝递归复制所有层级深拷贝deep copy会递归地复制对象及其内部包含的所有嵌套对象直到所有层级都是独立的副本。importcopy original[1,2,[3,4]]deepcopy.deepcopy(original)print(originalisdeep)# Falseprint(original[2]isdeep[2])# False — 内层列表也是新对象deep[2].append(5)print(original)# [1, 2, [3, 4]] ← 没被改print(deep)# [1, 2, [3, 4, 5]]deepcopy递归遍历了原对象的所有嵌套层每一层的可变对象都被复制了一份。修改deep中任何层级的元素都不会影响到original。对于多层嵌套的结构original{users:[{name:Alice,scores:[90,85]},{name:Bob,scores:[78,92]}]}deepcopy.deepcopy(original)deep[users][0][scores].append(88)print(original[users][0][scores])# [90, 85] ← 没被改print(deep[users][0][scores])# [90, 85, 88]四、浅拷贝和深拷贝的内存结构用图来表示original [1, 2, [3, 4]]经历赋值、浅拷贝、深拷贝后的内存状态。直接赋值b a变量名 对象 a ──────────→ [1, 2, ●───→ [3, 4]] b ──────────↗a和b是同一个对象的两个标签。只有一块列表对象内层的[3, 4]也只有一份。浅拷贝b copy.copy(a)变量名 对象 a ──────────→ [1, 2, ●───→ [3, 4]] ← 内层共享 b ──────────→ [1, 2, ●──↗外层列表是两个独立的对象但它们内部索引[2]指向同一个子列表[3, 4]。修改子列表的内容a和b都能看到变化。深拷贝b copy.deepcopy(a)变量名 对象 a ──────────→ [1, 2, ●───→ [3, 4]] b ──────────→ [1, 2, ●───→ [3, 4]] ← 完全独立外层列表是独立的内层子列表也是独立的。两层之间没有任何共享。五、copy模块的实现细节Python 标准库的copy模块源码值得读一读尤其是理解它的类型分派与协议回退两层机制。copy.copy()和copy.deepcopy()的内部逻辑分为两步走先查内部分派表dispatch table查不到才走对象协议。内部分派表快速路径为了保证基础数据结构的复制性能copy模块内部维护了两张字典——_copy_dispatch和_deepcopy_dispatch——对list、dict、tuple、set、frozenset、bytearray等内置类型硬编码了专用的复制函数。调用copy.copy()或copy.deepcopy()时模块会先用type(x)作为键去查分派表命中就直接调用对应的函数不走任何协议检查。# copy.py 源码CPython简化示意_copy_dispatch{list:list.copy,# list 的浅拷贝直接用 list.copy()dict:dict.copy,# dict 的浅拷贝直接用 dict.copy()set:set.copy,# set 的浅拷贝直接用 set.copy()frozenset:lambdax:x,# frozenset 不可变直接返回自身# ... 其他内置类型}这意味着对于最常见的内置容器copy.copy([1, 2, 3])和直接调[1, 2, 3].copy()的开销几乎一样——省去了协议查找的类型检查开销。协议回退通用路径当分派表中没有匹配的类型通常是自定义类copy模块才会依次尝试以下协议__copy__协议如果对象定义了__copy__()方法copy.copy()会调用它来获取浅拷贝结果__deepcopy__协议如果对象定义了__deepcopy__(memo)方法copy.deepcopy()会调用它来获取深拷贝结果pickling 协议如果对象没有定义上述方法copy模块会退而使用__reduce_ex__、__reduce__、__getstate__/__setstate__等 pickle 协议来重建对象类型注册机制copyreg模块允许为特定类型注册自定义的复制函数注册后也会被分派表查询到整个流程可以概括为分派表命中 → 直接复制分派表未命中 → 协议回退 → pickle 回退 → copyreg 注册。memo字典是deepcopy的核心机制。它记录了原对象 → 副本对象的映射关系。在递归复制过程中如果遇到一个已经被复制过的对象比如两个列表引用了同一个子列表deepcopy不会重复创建新副本而是复用之前已经创建的那个。这保证了深拷贝正确处理循环引用和共享引用importcopy# 共享引用inner[1,2]original[inner,inner]# 两个元素指向同一个子列表deepcopy.deepcopy(original)print(original[0]isoriginal[1])# Trueprint(deep[0]isdeep[1])# True — 共享关系被保留了deep[0].append(3)print(original[0])# [1, 2] ← 没被改print(deep[0])# [1, 2, 3]print(deep[1])# [1, 2, 3] ← 因为共享引用deep[0] 和 deep[1] 仍是同一个对象如果deepcopy不维护 memo 字典deep[0]和deep[1]会变成两个独立对象共享引用的语义就丢失了。循环引用也能正确处理a[1,2]a.append(a)# a 引用了自身deepcopy.deepcopy(a)# deepcopy 不会无限递归memo 字典会检测到 a 已经在复制过程中六、不同类型对象的拷贝行为Python 内置类型的拷贝行为并不完全一致有些需要特别注意。6.1 元组tuple元组是不可变对象但元组内部可以包含可变对象。浅拷贝一个包含列表的元组外层元组不会被复制因为不可变对象本身无需复制但copy.copy()仍然会返回同一个元组对象因为它是不可变的复制没有意义importcopy t(1,2,[3,4])shallowcopy.copy(t)print(tisshallow)# True — 不可变对象浅拷贝直接返回自身# 但如果用 deepcopydeepcopy.deepcopy(t)print(tisdeep)# False — 因为内部有可变对象deepcopy 会创建新元组print(t[2]isdeep[2])# False — 内部的 [3, 4] 被复制了这反映了一个设计原则不可变对象如果在所有层级上都只包含不可变对象它本身就是安全的无需复制。但如果不可变对象内部嵌套了可变对象问题就回来了。6.2 字典dict字典的浅拷贝通过dict.copy()或copy.copy()完成。和列表一样只复制外层字典结构key 和 value 仍然是原始引用original{config:{debug:True},data:[1,2]}shalloworiginal.copy()shallow[config][debug]Falseprint(original[config][debug])# False ← 被改了6.3 自定义类自定义类的默认拷贝行为取决于它的__dict__和__slots__classPoint:def__init__(self,x,y):self.xx self.yy p1Point(1,2)p2copy.copy(p1)print(p1isp2)# False — 创建了新的 Point 实例print(p1.xisp2.x)# True — 但 x 的值整数是不可变对象共享不影响classNode:def__init__(self,value,childrenNone):self.valuevalue self.childrenchildrenor[]n1Node(root,[Node(child_1),Node(child_2)])n2copy.copy(n1)print(n1isn2)# Falseprint(n1.childrenisn2.children)# True — 浅拷贝children 列表被共享n2.children[0].valuemodifiedprint(n1.children[0].value)# modified ← 被改了自定义类的deepcopy会递归复制__dict__中所有属性。如果类中包含不可序列化的字段比如打开的文件句柄、数据库连接deepcopy可能会失败或产生无意义的副本。6.4 特殊类型类型浅拷贝行为深拷贝行为备注int,float,str返回自身返回自身不可变无需复制tuple返回自身除非内部有可变对象且__deepcopy__被调用递归复制内层可变对象创建新元组含可变对象的元组在 deepcopy 时会被复制list创建新列表元素引用共享递归复制所有层级标准容器dict创建新字典key/value 引用共享递归复制所有层级标准容器set创建新集合元素引用共享递归复制所有层级元素本身必须是可哈希的frozenset返回自身返回自身不可变function返回自身返回自身函数对象不支持拷贝module返回自身返回自身模块对象不支持拷贝七、is和的区别讨论拷贝时经常需要区分这两个操作符。a is b判断两个变量是否指向同一个对象比较的是id(a) id(b)即内存地址a b判断两个对象的值是否相等调用a.__eq__(b)a[1,2,3]b[1,2,3]print(ab)# True — 值相等print(aisb)# False — 不是同一个对象caprint(aisc)# True在拷贝的语境中浅拷贝后original copy一定为True如果内部元素定义了正确的__eq__但original is copy为False深拷贝后同理但深拷贝保证所有嵌套层级都没有共享引用八、什么时候需要深拷贝实际开发中直接用deepcopy的场景其实不多。大部分情况下要么不需要复制函数参数传递时用不可变对象要么浅拷贝就够了。需要深拷贝的典型场景递归数据结构的修改。树、图这类数据结构经常需要在修改某个分支时保留原始状态。如果树的节点用列表或字典表示浅拷贝会导致分支之间互相污染。# 不用 deepcopy 的后果defadd_branch(tree,path,value):在树的某条路径上添加值nodetreeforkeyinpath[:-1]:nodenode[key]node[path[-1]]value tree1{a:{b:{c:1}}}tree2copy.copy(tree1)# 浅拷贝add_branch(tree2,[a,b,d],2)print(tree1)# {a: {b: {c: 1, d: 2}}} ← tree1 被污染了用copy.deepcopy(tree1)就不会有这个问题。缓存和快照。系统需要保存某个数据结构的历史状态时必须确保快照和当前状态完全独立。deepcopy是最直接的方式虽然不一定是性能最优的——对于大型对象序列化到 JSON 再反序列化可能更快因为 C 实现的 JSON 编解码器比 Python 层的递归遍历快。函数参数的防御性复制。如果你的函数接收一个可变对象作为参数且需要在函数内部修改它但不想影响外部传入的对象defprocess_data(data):working_copycopy.deepcopy(data)working_copy[results]compute(working_copy[raw])returnworking_copy这在编写库代码时比较常见——你不知道调用者会不会在传参之后继续使用原始对象。九、深拷贝的性能开销deepcopy的代价在于它需要递归遍历整个对象图object graph为每个可变对象创建新副本并维护 memo 字典来追踪已复制的对象。对于一个包含 N 个对象的结构deepcopy的时间和空间复杂度都是 O(N)。在性能敏感的场景中可以考虑替代方案手动构造新对象对于结构已知的对象显式创建新实例并逐字段赋值比deepcopy更快因为跳过了类型检查和 memo 管理的开销序列化/反序列化json.loads(json.dumps(obj))对于纯 JSON 兼容的结构字典、列表、字符串、数字性能往往优于deepcopy因为 JSON 编解码器是 C 实现的。缺点是无法处理自定义类、集合等非 JSON 类型copyreg注册对于频繁拷贝的自定义类通过copyreg注册一个高效的复制函数避免deepcopy的通用遍历逻辑十、总结操作外层对象内部可变对象典型方式赋值b a同一个对象同一个对象直接赋值浅拷贝新对象共享原对象copy.copy()、切片、list()、dict.copy()深拷贝新对象也是新对象copy.deepcopy()选择拷贝方式的原则如果对象内部没有可变元素比如纯数字列表[1, 2, 3]浅拷贝和深拷贝效果一样用浅拷贝就行如果对象内部有可变元素且你需要在拷贝后独立修改这些内层元素用深拷贝如果只是想把外层容器独立出来比如从函数里返回一个新列表但列表里的元素只是读取、不会修改浅拷贝就足够了性能敏感场景中优先考虑手动构造或序列化方案替代deepcopy