如果我想要一个非递归的对象深拷贝,我应该在Python中覆盖copy还是deepcopy?

7

我的类的一个对象具有列表作为其属性。也就是说,

class T(object):
    def __init__(self, x, y):
        self.arr = [x, y]

当这个对象被复制时,我希望有一个独立的列表arr,但是列表中的内容只进行浅拷贝(例如xy)。因此,我决定实现自己的复制方法,它将重新创建列表但不会重新创建其中的项目。但是我应该调用__copy__()还是__deepcopy__()?根据Python语义,哪一个是正确的名称?
我的猜测是__copy__()。如果我调用deepcopy(),我会期望克隆与原始对象完全解耦。然而,文档说:
“深拷贝构造一个新的复合对象,然后递归地将原始对象中的对象的副本插入到其中。”
它通过说“将副本插入其中”而不是“将深拷贝插入其中”而令人困惑,特别是强调部分。

“深度”的概念来自于递归部分,否则说“深拷贝构造一个新的复合对象,然后在其中插入深拷贝”将是一个递归描述。 - Tadhg McDonald-Jensen
3个回答

5
你需要实现的正确魔术方法是__copy__
你所描述的行为比默认的复制行为更深入(即对于没有实现__copy__的对象),但它还不足以被称为深复制。因此,你应该实现__copy__以获得期望的行为。
不要犯这样一个错误,认为简单地分配另一个名称就可以“复制”:
t1 = T('google.com', 123)
t2 = t1  # this does *not* use __copy__

这仅仅是将另一个名称绑定到同一实例上。相反,__copy__方法被一个函数挂钩:

import copy
t2 = copy.copy(t1)

谢谢!我也是这么想的,但当我阅读Python文档时感到有些怀疑。为什么Python不像strrepr一样将copy作为内置函数呢? - user31039
因为它的使用情况不多。Python开发人员通常采用避免副作用的编程风格,所以他们并不关心对象在内存中的位置。 - wim
@user31039 因为大多数内置函数都可以轻松复制。例如,实例可以传递给构造函数(dict({'a': 1})会创建一个副本),或者具有显式的 copy 方法:{1,2,3}.copy(),甚至可以使用切片语法来复制:[1,2,3][:]。对于普通的 Python 程序,使用内置函数时,您根本不需要复制模块。广告:我在我的答案中也涉及了其中一些要点。 :) - MSeifert

3
实际上,应该根据类的期望行为来决定重写哪个方法(__copy__还是__deepcopy__)。

一般情况下,copy.deepcopy可以正常工作,它会递归地复制所有内容,因此只有在有些属性不能被复制(永远不允许)时才需要重写。

另一方面,只有当用户(包括您自己)不希望更改传播到已复制的实例时,才应该定义__copy__。例如,如果您只是包装了可变类型(如list)或者将可变类型用作实现细节。

此外,有时不清楚要复制的最小属性集。在这种情况下,我也会重写__copy__,但可能会在其中引发一个TypeError,并可能包含一个或多个专用的公共copy方法。

然而,在我看来,arr算作实现细节,因此我会覆盖__copy__

class T(object):
    def __init__(self, x, y):
        self.arr = [x, y]

    def __copy__(self):
        new = self.__class__(*self.arr)
        # ... maybe other stuff
        return new

只是为了展示它按预期工作:

from copy import copy, deepcopy

x = T([2], [3])
y = copy(x)
x.arr is y.arr        # False
x.arr[0] is y.arr[0]  # True
x.arr[1] is y.arr[1]  # True

x = T([2], [3])
y = deepcopy(x)
x.arr is y.arr        # False
x.arr[0] is y.arr[0]  # False
x.arr[1] is y.arr[1]  # False

关于期望值的快速说明:

通常用户希望你能够通过将实例传递给构造函数来创建一个最小化的副本(与 __copy__ 类似或者完全相同)。例如:

lst1 = [1,2,3,4]
lst2 = list(lst1)
lst1 is lst2        # False

一些Python类型有一个显式的copy方法,如果存在,应该与__copy__相同。这将允许显式传递参数(但我还没有看到这种情况下的实际操作):

lst3 = lst1.copy()  # python 3.x only (probably)
lst3 is lst1        # False

如果您的类应该被其他人使用,那么您可能需要考虑以下几点。但是,如果您只想让您的类与 copy.copy 一起使用,那么只需重写 __copy__ 即可。

1
我认为覆盖 __copy__ 是一个好主意,正如其他答案详细解释的那样。另一种替代方案可能是编写一个显式的复制方法,以绝对明确调用了什么:
class T(object):
    def __init__(self, x, y):
        self.arr = [x, y]

    def copy(self):
        return T(*self.arr)

当任何未来的读者看到 t.copy() 时,他立即知道已经实现了自定义复制方法。缺点当然是它可能与使用 copy.copy(t) 的第三方库不兼容。

你对于“我应该覆盖copy还是deepcopy?”这个问题的回答是……两者都不需要,干脆就忘掉copy.copy吧? - Tadhg McDonald-Jensen
这只是一个替代性的想法。我认为它有一些优点,但我也完全意识到其负面影响。 - David Zwicker
2
好的,除非定义了 __copy__,否则当你执行 copy.copy(t) 时不会调用它。但我觉得你并没有真正给出问题的解决方案。(我猜想投票者可能是因为这个原因) - Tadhg McDonald-Jensen
好的 - 我会修改我的答案。然而,我认为这个问题的其他答案已经详细说明并且已经给出了覆盖__copy__的很好的理由。 - David Zwicker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接