第一章:一切从“赋值”引发的问题开始
在 python 的世界里,变量不仅仅是存储数据的标签,更像是指向内存中某个对象的“指针”。很多初学者,甚至是有一定经验的开发者,都曾在深浅拷贝的问题上栽过跟头。
想象一个场景:你有一个列表,里面包含了一些子列表,你想复制一份用来做修改,保留原数据。于是你顺手写了这样一行代码:
original_list = [[1, 2], [3, 4]]
new_list = original_list
# 现在,我想修改 new_list 的第一个子列表
new_list[0][0] = 999
print("original:", original_list)
print("new:", new_list)
如果你的预期是 original 保持 [[1, 2], [3, 4]],而 new 变为 [[999, 2], [3, 4]],那么很遗憾,现实会给你沉重一击。运行结果是:
original: [[999, 2], [3, 4]]
new: [[999, 2], [3, 4]]
为什么?
这就是 python 中最基础但也最容易被忽视的概念:赋值(assignment)并不是拷贝。
在上面的代码中,new_list = original_list 并没有创建一个新的列表对象,它仅仅是创建了一个新的引用(reference)。这就好比你有两个名字(original_list 和 new_list),但它们都指向同一个实体(内存中的列表对象)。因此,通过任何一个名字去修改这个实体,另一个名字看到的自然也是修改后的样子。
为了彻底解决这个问题,我们需要深入理解 python 内存模型中的三个层次:赋值、浅拷贝和深拷贝。
第二章:浅拷贝(shallow copy)——“只复制第一层”
当我们意识到直接赋值不是复制时,我们通常会转向浅拷贝。在 python 中,实现浅拷贝的方法有很多:
- 切片操作:
new_list = old_list[:] - 工厂函数:
new_list = list(old_list) copy模块:import copy; new_list = copy.copy(old_list)
让我们看看浅拷贝的表现:
import copy
original_list = [[1, 2], [3, 4]]
shallow_copied_list = copy.copy(original_list)
# 修改外层
shallow_copied_list.append([5, 6])
# 修改内层(嵌套对象)
shallow_copied_list[0][0] = 888
print("original:", original_list)
print("shallow:", shallow_copied_list)
输出结果:
original: [[888, 2], [3, 4]]
shallow: [[888, 2], [3, 4], [5, 6]]
分析:
- 外层修改:
shallow_copied_list增加了一个元素[5, 6],original_list没变。这说明最外层的容器确实是新创建的。 - 内层修改:
shallow_copied_list[0][0]被改为 888,original_list也跟着变了。这说明内层的子列表并没有被复制。
什么是浅拷贝?
浅拷贝(shallow copy)会创建一个新的容器对象,但不会递归地复制容器内的元素。新容器中填充的是原容器中元素的引用。
对于不可变对象(如整数、字符串、元组),引用就引用吧,反正改不了。但如果你的列表里包含了可变对象(如列表、字典、集合),那么这些可变对象的引用被共享了,这就是所谓的“共享子对象”(shared sub-objects)。
适用场景:浅拷贝适用于你的数据结构是“扁平”的,或者你明确知道你需要共享子对象(这很少见)。
第三章:深拷贝(deep copy)——“斩断所有羁绊”
如果你需要一个完全独立的副本,无论嵌套多少层,修改副本都不影响原件,那么你需要的是深拷贝。
深拷贝使用 copy.deepcopy() 实现:
import copy
original_list = [[1, 2], [3, 4]]
deep_copied_list = copy.deepcopy(original_list)
# 彻底修改副本
deep_copied_list[0][0] = 777
deep_copied_list.append([9, 0])
print("original:", original_list)
print("deep:", deep_copied_list)
输出结果:
original: [[1, 2], [3, 4]]
deep: [[777, 2], [3, 4], [9, 0]]
什么是深拷贝?
深拷贝会递归地遍历原对象的所有子对象,并创建它们的副本。这意味着新对象和原对象在内存中是完全独立的,没有任何引用重叠。
深拷贝的陷阱与高级用法:
虽然深拷贝很强大,但它也有代价(性能开销大)和陷阱。
递归引用导致死循环:
如果一个对象直接或间接引用了自己,deepcopy 会抛出 recursionerror。
a = [1] a.append(a) # a 现在是 [1, [...]] # b = copy.deepcopy(a) # 这会报错
自定义类的拷贝控制:如果你需要控制类的深拷贝行为,可以实现 __deepcopy__ 方法。这在处理数据库连接、文件句柄等不可序列化或不可拷贝的资源时非常有用。
性能考量:对于巨大的数据结构,深拷贝可能非常慢。如果你的嵌套层级很浅,或者全是不可变数据,深拷贝就是杀鸡用牛刀。
第四章:核心原理图解与常见误区
为了彻底理清关系,我们可以通过一张简化的内存示意图来理解:
假设 a = [1, [2, 3]]
赋值 (b = a):
a-> [ptr1, ptr2]b-> [ptr1, ptr2] (指向同一个地址)
浅拷贝 (b = copy.copy(a)):
a-> [ptr1, ptr2]b-> [ptr3, ptr4]- 其中
ptr1 == ptr3(指向同一个整数 1,整数不可变所以无所谓) - 但是
ptr2 == ptr4(指向同一个列表[2, 3]) -> 这是问题的根源
深拷贝 (b = copy.deepcopy(a)):
a-> [ptr1, ptr2]b-> [ptr5, ptr6]ptr5指向新的整数 1ptr6指向一个新的列表[2, 3],且该新列表内的元素也是新的。
常见误区:字典的copy()方法
很多 python 开发者会直接用字典自带的 .copy() 方法,认为这就是深拷贝。
错误! 字典的 .copy() 也是浅拷贝!
d1 = {'a': [1, 2]}
d2 = d1.copy()
d2['a'].append(3)
print(d1) # 输出 {'a': [1, 2, 3]},d1 被修改了!
正确的做法依然是 copy.deepcopy(d1) 或者使用 d1.copy() 配合字典推导式(如果只有一层的话)。
第五章:总结与最佳实践
搞懂了深浅拷贝,我们其实是在搞懂 python 的对象引用模型。这是编写健壮、无副作用代码的基石。
最后的建议:
- 默认使用引用:除非你明确需要副本,否则不要随意拷贝。
- 优先考虑浅拷贝:如果数据结构是扁平的,或者你确定不需要处理嵌套可变对象,
list[:]或copy.copy()更快。 - 不确定时用深拷贝:对于复杂的嵌套结构,为了数据的安全性,
copy.deepcopy()是最稳妥的选择。 - 警惕函数副作用:在函数中修改传入的可变参数时,一定要想清楚这是不是你想要的行为。如果不想修改原数据,记得在函数内部先拷贝。
以上就是一文带你掌握python中的深浅拷贝的详细内容,更多关于python深浅拷贝的资料请关注代码网其它相关文章!
发表评论