Python中的浅拷贝与深拷贝
Python中的浅拷贝与深拷贝:你真的搞懂了吗?
在Python编程中,拷贝操作无处不在,尤其是在处理列表、字典等可变对象时。你是否曾因为直接赋值导致修改一个变量影响到另一个变量而困惑?又或者在使用list.copy()、切片[:]时,发现有时拷贝后的对象会跟着原对象变化,有时又不会?这一切的根源就在于浅拷贝与深拷贝的区别。今天,我们就来彻底搞懂这两个概念,并通过丰富的例子让你不再疑惑。
1. 什么是拷贝?
在Python中,赋值操作b = a并不会创建新对象,而是让b和a指向同一个对象。因此,无论修改a还是b,都会影响到另一方(如果是可变对象)。而拷贝则是创建一个新的对象,内容与原对象相同。根据拷贝的“深度”,分为浅拷贝和深拷贝。
2. 浅拷贝(Shallow Copy)
浅拷贝会创建一个新对象,但新对象中的元素仍然是对原对象中元素的引用。也就是说,如果原对象中的元素是可变对象(如列表、字典),那么新对象和原对象会共享这些可变子对象。
实现方式
- 切片操作:
new_list = old_list[:] - 工厂函数:
list()、dict()等 copy模块的copy()方法:import copy; new_obj = copy.copy(old_obj)- 对象自身的
copy()方法:如list.copy()
浅拷贝的特点
- 外层对象是新创建的,内层对象(如果可变)是共享的。
- 修改外层对象(如增删元素)不会互相影响。
- 修改共享的内层对象会互相影响。
示例1:浅拷贝的基本行为
import copy
original = [1, 2, [3, 4]]
shallow = copy.copy(original) # 浅拷贝
# 也可以用 original[:] 或 list(original)
print("original:", original) # [1, 2, [3, 4]]
print("shallow:", shallow) # [1, 2, [3, 4]]
# 修改外层元素(不可变对象替换)
original[0] = 100
print("修改原列表外层后:")
print("original:", original) # [100, 2, [3, 4]]
print("shallow:", shallow) # [1, 2, [3, 4]] 不影响
# 修改内层可变对象
original[2].append(5)
print("修改内层列表后:")
print("original:", original) # [100, 2, [3, 4, 5]]
print("shallow:", shallow) # [1, 2, [3, 4, 5]] 受影响!
解释:shallow是浅拷贝,它的第一个元素(整数1)和原列表的第一个元素(整数100)不同,因为整数是不可变的,替换元素实际上是让原列表的索引0指向新的整数对象,而浅拷贝的索引0仍指向原来的1。但内层列表[3,4]是可变对象,原列表和浅拷贝都引用同一个列表,因此通过原列表修改它,浅拷贝也能看到变化。
3. 深拷贝(Deep Copy)
深拷贝会递归地创建一个新对象,原对象中的所有元素都会被复制一份,包括嵌套的可变对象。新对象与原对象完全独立,任何修改都不会影响对方。
实现方式
copy模块的deepcopy()方法:import copy; new_obj = copy.deepcopy(old_obj)
深拷贝的特点
- 完全复制所有层级,新对象与原对象没有任何共享部分。
- 修改原对象或新对象中的任何元素(包括嵌套对象)都不会影响对方。
示例2:深拷贝的独立性
import copy
original = [1, 2, [3, 4]]
deep = copy.deepcopy(original)
print("original:", original) # [1, 2, [3, 4]]
print("deep:", deep) # [1, 2, [3, 4]]
# 修改外层
original[0] = 100
print("修改原列表外层后:")
print("original:", original) # [100, 2, [3, 4]]
print("deep:", deep) # [1, 2, [3, 4]] 不影响
# 修改内层
original[2].append(5)
print("修改内层列表后:")
print("original:", original) # [100, 2, [3, 4, 5]]
print("deep:", deep) # [1, 2, [3, 4]] 不影响!
解释:深拷贝创建了全新的列表,内层的[3,4]也被复制成了一个新列表。因此,无论修改原列表的外层还是内层,深拷贝对象都纹丝不动。
4. 浅拷贝 vs 深拷贝:直观对比
| 比较项 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 是否创建新对象 | 是 | 是 |
| 内层可变对象是否共享 | 共享(引用) | 不共享(递归复制) |
| 修改外层对拷贝的影响 | 无影响 | 无影响 |
| 修改内层可变对象对拷贝的影响 | 有影响 | 无影响 |
| 适用场景 | 对象无嵌套,或嵌套对象不需独立 | 需要完全独立的副本,避免任何副作用 |
| 性能 | 快(只需复制一层) | 慢(递归复制所有层) |
5. 常见陷阱与注意事项
陷阱1:以为浅拷贝就是完全复制
很多人误以为[:]或list.copy()创建的就是独立副本,从而在嵌套列表上出错。
matrix = [[1, 2], [3, 4]]
copy_matrix = matrix[:]
copy_matrix[0][0] = 99
print(matrix) # [[99, 2], [3, 4]] 原矩阵也被改了!
正确做法:使用深拷贝 copy.deepcopy(matrix)。
陷阱2:在自定义类中处理拷贝
自定义类如果需要支持拷贝,可以定义__copy__()和__deepcopy__()方法。
陷阱3:深拷贝的循环引用问题
deepcopy可以优雅地处理循环引用,不会陷入无限递归。
a = [1, 2]
b = [a, 3] # b 中包含 a
a.append(b) # a 中包含 b,形成循环
c = copy.deepcopy(a) # 正常工作,不会死循环
6. 回溯算法中的经典应用:为什么用path[:]?
在回溯算法(如子集、排列)中,我们经常看到这样的代码:
def subsets(nums):
res = []
path = []
def backtrack(start):
res.append(path[:]) # 为什么用 path[:] 而不是 path?
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i+1)
path.pop()
backtrack(0)
return res
这里的path[:]就是浅拷贝。因为path中只包含整数(不可变对象),所以浅拷贝已经足够保存当前状态的快照。后续path的pop操作不会影响已经保存的副本。如果path中包含可变对象,则需要根据具体情况决定是否使用深拷贝,但一般回溯中不会出现这种情况。
7. 总结
- 浅拷贝:新对象,但内层可变对象共享。适用于对象结构简单、无需隔离内层修改的场景。
- 深拷贝:完全独立的新对象,递归复制所有层。适用于需要完全隔离的场景,但代价较高。
- 选择哪种拷贝,取决于你是否希望原对象和副本中的嵌套对象相互影响。
希望这篇文章能帮你理清浅拷贝与深拷贝的来龙去脉。下次遇到拷贝问题,先问问自己:“我需要完全独立的副本吗?”答案就呼之欲出了。
Happy coding!

浙公网安备 33010602011771号