一、引言
在 Python 编程中,我们经常需要复制对象。但简单的赋值操作往往不能满足我们的需求,因为它只是创建了一个新的引用,而不是真正的对象副本。这时,我们就需要使用浅拷贝 和深拷贝来创建对象的独立副本。
所谓浅拷贝指的是:copy模块的copy(),深拷贝指的是copy模块的deepcopy()模块
深浅拷贝可以按照以下类型进行分类:
- 浅拷贝可变类型
- 浅拷贝不可变类型
- 深拷贝可变类型
- 深拷贝不可变类型
关于可变类和不可变类型的辨析:
在 python 中,strings, tuples, 和 numbers 是不可更改的对象,而 list,dict 等则是可以修改的对象。
- 不可变类型: 变量赋值 a=5 后再赋值 a=10,这里实际是新生成一个 int 值对象 10,再让 a 指向它,而 5 被丢弃,不是改变 a 的值,相当于新生成了 a。
- 可变类型: 变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改,本身la没有动,只是其内部的一部分值被修改了。
python 函数的参数传递:
- **不可变类型:**类似 C++ 的值传递,如整数、字符串、元组。如 fun(a),传递的只是 a 的值,没有影响 a 对象本身。如果在 fun(a) 内部修改 a 的值,则是新生成一个 a 的对象。
- **可变类型:**类似 C++ 的引用传递,如 列表,字典。如 fun(la),则是将 la 真正的传过去,修改后 fun 外部的 la 也会受影响
python 中一切都是对象,严格意义我们不能说值传递还是引用传递,我们应该说传不可变对象和传可变对象。
深浅拷贝主要是针对可变类型来讲的,如果是针对于不可变类型, 则用法和普通赋值一样.
二、赋值操作(Assignment)
在讲解深浅拷贝之前,我们首先需要理解 Python 中的赋值操作,因为它是最基础的对象"复制"方式,但往往也是最容易产生误解的。
2.1 定义
赋值操作(=)只是创建了一个新的变量名,该变量名与原变量名指向同一个内存地址中的对象。换句话说,赋值操作并没有创建新的对象,只是增加了一个引用。
2.2 代码示例
# 定义一个列表对象
a = [1, 2, [3, 4]]
# 赋值操作
b = a
print("a 的内存地址:", id(a))
print("b 的内存地址:", id(b)) # 与 a 相同
print("a == b:", a == b) # True,值相同
print("a is b:", a is b) # True,是同一个对象
# 修改 a 中的元素
a[0] = 100
# 修改 a 中嵌套的列表
a[2][0] = 300
print("修改后的 a:", a) # [100, 2, [300, 4]]
print("修改后的 b:", b) # [100, 2, [300, 4]],b 也跟着变了
2.3 注意事项
- 赋值操作后,两个变量共享同一个对象。
- 对其中一个变量所指向的对象进行修改(无论是修改顶层元素还是嵌套元素),都会影响到另一个变量。
三、浅拷贝(Shallow Copy)
浅拷贝是一种创建新对象的方式,但它只复制对象的"顶层"结构。对于对象中的嵌套元素(如子列表、子字典等),浅拷贝只复制它们的引用,而不复制嵌套对象本身。
3.1 定义
浅拷贝创建一个新的容器对象(如一个新的列表),但容器里面的元素仍然是原对象中元素的引用。
3.2 常见实现方式
在 Python 中,实现浅拷贝的常见方式有:
- 使用
copy模块的copy()函数。 - 使用列表的切片操作
list[:]。 - 使用列表、字典等内置数据结构的
copy()方法。 - 使用工厂函数,如
list()或dict()。
3.3 代码示例
import copy
# 定义一个包含嵌套列表的列表
a = [1, 2, [3, 4]]
# 方式 1: 使用 copy.copy()
b = copy.copy(a)
# 方式 2: 使用切片
# b = a[:]
# 方式 3: 使用 list.copy()
# b = a.copy()
# 方式 4: 使用 list() 工厂函数
# b = list(a)
print("a 的内存地址:", id(a))
print("b 的内存地址:", id(b)) # 与 a 不同,说明 b 是新对象
print("a == b:", a == b) # True,值相同
print("a is b:", a is b) # False,不是同一个对象
print("a[2] 的内存地址:", id(a[2]))
print("b[2] 的内存地址:", id(b[2])) # 与 a[2] 相同!说明嵌套列表没有被复制
# 修改 a 的顶层元素(不可变元素)
a[0] = 100
print("修改 a[0] 后的 a:", a) # [100, 2, [3, 4]]
print("修改 a[0] 后的 b:", b) # [1, 2, [3, 4]],b 不受影响
# 修改 a 中的嵌套列表(可变元素)
a[2][0] = 300
print("修改 a[2][0] 后的 a:", a) # [100, 2, [300, 4]]
print("修改 a[2][0] 后的 b:", b) # [1, 2, [300, 4]],b 也跟着变了!
3.4 工作原理
- 创建一个新的容器对象(如列表
b)。 - 将原对象(列表
a)中的元素引用复制到新容器中。 - 如果原对象中的元素是不可变对象(如数字、字符串、元组),修改原对象的顶层元素不会影响浅拷贝对象。
- 如果原对象中的元素是可变对象(如列表、字典),因为复制的是引用,所以修改原对象中的嵌套可变对象,浅拷贝对象中的对应元素也会改变。
四、深拷贝(Deep Copy)
深拷贝是一种完全复制对象的方式。它不仅复制对象的顶层结构,还会递归地复制对象中所有的嵌套元素。
4.1 定义
深拷贝创建一个新的容器对象,并且递归地将原对象中的所有元素(包括嵌套再深的对象)都复制一份。原对象和拷贝对象之间完全独立,互不影响。
4.2 常见实现方式
在 Python 中,实现深拷贝主要使用 copy 模块的 deepcopy() 函数。
4.3 代码示例
import copy
# 定义一个包含嵌套列表的列表
a = [1, 2, [3, 4]]
# 使用 copy.deepcopy() 进行深拷贝
b = copy.deepcopy(a)
print("a 的内存地址:", id(a))
print("b 的内存地址:", id(b)) # 与 a 不同
print("a[2] 的内存地址:", id(a[2]))
print("b[2] 的内存地址:", id(b[2])) # 与 a[2] 也不同!嵌套列表也被复制了
# 修改 a 的顶层元素
a[0] = 100
print("修改 a[0] 后的 a:", a) # [100, 2, [3, 4]]
print("修改 a[0] 后的 b:", b) # [1, 2, [3, 4]],b 不受影响
# 修改 a 中的嵌套列表
a[2][0] = 300
print("修改 a[2][0] 后的 a:", a) # [100, 2, [300, 4]]
print("修改 a[2][0] 后的 b:", b) # [1, 2, [3, 4]],b 仍然不受影响!
4.4 工作原理
- 创建一个新的容器对象。
- 递归地遍历原对象中的每一个元素。
- 如果是不可变对象,则复制其引用(或直接复用,因为不可变)。
- 如果是可变对象,则在内存中创建一个新的该对象,并将新对象的引用放入新容器中。
- 结果是,原对象和深拷贝对象在内存中是完全独立的两棵树。
五、赋值、浅拷贝、深拷贝区别对比
|----------------|--------------|---------------------|-------------------------|
| 特性 | 赋值 (=) | 浅拷贝 (copy.copy()) | 深拷贝 (copy.deepcopy()) |
| 新对象创建 | 不创建新对象,只增加引用 | 创建新对象(仅顶层容器) | 创建新对象(完全独立) |
| 非容器元素(如数字) | 指向同一对象 | 复制引用(效果类似赋值) | 复制引用(效果类似赋值) |
| 容器元素(如子列表) | 指向同一对象 | 复制引用(共享子对象) | 递归复制(独立子对象) |
| 内存开销 | 最小 | 较小 | 较大(取决于对象复杂度) |
| 修改原对象顶层元素 | 影响对方 | 不影响对方 | 不影响对方 |
| 修改原对象嵌套元素 | 影响对方 | 影响对方 | 不影响对方 |
六、使用场景建议
- 赋值操作 (
=):
-
- 当你只是想给同一个对象起个别名,或者不需要修改对象时使用。
- 例如:
user = current_user(只是为了代码可读性)。
- 浅拷贝 (
copy.copy()):
-
- 当你想复制一个容器,但容器内的元素都是不可变对象(如数字、字符串、元组)时使用。
- 当你想复制一个容器,且你确定不会修改其中的嵌套可变对象时使用。
- 例如:复制一个简单的配置列表
config_copy = config[:]。
- 深拷贝 (
copy.deepcopy()):
-
- 当你需要一个完全独立的副本,且对象中包含嵌套的可变对象时,必须使用深拷贝。
- 例如:在处理复杂的数据结构(如图、树、JSON数据)时,为了防止修改副本影响原始数据。
七、常见陷阱与注意事项
7.1 不可变对象的"特殊性"
对于不可变对象(如 tuple、str、int),浅拷贝有时并不会真正创建新对象,因为 Python 为了优化性能,会复用不可变对象。但这通常不会影响程序逻辑,因为对象本身不可修改。
import copy
a = (1, 2, 3)
b = copy.copy(a)
print(a is b) # 可能是 True,因为元组不可变,Python 优化了
7.2 循环引用
deepcopy 会智能地处理对象中的循环引用(即对象 A 引用 B,B 又引用 A),防止无限递归。
import copy
a = [1, 2]
a.append(a) # 自己引用自己,形成循环
print(a) # [1, 2, [...]]
b = copy.deepcopy(a)
print(b) # [1, 2, [...]],深拷贝成功,不会报错
7.3 自定义对象
对于自定义的类实例,深浅拷贝的行为同样适用。如果需要控制自定义对象的拷贝行为,可以重写 __copy__() 和 __deepcopy__() 方法。
八、总结
- 赋值:不是拷贝,只是引用计数加一。
- 浅拷贝:"表皮"复制,容器是新的,但里面的东西还是旧的引用。
- 深拷贝:"彻底"复制,容器和里面的所有东西都是新的。
理解了这三者的区别,你就能在处理复杂数据结构时避免很多难以察觉的 Bug。记住口诀:"赋值靠引用,浅拷贝只一层,深拷贝全递归"。