如何覆盖 Python 对象的复制/深度复制操作?

Posted

技术标签:

【中文标题】如何覆盖 Python 对象的复制/深度复制操作?【英文标题】:How to override the copy/deepcopy operations for a Python object? 【发布时间】:2010-12-02 19:27:50 【问题描述】:

我了解复制模块中copydeepcopy 之间的区别。我之前成功地使用过copy.copycopy.deepcopy,但这是我第一次真正开始重载__copy____deepcopy__ 方法。我已经用谷歌搜索并查看了内置的 Python 模块以寻找 __copy____deepcopy__ 函数的实例(例如 sets.pydecimal.pyfractions.py),但我仍然不是 100% 确定我做对了。

这是我的场景:

我有一个配置对象。最初,我将使用一组默认值实例化一个配置对象。此配置将移交给多个其他对象(以确保所有对象都以相同的配置开始)。但是,一旦用户交互开始,每个对象都需要独立调整其配置,而不会影响彼此的配置(这对我来说,我需要对我的初始配置进行深度复制以进行处理)。

这是一个示例对象:

class ChartConfig(object):

    def __init__(self):

        #Drawing properties (Booleans/strings)
        self.antialiased = None
        self.plot_style = None
        self.plot_title = None
        self.autoscale = None

        #X axis properties (strings/ints)
        self.xaxis_title = None
        self.xaxis_tick_rotation = None
        self.xaxis_tick_align = None

        #Y axis properties (strings/ints)
        self.yaxis_title = None
        self.yaxis_tick_rotation = None
        self.yaxis_tick_align = None

        #A list of non-primitive objects
        self.trace_configs = []

    def __copy__(self):
        pass

    def __deepcopy__(self, memo):
        pass 

在此对象上实现copydeepcopy 方法以确保copy.copycopy.deepcopy 给我正确的行为的正确方法是什么?

【问题讨论】:

有效吗?有问题吗? 我认为共享引用仍然存在问题,但我完全有可能在其他地方搞砸了。当我有机会时,我会根据@MortenSiebuhr 的帖子仔细检查并更新结果。 根据我目前有限的理解,我希望 copy.deepcopy(ChartConfigInstance) 返回一个与原始实例没有任何共享引用的新实例(无需自己重新实现 deepcopy)。这是不正确的吗? 【参考方案1】:

将 Alex Martelli 的回答和 Rob Young 的评论放在一起,您会得到以下代码:

from copy import copy, deepcopy

class A(object):
    def __init__(self):
        print 'init'
        self.v = 10
        self.z = [2,3,4]

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        return result

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, deepcopy(v, memo))
        return result

a = A()
a.v = 11
b1, b2 = copy(a), deepcopy(a)
a.v = 12
a.z.append(5)
print b1.v, b1.z
print b2.v, b2.z

打印

init
11 [2, 3, 4, 5]
11 [2, 3, 4]

这里__deepcopy__ 填写memo dict 以避免过度复制,以防对象本身被其成员引用。

【讨论】:

@bytestorm Transporter 是什么? @AntonyHatchkins Transporter 是我正在写的班级的名称。对于那个类,我想覆盖 deepcopy 行为。 @bytestorm Transporter的内容是什么? 我认为__deepcopy__ 应该包含一个测试以避免无限递归: d = id(self) result = memo.get(d, None)如果结果不是无:返回结果 @AntonyHatchkins 从您的帖子在哪里 memo[id(self)] 实际用于防止无限递归并不清楚。我已经整理了一个short example,这表明如果id()memo 的键,则copy.deepcopy() 在内部中止对对象的调用,对吗?还值得注意的是,deepcopy() 似乎是自己默认执行此操作的,这使得很难想象实际上需要手动定义__deepcopy__ 的情况...【参考方案2】:

定制建议在docs page的最后:

类可以使用相同的接口 控制他们使用的复制 控制酸洗。见说明 模块泡菜的信息 这些方法。复制模块 不使用 copy_reg 注册 模块。

