1. 前言
在 Python 中,拷贝是经常使用的操作,也包含了深拷贝和浅拷贝两种方式。正确使用拷贝方式不仅能够提高程序效率,还能避免出现不必要的错误。本文将分别介绍 Python 中深拷贝和浅拷贝的概念、区别以及使用场景。
2. 深拷贝
深拷贝是指在另一块内存中创建一个完全相同的对象,这两个对象拥有不同的 id,但是它们的所有元素都是完全相同的。对于可变对象而言,当拷贝对象进行修改时,不会影响被拷贝的对象。这种拷贝方式在处理嵌套的数据结构(如列表中嵌套着字典等)时非常有用。
2.1 使用 copy 模块实现深拷贝
Python 中有一种针对可变对象的深拷贝实现方式,可以直接使用标准库 copy
中的 deepcopy
方法实现深拷贝。
import copy
a = [1, 2, [3, 4]]
b = copy.deepcopy(a)
a == b # True
a is b # False
上述代码中,通过 deepcopy
方法从 a 中完全拷贝了一份 b,包括了其嵌套的列表。两个对象的 id
不同,但是元素完全相同。
2.2 递归实现深拷贝
深拷贝的本质是递归的复制内部的可变对象。因此,我们可以使用递归的方式,手动实现深拷贝的过程。
def deepcopy(obj, memo=None):
if memo is None:
memo = {}
if id(obj) in memo:
return memo[id(obj)]
dup = copy.copy(obj)
if hasattr(obj, "__dict__"):
dup.__dict__ = {deepcopy(k, memo): deepcopy(v, memo)
for k, v in obj.__dict__.items()}
if hasattr(obj, "__iter__"):
memo[id(obj)] = dup
if isinstance(obj, dict):
dup = {deepcopy(k, memo): deepcopy(v, memo)
for k, v in obj.items()}
else:
dup = [deepcopy(item, memo) for item in obj]
return dup
上述代码中,我们使用一个 memo 字典变量来保存已经拷贝过的对象,以避免产生递归循环。在拷贝过程中,我们首先使用 copy.copy 方法创建一个新的对象 dup,然后分别对其进行处理。若对象拥有 __dict__ 属性,说明其是一个类的实例对象,我们将其 __dict__ 字典中的元素递归拷贝;若对象可迭代,我们递归遍历其中的所有元素,并且在 memo 中记录该对象的 id。
2.3 对象复制与创造新对象的区别
需要注意的是,深拷贝与创建新对象是不同的。一个对象的实例不仅有数据,还有所有的内置方法。在一个新的对象上,所有的内置方法都需要重新构建,这个过程带来了很大的开销。而在深拷贝过程中,我们只是在另一个内存单元中创建了一份数据,内置方法是这个数据的共享部分。
class MyClass(object):
x = 10
a = MyClass()
b = copy.deepcopy(a)
a.x = 20
print(b.x) # 输出 10
上述代码中,我们创建了一个 MyClass 的对象 a,然后通过深拷贝创建了另一个对象 b。当我们修改 a.x 的值时,对 b 的值没有任何影响,这是因为 MyClass 的内置方法是共享的。
3. 浅拷贝
浅拷贝是指创建一个新的对象,然后将原始对象中的元素逐一复制到新对象中,对于容器类型的对象,只会将每个元素的引用复制一份。因此,当对象中包含了可变类型的元素(如列表或字典等)时,修改其中一个对象会影响到另一个对象,这事实上是对于原对象中的元素进行的修改,而不是对于拷贝对象进行的修改。
3.1 拷贝不可变对象
当原始对象是一个不可变的对象时,浅拷贝也会完全复制其数据。
import copy
a = (1, 2, 3, [4, 5])
b = copy.copy(a)
a == b # True
a is b # False
a[-1].append(6)
print(b) # 输出 (1, 2, 3, [4, 5, 6])
上述代码中,我们浅拷贝了一个元组类型对象 a,并且在拷贝对象中添加了元素。由于元组是不可变类型,因此对于拷贝对象的修改并没有对原始对象造成影响。浅拷贝只是在拷贝对象中创建了一个新的列表引用,指向了原始对象中相同的列表。
3.2 拷贝可变对象
浅拷贝拷贝的是每个元素的引用,容器中所存储的是被引用的实际元素,因此它们在内存中的地址还是和原来的对象相同。
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
a == b # True
a is b # False
a[-1].append(5)
print(b) # 输出 [1, 2, [3, 4, 5]]
上述代码中,我们浅拷贝了一个列表类型对象 a,并且在拷贝对象中添加了元素。由于列表是可变类型,因此对于拷贝对象的修改也会对原始对象造成影响。浅拷贝只是创建了一个新的引用,这个引用指向了原始对象中嵌套的列表对象。
3.3 修改拷贝对象时的注意事项
由于浅拷贝对象中的元素和原始对象中的元素是共享的,因此进行修改时需要特别小心。当容器内包含着可变类型的元素时,对拷贝对象中这个元素的修改会影响原始对象中的值。
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
a[2].append(5)
print(b) # 输出 [1, 2, [3, 4, 5]]
上述代码中,我们先对原始列表对象 a 中的嵌套列表添加了一个元素,然后将其浅拷贝并保存到列表 b 中。由于拷贝对象中的嵌套列表仍然和原始列表中的对象指向相同的内存空间,因此对拷贝对象中的嵌套列表进行修改会影响到原始列表中的值。
4. 深拷贝和浅拷贝的使用场景
在 Python 中,一般情况下推荐使用深拷贝操作。在需要拷贝一个大型的数据结构或者嵌套结构时,使用深拷贝能够保证我们的程序的正确性,并且也能够避免不必要的错误。
浅拷贝通常用于小型数据结构的拷贝。虽然浅拷贝操作开销比深拷贝小,但是其对于嵌套结构的数据拷贝并不完全正确,可能会出现一些不必要的错误。因此,在使用浅拷贝操作的时候,需要特别注意。
5. 结论
本文中,我们分别介绍了 Python 中深拷贝和浅拷贝的概念,讲解了深拷贝和浅拷贝的区别,并且给出了相应的代码实现方式。我们也简单介绍了深拷贝和浅拷贝在使用场景上的建议使用方式。在实际的开发过程中,合理地使用拷贝操作能够提高程序的效率,并且保证程序的正确性。