← 返回Python进阶编程目录
← 返回学习笔记首页
专题: Python进阶编程系统学习
关键词: Python, 浅拷贝, 深拷贝, copy, __copy__, __deepcopy__, __getstate__, 可变性
一、概述
在Python编程中,对象的拷贝是一个看似简单却暗藏陷阱的话题。许多初学者甚至有一定经验的开发者,都曾因对拷贝机制理解不透彻而踩过坑——修改了一个列表却意外影响了另一个、函数参数修改导致了外部的副作用、复制对象时得到了意想不到的共享引用……这些问题的根源,都在于对Python对象模型和拷贝机制的理解不够深入。
Python提供三种级别的对象复制方式:赋值(绑定引用) 、浅拷贝 和深拷贝 。三者的核心差异在于对嵌套对象的处理方式。赋值只是创建了一个新的引用,新旧变量指向同一对象;浅拷贝创建新对象,但只复制一层,嵌套的子对象仍共享引用;深拷贝则递归复制所有层级的对象,生成完全独立的副本。
核心概念: 理解"引用"与"对象"的区别是掌握拷贝机制的基础。Python中变量名是对象的"标签"(引用),而不是存储对象的盒子。赋值操作传递的是引用,而非对象本身。
学习目标: 学完本笔记后,你应当能够准确区分三种赋值方式的差异,掌握浅拷贝的三种实现方法,理解深拷贝的递归机制,能够自定义对象的拷贝行为,并能在实际项目中做出正确的拷贝选择。
二、Python中的对象引用与变量
在深入拷贝之前,必须首先理解Python的变量本质。Python中的变量不同于C或Java中的变量——它不是存储值的内存容器,而是指向对象的"名称"或"标签"。
2.1 变量即引用
Python是一门"一切皆对象"的语言。当你写下 a = [1, 2, 3] 时,实际发生的事情是:在内存中创建一个列表对象 [1, 2, 3],然后将名字 a 绑定到这个对象上。如果接着写 b = a,并不会创建新的列表,而是将名字 b 也绑定到同一个列表对象上。
import sys
a = [1 , 2 , 3 ]
b = a # 仅仅是绑定引用,不复制对象
print (id (a)) # 输出:140234567890123(举例)
print (id (b)) # 输出:140234567890123(与a完全相同)
print (a is b) # 输出:True(is 判断是否为同一对象)
b.append(4 ) # 通过b修改列表
print (a) # 输出:[1, 2, 3, 4](a也受到影响!)
关键洞察: b = a 并没有创建副本,它只是给现有的列表对象贴了一个新标签。修改通过任何一个标签进行,另一个标签看到的是同一个被修改的对象。这就是"赋值即引用"的本质含义。
2.2 不可变对象的特殊情况
对于不可变对象(如整数、字符串、元组),Python有时会进行内部优化(如小整数缓存、字符串驻留),但这并不改变"变量即引用"的本质。不可变对象的"不可变"意味着对象本身不能被修改,但变量可以重新绑定到其他对象。
x = 42
y = x # y 绑定到 42 这个整数对象
print (x is y) # True(小整数缓存机制)
x = x + 1 # 创建新整数对象 43,x 重新绑定
print (x) # 43
print (y) # 42(y 仍然指向原来的 42)
print (x is y) # False(现在指向不同对象)
这里的关键是:当执行 x = x + 1 时,Python创建了新的整数对象 43,然后让 x 指向它。原来 y 指向的 42 并未受影响。这与列表的原地修改(append等)行为完全不同——列表是可变对象,可以原地改变自身内容。
三、浅拷贝(Shallow Copy)
浅拷贝创建一个新的容器对象,然后用原始对象中包含的对象的引用填充它。换句话说,浅拷贝只复制"一层"——新容器中的元素仍然是原始容器中元素的引用。如果容器中存储的是不可变对象,这通常没有影响;但如果存储的是可变对象(如嵌套列表、字典等),修改嵌套对象会同时影响原始对象和副本。
3.1 浅拷贝的三种方式
Python提供了多种实现浅拷贝的方式,适用于不同的场景。
方式一:切片操作 [:]
切片是列表最常用的浅拷贝方式。[:] 表示从头到尾的完整切片,会创建一个新的列表对象。
original = [[1 , 2 ], [3 , 4 ], [5 , 6 ]]
shallow = original[:] # 通过切片创建浅拷贝
print (shallow is original) # False(新的容器对象)
print (shallow[0 ] is original[0 ]) # True(元素引用同一对象!)
# 修改内层对象
shallow[0 ].append(99 )
print (original) # [[1, 2, 99], [3, 4], [5, 6]](原始对象也变了!)
# 修改外层(重新赋值)
shallow[1 ] = [100 , 200 ]
print (original[1 ]) # [3, 4](不受影响——这是重新绑定,不是修改)
方式二:.copy() 方法
Python 3.3+ 引入了 list.copy() 方法,语义与 [:] 完全相同,但可读性更好。
dict_original = {'a' : [1 , 2 ], 'b' : [3 , 4 ]}
dict_shallow = dict_original.copy() # 字典的浅拷贝
dict_shallow['a' ].append(5 )
print (dict_original) # {'a': [1, 2, 5], 'b': [3, 4]}(也被修改了)
# 字典的浅拷贝也适用于 set 等集合类型
set_original = {{'x' : 1 }, {'y' : 2 }} # 不可哈希——这行只是为了说明,实际set元素必须可哈希
方式三:copy.copy() 函数
copy.copy() 是 copy 标准库模块提供的通用浅拷贝接口。它的优势在于可以处理任何支持拷贝的对象,包括自定义类的实例。
import copy
class Node:
def __init__ (self, value, children=None ):
self.value = value
self.children = children or []
n1 = Node('root' , [Node('child1' ), Node('child2' )])
n2 = copy.copy(n1) # 浅拷贝
print (n2 is n1) # False(新对象)
print (n2.children is n1.children) # True(共享同一个children列表!)
# 修改共享的列表
n2.children.append(Node('child3' ))
print (len (n1.children)) # 3(n1也被影响了!)
3.2 浅拷贝的适用场景
浅拷贝并非"不好的"拷贝方式,恰当地使用它非常高效:
扁平结构: 对象只有一层,没有嵌套的可变子对象时,浅拷贝就完全足够
不可变嵌套: 所有嵌套子对象都是不可变类型(如字符串、元组),浅拷贝和深拷贝效果相同
性能敏感场景: 浅拷贝只复制顶层容器,不递归,性能远高于深拷贝
只读共享: 明确需要共享内部状态的场景(如某些缓存实现)
# 扁平结构——浅拷贝足够安全
flat = [1 , 2 , 3 , 'hello' , (4 , 5 )]
flat_copy = copy.copy (flat)
flat_copy[0 ] = 99 # 替换外层元素——安全
flat_copy[3 ] = 'world' # 替换为新的字符串——安全(字符串不可变)
print (flat) # [1, 2, 3, 'hello', (4, 5)]——不受影响
四、深拷贝(Deep Copy)
深拷贝递归地复制对象及其所有子对象,构建一个与原对象完全独立的副本。原对象和副本之间不存在任何共享引用——对副本的任何修改都不会影响原对象,反之亦然。
4.1 copy.deepcopy() 基本用法
深拷贝通过 copy.deepcopy() 函数实现。它遍历对象的所有层级,为遇到的每一个对象创建全新的副本。
import copy
nested = [[1 , 2 , [3 , 4 ]], {'key' : [5 , 6 ]}, (7 , 8 , [9 , 10 ])]
deep = copy.deepcopy (nested)
print (deep is nested) # False
print (deep[0 ] is nested[0 ]) # False(内层列表也是新对象)
print (deep[0 ][2 ] is nested[0 ][2 ]) # False(嵌套两层仍然独立)
print (deep[1 ] is nested[1 ]) # False(字典也是新对象)
print (deep[2 ] is nested[2 ]) # True(元组不可变——因为要修改元组必须创建新对象)
# 验证完全独立
deep[0 ][2 ].append(999 )
print (nested[0 ][2 ]) # [3, 4](原对象完全不受影响!)
关于元组的说明: 上述代码中 deep[2] is nested[2] 的结果为 True,这并非深拷贝的缺陷。因为元组是不可变对象,如果 deepcopy 为元组创建新副本,新副本与原对象在内容上完全一致且都不可能被修改,那么创建新副本就是浪费。deepcopy 对不可变对象的优化是合理的——既然不可能被修改,共享引用也没有风险。
4.2 深拷贝的递归特性
deepcopy 的核心是递归算法。它会遍历对象的所有属性(通过 __dict__ 或 __slots__),对每个属性递归调用 deepcopy,直到遇到不可变对象或基本类型为止。
递归深拷贝的简化实现可以这样理解:
def simplified_deepcopy (obj, memo=None ):
if memo is None :
memo = {}
# 1. 如果已经复制过这个对象,直接返回缓存的副本
obj_id = id (obj)
if obj_id in memo:
return memo[obj_id]
# 2. 处理不可变类型——直接返回
if isinstance (obj, (int , float , str , bytes )):
return obj
# 3. 处理容器类型——递归复制每个元素
if isinstance (obj, list ):
new = []
memo[obj_id] = new # 先注册,防止自引用无限递归
for item in obj:
new.append(simplified_deepcopy (item, memo))
return new
if isinstance (obj, dict ):
new = {}
memo[obj_id] = new
for k, v in obj.items():
new[simplified_deepcopy (k, memo)] = simplified_deepcopy (v, memo)
return new
# 4. 处理自定义对象——创建空实例后递归填充属性
if hasattr (obj, '__dict__' ):
cls = type (obj)
new = cls.__new__(cls)
memo[obj_id] = new
for attr, val in obj.__dict__.items():
setattr (new, attr, simplified_deepcopy (val, memo))
return new
return obj # 兜底(实际copy.deepcopy远此复杂)
注意上述代码是教学演示,真实的 copy.deepcopy 实现要复杂得多——它需要处理 __slots__、__getstate__/__setstate__、模块类型、文件对象、具有特殊构造函数的类型等众多边界情况。
4.3 共享引用问题的处理
当对象结构中存在共享引用时,深拷贝能否正确保持这种共享关系?答案是:默认情况下,deepcopy 会在新副本中"打破"共享关系,为每个对象创建独立的副本。如果需要在副本中保持同样的共享模式,需要自定义拷贝行为。
import copy
# 创建一个有共享引用的结构
shared = ['shared' ]
obj = [shared, shared, shared] # 同一个对象被引用了三次
print (obj[0 ] is obj[1 ]) # True(共享引用)
obj_deep = copy.deepcopy (obj)
print (obj_deep[0 ] is obj_deep[1 ]) # True(共享引用被保持了!)
print (obj_deep[0 ] is shared) # False(但新副本与原对象无关)
# 这是因为 deepcopy 的 memo 机制记住了已经复制过的对象
obj_deep[0 ].append('new' )
print (obj_deep[1 ]) # ['shared', 'new'](共享关系被保持!三个引用都受影响)
print (obj[0 ]) # ['shared'](原对象不受影响)
4.4 无限递归与自引用结构
如果对象中存在循环引用(自引用),天真地递归复制会导致无限递归和栈溢出。deepcopy 通过 memo 字典(备忘录)优雅地解决了这个问题:在复制一个对象之前,先检查它是否已被复制过,如果是,则返回已缓存的副本。
风险提示: 使用自定义 __deepcopy__ 方法时,如果忘记传递 memo 参数或未正确维护 memo,很容易导致无限递归。务必确保你的自定义实现正确处理 memo 字典。
import copy
# 创建一个自引用的列表(循环引用)
cyclic = [1 , 2 , 3 ]
cyclic.append(cyclic) # 自己引用自己
print (cyclic) # [1, 2, 3, [...]]
print (cyclic[3 ] is cyclic) # True
# 深拷贝自引用结构——没有栈溢出!
cyclic_deep = copy.deepcopy (cyclic)
print (cyclic_deep) # [1, 2, 3, [...]]
print (cyclic_deep[3 ] is cyclic_deep) # True(自引用关系保持)
print (cyclic_deep is cyclic) # False(副本与原对象独立)
五、可变与不可变对象的拷贝行为差异
Python中对象分为可变(mutable)和不可变(immutable)两大类。可变对象包括列表、字典、集合以及自定义类的实例;不可变对象包括整数、浮点数、字符串、元组、frozenset等。这一区别对拷贝行为有深远影响。
5.1 不可变对象的"假拷贝"
当你对不可变对象执行浅拷贝或深拷贝时,Python不会创建新对象,而是返回原对象的引用。这是因为不可变对象的值永远不会改变,共享引用不会产生任何安全隐患。
import copy
t = (1 , 2 , 3 )
t_copy1 = copy.copy (t)
t_copy2 = copy.deepcopy (t)
print (t_copy1 is t) # True(copy.copy 对不可变对象直接返回原对象)
print (t_copy2 is t) # True(copy.deepcopy 也一样)
# 但如果元组中包含可变对象,情况就不同了
t_mixed = (1 , [2 , 3 ], {'a' : 4 })
t_shallow = copy.copy (t_mixed)
t_deep = copy.deepcopy (t_mixed)
print (t_shallow is t_mixed) # True(元组本身还是同一个)
print (t_shallow[1 ] is t_mixed[1 ]) # True(浅拷贝,列表共享)
print (t_deep[1 ] is t_mixed[1 ]) # False(深拷贝,列表是新对象)
5.2 可变对象的拷贝行为
可变对象的浅拷贝和深拷贝都会创建新对象。区别在于子对象的处理方式。
浅拷贝(新建顶层容器)
new_object is not original
new_object[0] is original[0] (嵌套对象共享)
深拷贝(完全独立)
new_object is not original
new_object[0] is not original[0] (嵌套对象也复制)
5.3 完整对比表
操作
顶层对象
嵌套可变对象
嵌套不可变对象
b = a
共享(同一对象)
共享
共享
b = copy.copy(a)
新对象
共享
共享(无影响)
b = copy.deepcopy(a)
新对象
新对象
共享(无影响)
六、__copy__ 与 __deepcopy__:自定义拷贝行为
Python 的 copy 模块提供了扩展点,允许自定义类控制自己的拷贝行为。通过定义 __copy__ 和 __deepcopy__ 方法,你可以精确指定类实例被浅拷贝和深拷贝时应当如何进行。
6.1 __copy__ 方法
__copy__ 方法不接受额外参数(除了 self),返回浅拷贝的结果。当你实现这个方法时,copy.copy() 会调用它而不是使用默认行为。
import copy
class DatabaseConnection:
def __init__ (self, host, port, cache=None):
self.host = host
self.port = port
self.cache = cache or {}
def __copy__ (self):
"""浅拷贝:共享缓存数据,但创建一个新的连接配置对象"""
print ("调用 __copy__ 方法" )
# 注意:新对象共享了缓存(浅拷贝的典型行为)
return DatabaseConnection(self.host, self.port, self.cache)
def __repr__ (self):
return f"DBConnection(host={self.host}, port={self.port})"
db1 = DatabaseConnection('localhost' , 5432 )
db1.cache['users' ] = ['alice' , 'bob' ]
db2 = copy.copy (db1)
print (db2.cache is db1.cache) # True(共享缓存)
# 修改缓存会影响所有浅拷贝
db2.cache['new_key' ] = 'value'
print ('new_key' in db1.cache) # True
6.2 __deepcopy__ 方法
__deepcopy__ 方法接受一个 memo 参数(字典),必须将新创建的副本注册到 memo 中,以防止无限递归并保持共享引用关系。
import copy
class TreeNode:
def __init__ (self, value):
self.value = value
self.left = None
self.right = None
self.parent = None # 指向父节点(可能形成循环引用)
def __deepcopy__ (self, memo):
"""深拷贝:递归复制整棵子树,并正确处理 parent 引用"""
print (f"deepcopy: {self.value}" )
# 1. 检查 memo,避免重复复制和循环引用
if id (self) in memo:
return memo[id (self)]
# 2. 创建新实例(不调用 __init__)
new_node = TreeNode.__new__(TreeNode)
memo[id (self)] = new_node # 必须先注册!
# 3. 递归复制属性
new_node.value = copy.deepcopy (self.value, memo)
new_node.left = copy.deepcopy (self.left, memo) # 递归复制左子树
new_node.right = copy.deepcopy (self.right, memo) # 递归复制右子树
new_node.parent = copy.deepcopy (self.parent, memo) # 正确关联父节点
return new_node
# 构建树结构
root = TreeNode('root' )
left = TreeNode('left' )
right = TreeNode('right' )
root.left = left
root.right = right
left.parent = root
right.parent = root
# 深拷贝
root_copy = copy.deepcopy (root)
print (root_copy is root) # False(不同对象)
print (root_copy.left.parent is root_copy) # True(parent 指向正确的副本)
print (root_copy.right.parent is root_copy) # True(parent 指向正确的副本)
6.3 自定义拷贝的最佳实践
始终将新对象注册到 memo: 在 __deepcopy__ 中,创建新实例后立即 memo[id(self)] = new_obj,然后再递归复制其他属性。顺序很重要——必须先注册,后递归。
调用 copy.deepcopy 而非手动复制: 在 __deepcopy__ 内部递归子对象时,应使用 copy.deepcopy(sub_obj, memo) 而非手动编写复制逻辑,这样能正确处理各种类型。
区分 __copy__ 和 __deepcopy__: 两者承担不同的职责。如果只定义了 __copy__ 而没有定义 __deepcopy__,copy.deepcopy 仍会使用默认的深拷贝逻辑。
利用 __copy__ 实现高效的深拷贝替代: 对于某些不可变或只读对象,__copy__ 返回 self 可以完全避免拷贝开销。
七、deepcopy 的 memo 参数详解
memo 是 deepcopy 内部使用的"备忘录"字典,它在深拷贝过程中承担两个关键职责:
避免重复复制: 当同一个对象被多次引用时,memo 确保它只被复制一次,后续引用直接返回已创建的副本
打破循环引用: 对于自引用结构(如双向链表、树结构的 parent 指针),memo 防止无限递归
7.1 memo 的工作原理
memo 是一个以 id(original) 为键、以副本对象为值的映射表。在 deepcopy 的递归过程中:
import copy
# 观察 memo 的工作过程
class WatchedObject:
def __init__ (self, name):
self.name = name
self.ref = None
def __deepcopy__ (self, memo):
# 检查 memo:如果已经复制过,立刻返回
if id (self) in memo:
print (f" 命中缓存: {self.name}" )
return memo[id (self)]
print (f" 开始复制: {self.name}" )
new = WatchedObject.__new__(WatchedObject)
memo[id (self)] = new # 立即注册
new.name = copy.deepcopy (self.name, memo)
new.ref = copy.deepcopy (self.ref, memo) # 递归处理引用
print (f" 完成复制: {self.name}" )
return new
def __repr__ (self):
return f"Watched({self.name})"
a = WatchedObject('A' )
b = WatchedObject('B' )
a.ref = b
b.ref = a # 形成循环引用
print ("开始深拷贝..." )
a_copy = copy.deepcopy (a)
print ("深拷贝完成" )
print (a_copy.ref.ref is a_copy) # True(循环引用正确保持)
7.2 外部传入 memo
你可以在调用 deepcopy 时传入一个已有的 memo 字典。这在需要跨多个独立对象保持共享引用关系时非常有用。
import copy
class SharedData:
def __init__ (self, data):
self.data = data
shared_resource = SharedData([1 , 2 , 3 ])
# 两个对象共享同一个 resource
container1 = [shared_resource, 'first' ]
container2 = [shared_resource, 'second' ]
# 分别深拷贝——每个容器中的 shared_resource 被复制成独立对象
c1_independent = copy.deepcopy (container1)
c2_independent = copy.deepcopy (container2)
print (c1_independent[0 ] is c2_independent[0 ]) # False(两份独立副本)
# 使用同一个 memo 深拷贝——共享关系被保持
memo = {}
c1_shared = copy.deepcopy (container1, memo)
c2_shared = copy.deepcopy (container2, memo)
print (c1_shared[0 ] is c2_shared[0 ]) # True(共享同一个副本!)
应用场景: 当需要深拷贝一个复杂对象图(object graph),且图中存在跨对象的共享引用时,使用同一个 memo 字典可以确保共享关系在新副本中得以保持。这在序列化/反序列化、缓存复制、模型克隆等场景中非常有用。
八、__getstate__ 与 __setstate__ 在深拷贝中的作用
__getstate__ 和 __setstate__ 是 Python 中与对象序列化相关的两个特殊方法,它们在 pickle 模块中广泛使用。有趣的是,copy.deepcopy 也会检查并利用这两个方法,因为它们提供了一种精确控制对象状态提取和恢复的机制。
8.1 基本机制
当 deepcopy 遇到一个定义了 __getstate__ 方法的对象时,会调用该方法获取对象的"可复制状态"。然后创建一个空实例(不调用 __init__),再调用 __setstate__ 将状态恢复到新对象中。
import copy
class SecureDocument:
def __init__ (self, content, password):
self.content = content
self._password = password # 敏感信息,不应出现在副本中
self._cache = {} # 缓存数据,复制后应清空
self._loaded = True
def __getstate__ (self):
"""控制哪些状态被复制到深拷贝中"""
print ("调用 __getstate__" )
# 返回一个 dict,包含需要复制的状态
return {
'content' : self.content,
# 故意不包含 _password 和 _cache
'_loaded' : False , # 重置加载状态
}
def __setstate__ (self, state):
"""从深拷贝恢复状态"""
print ("调用 __setstate__" )
self.content = state['content' ]
self._password = None # 安全地设为 None
self._cache = {} # 空缓存
self._loaded = state.get('_loaded' , False )
def __repr__ (self):
return f"SecureDocument(content={self.content!r}, has_pwd={self._password is not None})"
doc = SecureDocument("机密数据" , "secret123" )
doc._cache['计算结果' ] = 42
doc_copy = copy.deepcopy (doc)
print (doc_copy)
print (doc_copy._password) # None(密码未被复制)
print (doc_copy._cache) # {}(缓存被清空)
print (doc_copy._loaded) # False(状态被重置)
8.2 deepcopy 与 pickle 的关系
copy.deepcopy 和 pickle 都使用 __getstate__/__setstate__,但目的不同:
特性
copy.deepcopy
pickle
主要用途
创建对象的在内存中的副本
对象的序列化/反序列化(存储或传输)
调用 __reduce__
否(有单独的复制协议)
是(主要序列化协议)
__getstate__ 支持
是(deepcopy 会检查)
是(pickle 的核心协议)
__setstate__ 支持
是
是
__copy__ 支持
是(浅拷贝协议)
否
__deepcopy__ 支持
是(深拷贝协议)
否
memo 参数
是(核心机制)
否(使用 pickler 内部缓存)
8.3 deepcopy 的内部决策流程
当 deepcopy 处理一个对象时,它会按以下优先级选择复制方式:
检查 memo: 如果对象已被复制,返回缓存的副本
检查 __deepcopy__: 如果定义了,委托给该方法
检查 __getstate__: 如果定义了,使用状态提取+恢复的方式复制
默认行为: 遍历 __dict__ 或 __slots__ 递归复制每个属性
设计建议: 如果你需要在深拷贝时精确控制哪些属性被复制、哪些被排除(如缓存、连接池、密码等),定义 __getstate__ 是比 __deepcopy__ 更简洁的方案。但如果你需要完全控制复制过程的每一步(如处理循环引用、优雅地处理不可复制资源),则应当使用 __deepcopy__。
九、实战应用与常见陷阱
9.1 函数参数的拷贝陷阱
将可变对象作为函数默认参数时,如果不注意拷贝问题,可能导致难以追踪的 bug。
错误示范(共享引用)
def add_item (item, target=[]): # 危险!
target.append(item)
return target
print (add_item (1 )) # [1]
print (add_item (2 )) # [1, 2] —— 污染了!
print (add_item (3 )) # [1, 2, 3] —— 不断累积
正确做法(浅拷贝)
def add_item (item, target=None ):
if target is None :
target = [] # 每次调用创建新列表
target.append(item)
return target
print (add_item (1 )) # [1]
print (add_item (2 )) # [2] —— 干净!
9.2 多维列表初始化
这是一个经典陷阱,源于对浅拷贝机制理解不足。
错误示范
# [[0]*3]*3 创建了三个对同一行的引用!
matrix = [[0 ] * 3 ] * 3
matrix[0 ][0 ] = 1
print (matrix)
# [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
# 所有"行"都变了!
正确做法
# 使用列表推导式,每行单独创建
matrix = [[0 ] * 3 for _ in range (3 )]
matrix[0 ][0 ] = 1
print (matrix)
# [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
# 只有第一行被修改
9.3 不可复制对象的处理
某些对象(如文件句柄、网络连接、线程锁)本质上不支持复制。深拷贝它们会引发 TypeError。
import copy
import threading
lock = threading.Lock()
try :
lock_copy = copy.deepcopy (lock)
except TypeError as e:
print (f"无法复制锁对象:{e}" )
# 解决方案:自定义 __deepcopy__ 或 __getstate__
class SafeLock:
def __init__ (self):
self._lock = threading.Lock()
self._owner = None
def __deepcopy__ (self, memo):
# 创建新实例,锁对象不被复制
new_obj = SafeLock.__new__(SafeLock)
memo[id (self)] = new_obj
new_obj._lock = threading.Lock() # 创建新的锁
new_obj._owner = copy.deepcopy (self._owner, memo)
return new_obj
safe = SafeLock()
safe_copy = copy.deepcopy (safe) # 正常工作
print (safe_copy._lock is safe._lock) # False
9.4 性能考量
深拷贝的性能开销远高于浅拷贝。对于大型复杂对象图,深拷贝可能消耗大量时间和内存。
import copy
import time
# 模拟一个大型数据结构
big_data = {'level1' : [{'level2' : ['x' * 1000 for _ in range (100 )]}
for _ in range (1000 )}
t0 = time.time ()
shallow = copy.copy (big_data)
t1 = time.time ()
deep = copy.deepcopy (big_data)
t2 = time.time ()
print (f"浅拷贝耗时:{t1 - t0:.4f}秒" )
print (f"深拷贝耗时:{t2 - t1:.4f}秒" )
print (f"深度比:{(t2 - t1) / (t1 - t0):.1f}x" )
# 深拷贝通常比浅拷贝慢数十到数百倍
性能建议:
能用浅拷贝就不要用深拷贝
如需多次深拷贝同一结构,考虑先 pickle 序列化再反序列化(对某些结构更快)
对于游戏对象、物理模拟等性能敏感场景,手动编写专门的复制方法通常比通用 deepcopy 更高效
使用 __deepcopy__ 实现自定义复制可以跳过不必要的复制,显著提升性能
十、核心要点总结
1. 变量即引用: Python 变量是对象的标签,赋值操作传播的是引用而非对象本身。b = a 不创建副本。
2. 浅拷贝复制一层: [:]、.copy()、copy.copy() 都创建新容器,但嵌套的子对象仍共享引用。适合扁平结构或明确需要共享的场景。
3. 深拷贝递归复制: copy.deepcopy() 递归遍历所有层级,为每个可变对象创建独立副本,完全的隔离保护。
4. memo 是深拷贝的基石: 通过备忘录字典避免重复复制和无限递归,同一个对象只被复制一次,共享引用关系得以保持。
5. 不可变对象不会被复制: 整数、字符串、元组等不可变对象在浅拷贝和深拷贝中都返回原引用,这是安全的优化。
6. __copy__ 和 __deepcopy__ 自定义拷贝: 定义这两个方法可以精确控制类的拷贝行为,处理特殊逻辑(如排除密码、重置缓存、处理不可复制资源)。
7. __getstate__ 和 __setstate__: deepcopy 利用这两个方法进行状态提取和恢复,提供了一种简洁的定制拷贝方式。
8. 注意默认参数和多维列表陷阱: 函数默认参数使用可变对象、[[0]*3]*3 都是浅拷贝陷阱的高发区。
9. 性能权衡: 深拷贝比浅拷贝慢数十到数百倍,应根据实际需求选择最合适的复制策略。
10. 选择指南: 赋值(仅需要额外引用) -> 浅拷贝(扁平结构或明确共享) -> 深拷贝(需要完全隔离、对象图复杂多变)。
十一、进一步思考
思考一: 为什么 Python 的默认参数在函数定义时(而非调用时)求值?这与拷贝机制有何关系?
思考二: 在单例模式中,__copy__ 和 __deepcopy__ 应该如何实现?是否应该禁止拷贝单例对象?
思考三: Python 的 dataclasses.dataclass 装饰器生成的类,其 __copy__ 和 __deepcopy__ 行为是怎样的?你能否写一个 dataclass 的混入类,使得它的深拷贝默认排除某些字段?
思考四: 如果你需要实现一个"部分深拷贝"——复制对象的某些子对象但共享其他子对象——应该如何设计 API?