为了让一个类定义自己的 复制实现,它可以定义 特殊方法__copy__()__deepcopy__()。调用前者实现浅拷贝 手术;没有额外的论点 通过了。后者被称为 实现深拷贝操作;它 传递一个参数,备忘录 字典。如果__deepcopy__() 实施需要深入 组件的副本,它应该调用 deepcopy() 函数与 组件作为第一个参数和 备忘录字典作为第二个参数。

由于您似乎不关心酸洗自定义,因此定义 __copy____deepcopy__ 绝对是适合您的正确方法。

具体来说,__copy__(浅拷贝)在您的情况下非常简单......:

def __copy__(self):
  newone = type(self)()
  newone.__dict__.update(self.__dict__)
  return newone

__deepcopy__ 将类似(也接受 memo 参数),但在返回之前,它必须为任何需要深度复制的属性 self.foo 调用 self.foo = deepcopy(self.foo, memo)(本质上是容器属性 - 列表, dicts,通过__dict__s 保存其他东西的非原始对象。

【讨论】:

@kaizer,他们可以自定义酸洗/取消酸洗以及复制,但如果你不关心酸洗,使用__copy__/__deepcopy__ 更简单直接. 这似乎不是复制/深复制的直接翻译。 copy 和 deepcopy 都不会调用被复制对象的构造函数。考虑这个例子。类 Test1(object): def init__(self): print "%s.%s" % (self.__class.__name__, "init") 类 Test2(Test1 ): def __copy__(self): new = type(self)() return new t1 = Test1() copy.copy(t1) t2 = Test2() copy.copy(t2) 我认为你应该使用 cls = self.__class__; 而不是 type(self)() cls.__new__(cls) 对构造函数接口不敏感(特别是对于子类化)。然而,这并不重要。 为什么是self.foo = deepcopy(self.foo, memo)...?你不是说newone.foo = ...吗? @Juh_ 的评论很到位。你不想打电话给__init__。这不是副本的作用。也经常有酸洗和复制需要不同的用例。事实上,我什至不知道为什么 copy 默认会尝试使用酸洗协议。复制用于内存操作,酸洗用于跨时代持久化;它们是完全不同的东西,彼此之间几乎没有关系。【参考方案3】:

按照Peter's excellent answer,实现自定义深度复制,对默认实现的改动最小(例如,只修改我需要的字段):

class Foo(object):
    def __deepcopy__(self, memo):
        deepcopy_method = self.__deepcopy__
        self.__deepcopy__ = None
        cp = deepcopy(self, memo)
        self.__deepcopy__ = deepcopy_method
        cp.__deepcopy__ = deepcopy_method

        # custom treatments
        # for instance: cp.id = None

        return cp

【讨论】:

这是优先使用delattr(self, '__deepcopy__')然后setattr(self, '__deepcopy__', deepcopy_method)吗? 这是我个人的最爱,我在生产环境中使用它,其中一个对象有一个记录器,然后它有一个线程锁,不能被腌制。保存记录器,将其设置为None,调用其他所有内容的默认值,然后将其放回原处。面向未来,因为我不必担心忘记处理字段,并且继承的类“正常工作”。 顺便说一句,我尝试了delattr(),但它在 Python2.7 中使用AttributeError 失败了。 “将其设置为None”是我一直在使用的。 非常棒 - 用于制作具有自定义属性的 PyTorch nn.Modules 的深层副本。 @EinoGourdin deepcopy_method = self.__deepcopy__ 正在创建一个绑定到 self 的引用,然后两个对象都从类本身获取它而不是未绑定的版本。这将使从任何其他副本制作的所有副本实际上总是从原始对象制作。除非所有副本都被删除,否则原始对象永远不会被删除。【参考方案4】:

从您的问题中不清楚为什么需要覆盖这些方法,因为您不想对复制方法进行任何自定义。

无论如何,如果您确实想自定义深层副本(例如,通过共享某些属性并复制其他属性),这里有一个解决方案:

from copy import deepcopy


def deepcopy_with_sharing(obj, shared_attribute_names, memo=None):
    '''
    Deepcopy an object, except for a given list of attributes, which should
    be shared between the original object and its copy.

    obj is some object
    shared_attribute_names: A list of strings identifying the attributes that
        should be shared between the original and its copy.
    memo is the dictionary passed into __deepcopy__.  Ignore this argument if
        not calling from within __deepcopy__.
    '''
    assert isinstance(shared_attribute_names, (list, tuple))
    shared_attributes = k: getattr(obj, k) for k in shared_attribute_names

    if hasattr(obj, '__deepcopy__'):
        # Do hack to prevent infinite recursion in call to deepcopy
        deepcopy_method = obj.__deepcopy__
        obj.__deepcopy__ = None

    for attr in shared_attribute_names:
        del obj.__dict__[attr]

    clone = deepcopy(obj)

    for attr, val in shared_attributes.iteritems():
        setattr(obj, attr, val)
        setattr(clone, attr, val)

    if hasattr(obj, '__deepcopy__'):
        # Undo hack
        obj.__deepcopy__ = deepcopy_method
        del clone.__deepcopy__

    return clone



class A(object):

    def __init__(self):
        self.copy_me = []
        self.share_me = []

    def __deepcopy__(self, memo):
        return deepcopy_with_sharing(self, shared_attribute_names = ['share_me'], memo=memo)

a = A()
b = deepcopy(a)
assert a.copy_me is not b.copy_me
assert a.share_me is b.share_me

c = deepcopy(b)
assert c.copy_me is not b.copy_me
assert c.share_me is b.share_me

【讨论】:

克隆是否也需要 __deepcopy__ 方法重置,因为它将具有 __deepcopy__ = None? 不。如果未找到 __deepcopy__ 方法(或 obj.__deepcopy__ 返回 None),则 deepcopy 回退到标准的深度复制功能。这个可以看here 那么 b 就不能通过共享进行深度复制了吗? c = deepcopy(a) 与 d=deepcopy(b) 不同,因为 d 将是默认的 deepcopy,其中 c 与 a 有一些共享属性。 啊,现在我明白你在说什么了。好点子。我认为,我通过从克隆中删除虚假的__deepcopy__=None 属性来修复它。查看新代码。 python 专家可能很清楚:如果您在 python 3 中使用此代码,请将 " for attr, val in shared_attributes.iteritems():" 更改为 " for attr, val in shared_attributes.items() :"【参考方案5】:

我可能在细节上有点偏离,但这里是;

来自copy docs;

浅拷贝构造一个新的复合对象,然后(在可能的范围内)向其中插入对原始对象的引用。 深拷贝构造一个新的复合对象,然后递归地将原始对象的副本插入其中。

换句话说:copy() 将仅复制顶部元素,并将其余元素作为指向原始结构的指针。 deepcopy() 将递归复制所有内容。

也就是说,deepcopy() 就是您所需要的。

如果您需要做一些非常具体的事情,您可以覆盖__copy__()__deepcopy__(),如手册中所述。就个人而言,我可能会实现一个简单的函数(例如config.copy_config() 或类似的)以明确表明它不是 Python 标准行为。

【讨论】:

一个类为了定义自己的拷贝实现,可以定义特殊方法__copy__()和__deepcopy__() docs.python.org/library/copy.html 我会仔细检查我的代码,谢谢。如果这是其他地方的一个简单错误,我会感到很愚蠢:-P @MortenSiebuhr 你是对的。我并不完全清楚 copy/deepcopy 默认情况下会做任何事情,而无需我覆盖这些功能。我一直在寻找实际的代码,尽管我可以稍后进行调整(例如,如果我不想复制所有属性),所以我给了你一个赞成票,但我会接受@AlexMartinelli 的回答。谢谢!【参考方案6】:

copy 模块最终使用__getstate__()/__setstate__() pickling 协议,因此这些也是可以覆盖的有效目标。

默认实现只是返回并设置类的__dict__,因此您不必调用super() 并担心Eino Gourdin 的巧妙技巧above。

【讨论】:

如此简洁。不错的答案。这对我有用。【参考方案7】:

基于 Antony Hatchkins 的明确回答,这是我的版本,其中相关类派生自另一个自定义类(s.t. 我们需要调用 super):

class Foo(FooBase):
    def __init__(self, param1, param2):
        self._base_params = [param1, param2]
        super(Foo, result).__init__(*self._base_params)

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        super(Foo, result).__init__(*self._base_params)
        return result

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, copy.deepcopy(v, memo))
        super(Foo, result).__init__(*self._base_params)
        return result

