如何覆盖 Python 对象的复制/深度复制操作?
Posted
技术标签:
【中文标题】如何覆盖 Python 对象的复制/深度复制操作?【英文标题】:How to override the copy/deepcopy operations for a Python object? 【发布时间】:2010-12-02 19:27:50 【问题描述】:我了解复制模块中copy
与deepcopy
之间的区别。我之前成功地使用过copy.copy
和copy.deepcopy
,但这是我第一次真正开始重载__copy__
和__deepcopy__
方法。我已经用谷歌搜索并查看了内置的 Python 模块以寻找 __copy__
和 __deepcopy__
函数的实例(例如 sets.py
、decimal.py
和 fractions.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
在此对象上实现copy
和deepcopy
方法以确保copy.copy
和copy.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 以避免过度复制,以防对象本身被其成员引用。
【讨论】:
@bytestormTransporter
是什么?
@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 对象的复制/深度复制操作?的主要内容,如果未能解决你的问题,请参考以下文章