文章目录
在 Python 编程中,我们经常需要复制数据。一个常见的场景是:你有一个复杂的数据结构(比如一个包含用户信息的列表),你想创建一个副本进行修改,同时又不希望影响到原始数据。这时候,如果你只是简单地使用
= 赋值,那你可能已经掉进了第一个"坑"。
要真正地复制一个对象,Python 的 copy 模块为我们提供了两种方式:浅拷贝(Shallow Copy)和深拷贝(Deep Copy)。理解这两者的区别,是编写稳健、无副作用代码的关键一步。
一、从"假复制"开始:赋值操作 (=)
在深入拷贝之前,我们必须先弄清楚最常见的误解:赋值不是复制。
当你执行 new_list = old_list 时,你并没有创建一个新的列表。你只是创建了一个新的变量名(标签),让它指向了和 old_list 完全相同的那个列表对象。
它们是同一个对象的两个不同名字而已。
# 原始列表
old_list = [1, 2, ['a', 'b']]
# 赋值操作
new_list = old_list
print(f"old_list 的内存地址: {id(old_list)}")
print(f"new_list 的内存地址: {id(new_list)}")
# 修改 new_list
new_list[2].append('c')
print(f"修改后,old_list 的内容: {old_list}")
print(f"修改后,new_list 的内容: {new_list}")
输出:
old_list 的内存地址: 4442784960
new_list 的内存地址: 4442784960
修改后,old_list 的内容: [1, 2, ['a', 'b', 'c']]
修改后,new_list 的内容: [1, 2, ['a', 'b', 'c']]
内存地址完全相同,修改 new_list 会直接影响 old_list,因为它们本质上就是同一个东西。这显然不是我们想要的"副本"。
二、浅拷贝 (Shallow Copy): 只复制"表面"
为了解决上面的问题,copy 模块登场了。首先是浅拷贝 copy.copy()。
浅拷贝会创建一个新的顶层对象,但它内部的元素,是原始对象元素的引用(指针)。
可以把它想象成复印一份合同:你得到了一份新的合同文件(新的顶层对象),但如果合同里提到了某个具体的、共享的附件(内部的嵌套对象),两份合同引用的都是同一个附件。
让我们看看代码:
import copy
old_list = [1, 2, ['a', 'b']]
# 进行浅拷贝
shallow_copied_list = copy.copy(old_list)
print(f"--- 内存地址对比 ---")
print(f"old_list: {id(old_list)}")
print(f"shallow_copied_list: {id(shallow_copied_list)}")
print(f"old_list[2] (嵌套列表) 的内存地址: {id(old_list[2])}")
print(f"shallow_copied_list[2] (嵌套列表) 的内存地址: {id(shallow_copied_list[2])}")
print("\n--- 修改数据 ---")
# 修改嵌套列表
shallow_copied_list[2].append('c')
# 修改顶层元素
shallow_copied_list[0] = 99
print(f"修改后,old_list: {old_list}")
print(f"修改后,shallow_copied_list: {shallow_copied_list}")
输出:
--- 内存地址对比 ---
old_list: 4532536512
shallow_copied_list: 4532536640
old_list[2] (嵌套列表) 的内存地址: 4532536448
shallow_copied_list[2] (嵌套列表) 的内存地址: 4532536448
--- 修改数据 ---
修改后,old_list: [1, 2, ['a', 'b', 'c']]
修改后,shallow_copied_list: [99, 2, ['a', 'b', 'c']]
结果分析:
old_list和shallow_copied_list的内存地址不同,说明浅拷贝确实创建了一个新的顶层列表。- 但它们内部的嵌套列表
['a', 'b']的地址是相同的。它们共享着同一个嵌套列表对象。 - 因此,当我们修改这个共享的嵌套列表时(
append('c')),old_list和shallow_copied_list都受到了影响。 - 而当我们修改顶层元素
shallow_copied_list[0] = 99时,由于整数是不可变类型,这相当于给新列表的第一个位置换上了一个新对象,所以old_list不受影响。
浅拷贝的结论:它只拷贝了第一层。如果对象里包含了其他可变对象(如列表、字典),拷贝的只是这些可变对象的引用。
三、深拷贝 (Deep Copy): 彻底的"克隆"
如果你希望得到一个完完全全、彻彻底底的独立副本,无论数据结构有多深,那么你需要深拷贝 copy.deepcopy()。
深拷贝会创建一个新的对象,并递归地复制原始对象中的所有子对象。
继续用合同的比喻:深拷贝不仅复印了合同本身,还把合同里提到的所有附件也全都复印了一份新的。这样,新旧两份合同及其所有附件之间,再无任何关联。
import copy
old_list = [1, 2, ['a', 'b']]
# 进行深拷贝
deep_copied_list = copy.deepcopy(old_list)
print(f"--- 内存地址对比 ---")
print(f"old_list: {id(old_list)}")
print(f"deep_copied_list: {id(deep_copied_list)}")
print(f"old_list[2] (嵌套列表) 的内存地址: {id(old_list[2])}")
print(f"deep_copied_list[2] (嵌套列表) 的内存地址: {id(deep_copied_list[2])}")
print("\n--- 修改数据 ---")
# 修改嵌套列表
deep_copied_list[2].append('c')
print(f"修改后,old_list: {old_list}")
print(f"修改后,deep_copied_list: {deep_copied_list}")
输出:
--- 内存地址对比 ---
old_list: 4420195520
deep_copied_list: 4420195648
old_list[2] (嵌套列表) 的内存地址: 4420195456
deep_copied_list[2] (嵌套列表) 的内存地址: 4420195776
--- 修改数据 ---
修改后,old_list: [1, 2, ['a', 'b']]
修改后,deep_copied_list: [1, 2, ['a', 'b', 'c']]
结果分析:
这次,不仅顶层列表的地址不同,连嵌套列表的地址也不同了。deepcopy 为我们创建了一个全新的、独立的嵌套列表。
因此,修改 deep_copied_list 里的任何内容,都丝毫不会影响到 old_list。这才是真正的"克隆"。
四、总结:何时使用哪种拷贝?
| 操作方式 | 行为 | 何时使用 |
|---|---|---|
赋值 (=) | 创建一个指向相同对象的新别名 | 当你希望多个变量共享和操作同一个对象时。 |
浅拷贝 (copy.copy) | 创建新的顶层对象,但共享内部的子对象引用 | 当你的数据结构只有一层,或者你确定不会修改内部的嵌套可变对象时。它比深拷贝快。 |
深拷贝 (copy.deepcopy) | 递归地创建所有对象的全新副本 | 当你的数据结构包含嵌套的可变对象(如列表、字典),且你需要一个完全独立的副本时。这是最安全的选择,可以避免意外的副作用。 |
经验法则:
- 如果你的数据很简单,比如一个只包含数字和字符串的列表,用浅拷贝就足够了,而且效率更高。
- 如果你不确定数据的内部结构,或者数据中包含了列表、字典等可变容器,为了避免未来出现难以追踪的 Bug,请毫不犹豫地使用深拷贝。
掌握深浅拷贝的区别,能让你在处理复杂数据时更加得心应手,写出更可靠的 Python 代码.
五、进阶:拷贝的更多细节
1. 其他内置容器也需要注意
- 字典 / 集合:与列表类似,
copy.copy()只复制第一层键值或元素,嵌套结构依旧共享引用。 - 元组:元组不可变,但如果内部包含可变对象(如列表),浅拷贝仍会共享这些对象。
import copy
t1 = (1, [2, 3])
# 浅拷贝:返回同一元组对象(ID 不变)
shallow = copy.copy(t1)
# 深拷贝:创建新元组并递归复制内部可变对象
deep = copy.deepcopy(t1)
shallow[1].append(4) # 影响原元组
print(t1) # (1, [2, 3, 4]) ← 受浅拷贝修改影响
deep[1].append(5) # 不影响原元组
print(t1) # (1, [2, 3, 4])
print(deep) # (1, [2, 3, 5]) ← 深拷贝独立
2. list.copy()、切片与 copy.copy()
- 对列表、字典、集合而言,
.copy()方法或切片old_list[:]与copy.copy()完全等价,都是浅拷贝:
shallow1 = old_list[:] # 切片
shallow2 = old_list.copy() # 内置方法
shallow3 = copy.copy(old_list)
3. 为自定义类定制拷贝行为
如果你的类包含复杂状态,可实现 __copy__ / __deepcopy__ 钩子来自定义拷贝逻辑:
class User:
def __init__(self, name, settings):
# 初始化:name 为不可变字符串,settings 为可变字典
self.name = name
self.settings = settings
def __copy__(self):
# 浅拷贝:复制对象本身,但内部可变字段仍共享
cls = self.__class__ # 获取当前类引用
new_obj = cls.__new__(cls) # 仅分配内存,不调用 __init__
new_obj.__dict__.update(self.__dict__) # 复制所有属性引用
return new_obj
def __deepcopy__(self, memo):
# 深拷贝:递归复制所有可变子对象
import copy
cls = self.__class__
new_obj = cls.__new__(cls) # 创建空对象
memo[id(self)] = new_obj # 记录到 memo,处理循环引用
for k, v in self.__dict__.items(): # 遍历原对象属性
# 递归 deepcopy 属性值,并写入新对象
setattr(new_obj, k, copy.deepcopy(v, memo))
return new_obj
4. 深拷贝如何处理循环引用
copy.deepcopy() 使用内部的 memo 字典记录已复制对象,检测到循环引用时会返回已创建的副本,从而避免无限递归。
5. 不可复制的系统资源
文件句柄、线程锁、网络连接等系统资源无法真正复制,浅拷贝或深拷贝只会共享这些底层对象或抛出异常。如需复制,应在类的 __deepcopy__ 中自行处理,或者显式关闭并重新创建资源。
6. 性能与内存开销
深拷贝会递归创建新对象,消耗显著高于浅拷贝。在大型数据结构或高频调用场景中,建议:
- 评估是否真的需要深拷贝。
- 尝试"浅拷贝 + 手动复制关键子对象"以减少成本。
3709

被折叠的 条评论
为什么被折叠?