【讨论】:

【参考方案8】:

Peter's 和Eino Gourdin's 的回答很聪明很有用,但是他们有一个非常微妙的错误!

Python 方法绑定到它们的对象。当您执行cp.__deepcopy__ = deepcopy_method 时,实际上是在给对象cp 引用 __deepcopy__ 在原始对象上。对cp.__deepcopy__ 的任何调用都将返回原件的副本! 如果您对对象进行深度复制,然后对该副本进行深度复制,则输出是不是副本的副本!

这是行为的一个最小示例,以及我的固定实现,您复制 __deepcopy__ 实现然后将其绑定到新对象:

from copy import deepcopy
import types


class Good:
    def __init__(self):
        self.i = 0

    def __deepcopy__(self, memo):
        deepcopy_method = self.__deepcopy__
        self.__deepcopy__ = None
        cp = deepcopy(self, memo)
        self.__deepcopy__ = deepcopy_method
        # Copy the function object
        func = types.FunctionType(
            deepcopy_method.__code__,
            deepcopy_method.__globals__,
            deepcopy_method.__name__,
            deepcopy_method.__defaults__,
            deepcopy_method.__closure__,
        )
        # Bind to cp and set
        bound_method = func.__get__(cp, cp.__class__)
        cp.__deepcopy__ = bound_method

        return cp


