社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

Python对象中的浅拷贝和深拷贝

Python程序员 • 7 年前 • 938 次点击  

Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。

Python中的赋值语句不会创建对象的副本,而只是给对象绑定了新的名称。对于不可变对象,这通常没什么区别。

但是在处理可变对象或可变对象集合时,你可能想找到一种方法来创建这些对象的“真正的副本”或“克隆”。

本质上来说就是有时候你会希望拷贝被修改时原对象不会自动修改。在这篇文章中,我将会告诉你如何在Python 3中拷贝或“克隆”对象,以及涉及的其他注意事项。

注意:本教程是用Python 3编写的,但涉及到拷贝对象时,Python 2和3之间几乎没有区别。当有差异时,我会在文中指出它们。

我们先看看如何拷贝Python的内置集合。通过在现有集合上调用其工厂函数即可拷贝Python的内置可变集合(如列表,字典和集合):

但是,此方法不适用于自定义对象,并且最重要的是,它仅创建浅拷贝。对于像列表,字典和集合这样的复合对象,浅拷贝和深拷贝之间有一个重要区别:

  • 浅拷贝会构建一个新的集合对象,然后用原对象的子对象的引用填充它。实质上,浅拷贝只有一层。拷贝过程不会递归,因此不会创建子对象本身的副本。

  • 深拷贝会递归拷贝过程。这意味着会首先构造一个新的集合对象,然后递归地填充原始对象中的子对象的副本。以这种方式拷贝对象会遍历整个对象树,从而创建原始对象及其所有子对象的完全独立的副本。

我知道这有点绕口,所以让我们通过一些例子来深入了解浅拷贝和深拷贝的区别。

创建浅拷贝

在下面的例子中,我们将创建一个新的嵌套列表,然后用工厂函数list()对它进行浅拷贝:

这意味着ys现在将是一个新的独立的对象,其内容与xs一致。你可以通过检查这两个对象来验证这一点:

为了确认ys真的是独立于原对象,我们来设计一个小实验。你可以尝试添加一个新的子列表到原始对象(xs),然后检查以确保此修改不会影响副本(ys):

正如你所看到的,这具有预期的效果。在“表面”级别修改列表副本完全没有问题。

但是,由于我们只创建了原始列表的浅拷贝,因此ys仍包含对存储在xs中的原始子对象的引用。

这些子对象没有被复制。他们只是在复制列表中再次引用。

因此,当你修改xs中一个子对象时,此修改也会在ys中反映出来——这是因为两个列表共享相同的子对象。这种拷贝只是一个浅层,一层的拷贝:

在上面的例子中,我们(似乎)只对xs做了一些改变。但事实证明,xs和ys索引为1的子列表都进行了修改。发生这种情况是因为我们只创建了原始列表的浅拷贝。

如果我们在第一步创建了xs的深拷贝,那么这两个对象将完全独立。这是对象的浅层和深层拷贝之间的实际区别。

现在你知道了如何创建一些内置集合类的浅拷贝,并且也知道了浅拷贝和深拷贝之间的区别。我们还想知道的是:

  • 如何创建内置集合的深拷贝?

  • 如何创建任意对象的(浅和深)拷贝,包括自定义类?

这些问题的答案是Python标准库中的copy模块。该模块提供了一个简单的接口来创建任意Python对象的深浅拷贝。

创建深拷贝

让我们重复前面的列表复制示例,但有一个重要的区别。这次我们将使用copy模块中定义的deepcopy()函数创建一个深拷贝:

当你检查xs和我们用copy.deepcopy()创建的副本zs时,你会发现它们看起来都一样——就像前面的例子一样:

但是,如果你对原始对象(xs)中的某个子对象进行了修改,则会看到此修改不会影响深拷贝(zs)。

这两个对象,原始对象和副本,这次是完全独立的。xs被递归拷贝,包括它的所有子对象:

你可能需要花一些时间利用Python解释器尝试正确地使用这些示例。当你亲身体验这些例子后,你对拷贝的理解会更容易。

顺便说一下,你还可以使用copy模块中的函数创建浅拷贝。copy.copy()函数会创建对象的浅拷贝。

如果你需要清楚地表明你正在代码中某处创建浅拷贝,这非常有用。使用copy.copy()可以让你指出这一事实。但是,对于内置集合,只需使用列表,字典和集合的工厂函数来创建浅拷贝,这更有python的风格。

复制任意Python对象

我们仍然需要回答的问题是如何创建任意对象的(深浅)拷贝,包括自定义类。现在我们来看看。

copy模块能再次帮我们。它的copy.copy()和copy.deepcopy()函数可用于复制任何对象。

同样再次,理解如何使用这些的最好方法是通过一个简单的实验。依然以之前的列表复制示例为例。我们首先定义一个简单的二维点类:

我希望你们认可这很简单。我添加了一个__repr__()实现,以便我们可以轻松地在Python解释器中检查由此类创建的对象。

注意:上面的例子使用Python 3.6的f-string来构造__repr__返回的字符串。在Python 2和Python 3之前的版本中,你需要使用不同的字符串格式表达式,例如:

接下来,我们将创建一个点实例,然后使用copy模块进行浅拷贝:

如果我们检查原始点对象和它的(浅)拷贝的内容,我们会看到正如预期的那样:

要记住,由于我们的点对象使用原始类型(int)作为其坐标,因此在这种情况下,浅拷贝和深拷贝之间没有区别。但我接下来会扩展这个例子。

我们来看一个更复杂的例子。我要定义另一个类来表示二维矩形。我将使对象的层次结构更复杂——我的矩形将使用点对象来表示它们的坐标:

再次,我们首先尝试创建一个矩形实例的浅拷贝:

如果你检查原始矩形及其副本,你会看到__repr__()重写进行良好,并且浅拷贝过程按预期工作:

还记得前面的列表示例如何说明深和浅拷贝之间的区别吗?我将在这里使用相同的方法。我将在对象层次结构中的更深层修改一个对象,然后你会看到(浅层)拷贝中反映的此更改:

我希望这和你预期的一致。接下来,我将创建原始矩形的深拷贝。然后我将进行另一个修改,你会看到哪些对象受到影响:

瞧!这次深拷贝(drect)完全独立于原始(rect)和浅拷贝(srect)。

我们已经在这里介绍了很多内容,但关于复制对象还有一些细节。

在这个话题上深入研究是值得的,因此你可能需要研究copy模块文档。例如,通过定义特殊函数__copy__()和__deepcopy__(),对象可以控制它们如何被拷贝。

记住三件事

  • 创建对象的浅拷贝不会克隆子对象。因此,副本不完全独立于原对象。

  • 对象的深层副本将递归地拷贝子对象。拷贝完全独立于原始文件,但创建深拷贝较慢。

  • 可以使用copy模块拷贝任意对象(包括自定义类)。



英文原文:https://realpython.com/blog/python/copying-python-objects/
译者:β



今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/Ycj0ObH5b7
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/10231
 
938 次点击