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


100

我了解复制模块copy与vs 之间的区别deepcopy。我已经使用过copy.copy并且copy.deepcopy在成功之前使用过,但这是我第一次真正地重载__copy__and __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提供适当行为的正确方法是什么?


它行得通吗?有问题吗?
Ned Batchelder

我以为我仍然在共享引用方面遇到问题,但是我很可能在其他地方搞砸了。如果有机会,我将根据@MortenSiebuhr的帖子再次检查并更新结果。
布伦特写代码

根据我目前有限的理解,我希望copy.deepcopy(ChartConfigInstance)返回一个新实例,该实例与原始实例没有任何共享引用(无需自己重新实现deepcopy)。这不正确吗?
emschorsch 2015年

Answers:


81

有关自定义的建议位于文档页面的最后

类可以使用与控制酸洗相同的接口来控制复制。有关这些方法的信息,请参见模块pickle的描述。复制模块不使用copy_reg注册模块。

为了让一个类定义自己的副本实现,它可以定义特殊的方法__copy__()__deepcopy__()。前者被称为实现浅拷贝操作;没有传递其他参数。后者被称为实现深度复制操作。它传递了一个参数,即备忘字典。如果__deepcopy__() 实现需要复制组件的深层副本,则应deepcopy()以该组件为第一个参数,并以备注字典为第二个参数来调用该函数。

由于您似乎不关心腌制自定义,因此定义__copy____deepcopy__无疑似乎是适合您的正确方法。

具体来说,__copy__(浅表副本)在您的情况下非常容易...:

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

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


1
@kaizer,可以很好地自定义酸洗/酸洗以及复制,但是,如果您不关心酸洗,则使用__copy__/ 更简单直接__deepcopy__
Alex Martelli

4
那似乎不是复制/深拷贝的直接翻译。复制和深度复制都不会调用要复制的对象的构造函数。考虑这个例子。class Test1(object):def init __(self):print“%s。%s”%(self .__ class .__ name__,“ init ”)class Test2(Test1):def __copy __(self):new = type(self) ()返回新的t1 = Test1()copy.copy(t1)t2 = Test2()copy.copy(t2)
Rob Young

12
我认为应该使用cls = self .__ class__;而不是type(self)()。cls .__ new __(cls)对构造函数接口不敏感(尤其是对于子类化)。但是,这里并不重要。
2013年

11
为什么self.foo = deepcopy(self.foo, memo)……?你不是真的newone.foo = ...
Alois Mahdal 2013年

4
@Juh_的评论是现场的。你不想打电话__init__。那不是副本的作用。此外,在很多情况下,酸洗和复印也需要不同。实际上,我什至不知道为什么复制默认会尝试使用酸洗协议。复制用于内存中操作,酸洗用于跨时期持久性;它们是完全不同的事物,彼此之间几乎没有关系。
Nimrod

96

将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以避免在对象本身从其成员引用的情况下过度复制。


2
@bytestorm是什么Transporter
Antony Hatchkins '18

@AntonyHatchkins Transporter是我正在编写的班级的名称。对于该类,我想覆盖Deepcopy行为。
bytestorm

1
@bytestorm的内容是Transporter什么?
安东尼·哈奇金斯

1
我认为__deepcopy__应该包括一个测试,以避免无限递归:< -语言:郎蟒蛇- > d = ID(个体经营)导致= memo.get(d,无),如果结果不无:返回结果
安东宁Hoskovec

@AntonyHatchkins尚不清楚您的帖子 memo[id(self)]实际用于防止无限递归的地方。我整理了一个简短的示例,该示例建议copy.deepcopy()如果对象id()的键为memo,内部中止对对象的调用,对吗?还值得注意的是,默认情况下deepcopy()它似乎是自行执行,这使我们很难想象__deepcopy__实际上需要手动定义的情况……
Jonathan H

14

遵循Peter的出色回答,实现自定义深度复制,对默认实现的更改最少(例如,仅修改所需的字段):

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

1
这比使用delattr(self, '__deepcopy__')then更合适setattr(self, '__deepcopy__', deepcopy_method)吗?
joel

根据这个答案,两者是等价的; 但是在设置名称为动态/在编码时未知的属性时,setattr更为有用。
艾诺·古尔丁

8

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

无论如何,如果您确实想自定义深层副本(例如,通过共享某些属性并复制其他属性),则可以采用以下解决方案:

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?
flutefreak17年

2
不。如果__deepcopy__未找到method(或obj.__deepcopy__返回None),则deepcopy使用标准的深度复制功能。这可以在这里
彼得

1
但是,b不会具有共享的深度复制功能吗?c = deepcopy(a)与d = deepcopy(b)不同,因为d是默认的Deepcopy,其中c与a具有一些共享的attrs。
flutefreak17年

1
啊,现在我明白了你在说什么。好点子。我认为是通过__deepcopy__=None从克隆中删除假属性来解决的。查看新代码。
彼得

1
也许对python专家很清楚:如果您在python 3中使用此代码,请将shared_attributes.iteritems()中的attr,val更改为“ attr,shared_attributes.items()中的val更改:”
complexM '18

6

我可能在细节上有些偏离,但是这里有:

copy文档 ;

  • 浅表副本将构造一个新的复合对象,然后(在可能的范围内)将对原始对象中找到的对象的引用插入其中。
  • 深层副本将构造一个新的复合对象,然后递归地将原始对象中发现的对象的副本插入其中。

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

那就是deepcopy()你所需要的。

如果您需要做一些真正特定的事情,则可以按照手册中的说明覆盖__copy__()__deepcopy__()。就个人而言,我可能会实现一个普通函数(例如config.copy_config(),诸如此类)以明确表明它不是Python标准行为。


3
为了让类定义其自己的副本实现,可以定义特殊的方法__copy__()和__deepcopy__() docs.python.org/library/copy.html
SilentGhost

我会仔细检查我的代码,谢谢。如果这是其他地方的简单错误,我将感到愚蠢:-P
布伦特撰写代码

@MortenSiebuhr你是正确的。我并不完全清楚复制/深层复制默认情况下会执行任何操作而不会覆盖这些功能。虽然我一直在寻找实际的代码,但是以后可以进行调整(例如,如果我不想复制所有属性),所以我给了你一个赞成票,但我将使用@AlexMartinelli的答案。谢谢!
布伦特写代码


1

建立在安东尼·哈奇金斯(Antony Hatchkins)干净答案的基础上,这是我的版本,其中相关类来自另一个自定义类(我们需要调用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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.