阅读本文需要5.5分钟
Python中的赋值语句没有创建副本对于对象来说,它们只是将名称绑定到对象。对于不可变的对象来说,通常是没有什么区别的。但是,为了处理可变对象或可变对象的集合,我们可能需要一种方法来创建这些对象的“真实副本“。
在本文中,将介绍如何在Python 3中复制或“克隆”对象,以及所涉及的一些注意事项。
注:本教程是用Python 3编写的,但是在复制对象时,Python 2和3并没有什么区别。当有不同时,会在文中指出。
让我们首先看看如何复制Python的内置集合。Python内置的集合是可变的,如列表、数据集和集合都可以通过在现有集合上调用它们的原来函数进行复制:
new_list = list(original_list) new_dict = dict(original_dict) new_set = set(original_set)
但是,此方法不适用于自定义对象,而且在此基础上,它只创建浅拷贝...对于复合对象,如列表、数据集和集合,有一个重要的区别:浅拷贝和深拷贝
- A 浅拷贝意味着构建一个新的集合对象,然后用对原始集合中的子对象引用填充它。本质上,一个浅拷贝只是一个层次的深度。复制过程不会递归,因此不会创建子对象本身的副本。
- A 深拷贝使复制过程递归。这意味着首先构造一个新的集合对象,然后使用递归在原始集合中找到的子对象的副本来填充它。以这种方式复制一个对象会遍历整个对象树,从而创建一个完全独立的原对象及其所有子对象的克隆。
让我们来看看一些例子来证明深拷贝和浅拷贝之间的区别。
浅拷贝
在下面的示例中,我们将创建一个新的嵌套列表,然后将其复制到list()
中:
>>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> ys = list(xs) # 开始浅拷贝
这意味着ys
将成为一个新的、独立的对象,我们可以通过检查这两个对象来验证这一点:
>>> xs [[1, 2, 3], [4, 5, 6], [7, 8, 9] >>> ys [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
确认ys
是独立对象,让我们设计一个小实验。可以尝试将一个新的子列表添加到原始(xs
),然后检查确保此修改不影响副本(ys
):
>>> xs.append(['new sublist']) >>> xs[[1, 2, 3], [4, 5, 6], [7, 8, 9], ['new sublist']] >>> ys [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
正如所看到的,达到了预期的效果。
但是,因为我们只创建了一个浅层的副本,ys
中存储的原始子对象引用xs
.
这些没有复制,只是在复制的列表中再次被引用。
因此,当在xs中
,此修改反映在ys
也是一样,那是因为两个列表共享相同的子对象。
>>> xs[1][0] = 'X' >>> xs[[1, 2, 3], ['X', 5, 6], [7, 8, 9], ['new sublist']] >>> ys [[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
如果我们创建了一个深拷贝xs,
在第一步中,这两个对象将完全独立。这是物体的浅拷贝和深拷贝之间的实际区别。
现在知道了如何创建一些内置集合类的浅拷贝,并且了解了浅拷贝和深拷贝之间的区别。但是我们仍然希望得到答案是:
- 如何创建内置集合的深拷贝?
- 如何创建任意对象(包括自定义类)的副本?
这些问题的答案在Python标准库中的copy
模块里。该模块为创建任意Python对象的浅拷贝和深拷贝提供了一个简单的接口。
深拷贝
让我们重复前面的列表复制示例,但有一个重要的区别。这次我们要使用deepcopy()这个方法
创建一个列表。
>>> import copy >>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> zs = copy.deepcopy(xs)
当检查xs
及其克隆zs
我们用了copy.deepcopy()
,将看到它们看起来是相同的--就像前面的示例:
>>> xs [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> zs [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
但是,如果对原始对象中的一个子对象进行了修改(xs
),将看到此修改不会影响深层副本(zs
).
这一次,两个对象,原件和副本都是完全独立的。xs
是递归地克隆的,包括它的所有子对象:
>>> xs[1][0] = 'X' >>> xs [[1, 2, 3], ['X', 5, 6], [7, 8, 9]] >>> zs [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
当你亲身体验这些例子的时候,理解起来就更容易了。
顺便说一句,还可以使用copy
模块,copy.copy()
函数创建对象的浅副本。
如果需要清楚地表达你正在代码中的某个地方创建一个浅表副本,这个方法非常有用的。
复制任意Python对象
现在我们需要回答的问题是如何创建任意对象(包括自定义类)的副本(浅的和深的),现在让我们看看这个。copy.copy()
和copy.deepcopy()
函数可用于复制任何对象。我将基于前面的列表举个简单的例子。首先让我们定义一个简单的类:
class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f'Point({self.x!r}, {self.y!r})'
我加了一个__repr__()
实现,这样我们就可以在Python解释器中轻松地检查从该类中创建的对象。
注:上面的示例使用Python3.6f-string构造由
__repr__
...在Python 2和3.6之前的Python 3版本中,将使用不同的字符串格式表达式,例如:
def__repr__(self):
return'Point(%r, %r)'%(self.x,self.y)
接下来,我们将创建一个Point
实例,使用copy
模块浅复制它:
>>> a = Point(23, 42) >>> b = copy.copy(a)
如果我们检查原件的内容Point
对象及其克隆,看到了我们所期望的:
>>> a Point(23, 42) >>> b Point(23, 42) >>> a is b False
我们来看一个更复杂的例子。定义另一个类来表示二维矩形:
class Rectangle: def __init__(self, topleft, bottomright): self.topleft = topleft self.bottomright = bottomright def __repr__(self): return (f'Rectangle({self.topleft!r}, ' f'{self.bottomright!r})')
同样,首先我们将尝试创建矩形实例的浅表副本:
rect = Rectangle(Point(0, 1), Point(5, 6)) srect = copy.copy(rect)
如果检查原始矩形及其副本,将看到__repr__()正在进行
覆盖,浅层复制过程:
>>> rect Rectangle(Point(0, 1), Point(5, 6)) >>> srect Rectangle(Point(0, 1), Point(5, 6)) >>> rect is srect False
还记得前面的列表示例是如何说明深拷贝和浅拷贝之间的区别的吗?我要用同样的方法,在对象层次结构中更深地修改一个对象,然后将在(浅)副本中更改:
>>> rect.topleft.x = 999 >>> rect Rectangle(Point(999, 1), Point(5, 6)) >>> srect Rectangle(Point(999, 1), Point(5, 6))
接下来,我将创建原始矩形的深拷贝。然后,我修改一下:
>>> drect = copy.deepcopy(srect) >>> drect.topleft.x = 222 >>> drect Rectangle(Point(222, 1), Point(5, 6)) >>> rect Rectangle(Point(999, 1), Point(5, 6)) >>> srect Rectangle(Point(999, 1), Point(5, 6))
这一次,深拷贝(drect
)完全独立于原始(rect
)和浅拷贝(srect
).
总结
- 创建对象的浅拷贝不会复制子对象。因此,副本并不完全独立于原件。
- 对象的深拷贝将递归地复制子对象。克隆完全独立于原始副本,但是创建深拷贝要慢一些。
- 类复制任意对象(包括自定义类)。