class Bad:
    def __init__(self):
        self.i = 0

    def __deepcopy__(self, memo):
        deepcopy_method = self.__deepcopy__
        self.__deepcopy__ = None
        cp = deepcopy(self, memo)
        self.__deepcopy__ = deepcopy_method
        cp.__deepcopy__ = deepcopy_method
        return cp


x = Bad()
copy = deepcopy(x)
copy.i = 1
copy_of_copy = deepcopy(copy)
print(copy_of_copy.i)  # 0

x = Good()
copy = deepcopy(x)
copy.i = 1
copy_of_copy = deepcopy(copy)
print(copy_of_copy.i)  # 1

【讨论】:

【参考方案9】:

出于性能原因,我来到这里。使用默认的 copy.deepcopy() 函数会使我的代码速度降低多达 30 倍。 以@Anthony Hatchkins 的answer 为起点,我意识到copy.deepcopy() 真的很慢,例如列表。我用简单的[:] 切片替换了setattr 循环以复制整个列表。对于任何关心性能的人来说,进行timeit.timeit() 比较并用更快的替代方法替换对copy.deepcopy() 的调用是值得的。

setup = 'import copy; l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]'
timeit.timeit(setup = setup, stmt='m=l[:]')
timeit.timeit(setup = setup, stmt='m=l.copy()')
timeit.timeit(setup = setup, stmt='m=copy.deepcopy(l)')

会给出这些结果:

0.11505379999289289
0.09126630000537261
6.423627900003339

【讨论】:

以上是关于如何覆盖 Python 对象的复制/深度复制操作?的主要内容,如果未能解决你的问题,请参考以下文章

python 对象克隆

如何在java中深度复制对象? [复制]

JS中如何更加深度的复制对象

如何使用 python 覆盖复制完整目录及其内容?

如何在java中深度复制对象。该对象可能是也可能不是可序列化的[重复]

如何增加 Python 中的最大递归深度? [复制]