具有容器的实例的有限深拷贝
我有一个类
- 这个类的实例有一些属性,这些属性是容器
- 这些容器里面又包含其他容器,每个容器里有很多项目
- 初始化这些容器的过程比较耗费资源
我想创建这些实例的副本,要求是
- 容器属性要被复制,而不是共享引用,但
- 每个容器里的容器不需要深度复制,而是共享引用
- 如果可能的话,避免调用这个类耗费资源的
__init__()
方法
举个例子,我们来看下面的 SetDict
类。创建这个类的实例时,会初始化一个类似字典的数据结构作为属性 d
。这个 d
用整数作为键,用集合作为值。
import collections
class SetDict(object):
def __init__(self, size):
self.d = collections.defaultdict(set)
# Do some initialization; if size is large, this is expensive
for i in range(size):
self.d[i].add(1)
我想复制 SetDict
的实例,使得 d
本身被复制,但它的值(集合)则 不 被深度复制,而是仅仅作为对这些集合的引用。
例如,目前这个类的行为是这样的:使用 copy.copy
时,属性 d
不会被复制到新的副本中,而使用 copy.deepcopy
时,会完全新建一份 d
的值(集合)。
>>> import copy
>>> s = SetDict(3)
>>> s.d
defaultdict(<type 'set'>, {0: set([1]), 1: set([1]), 2: set([1])})
>>> # Try a basic copy
>>> t = copy.copy(s)
>>> # Add a new key, value pair in t.d
>>> t.d[3] = set([2])
>>> t.d
defaultdict(<type 'set'>, {0: set([1]), 1: set([1]), 2: set([1]), 3: set([2])})
>>> # But oh no! We unintentionally also added the new key to s.d!
>>> s.d
defaultdict(<type 'set'>, {0: set([1]), 1: set([1]), 2: set([1]), 3: set([2])})
>>>
>>> s = SetDict(3)
>>> # Try a deep copy
>>> u = copy.deepcopy(s)
>>> u.d[0].add(2)
>>> u.d[0]
set([1, 2])
>>> # But oh no! 2 didn't get added to s.d[0]'s set
>>> s.d[0]
set([1])
我希望看到的行为是这样的:
>>> s = SetDict(3)
>>> s.d
defaultdict(<type 'set'>, {0: set([1]), 1: set([1]), 2: set([1])})
>>> t = copy.copy(s)
>>> # Add a new key, value pair in t.d
>>> t.d[3] = set([2])
>>> t.d
defaultdict(<type 'set'>, {0: set([1]), 1: set([1]), 2: set([1]), 3: set([2])})
>>> # s.d retains the same key-value pairs
>>> s.d
defaultdict(<type 'set'>, {0: set([1]), 1: set([1]), 2: set([1])})
>>> t.d[0].add(2)
>>> t.d[0]
set([1, 2])
>>> # s.d[0] also had 2 added to its set
>>> s.d[0]
set([1, 2])
这是我第一次尝试创建一个能实现这个功能的类,但由于无限递归而失败:
class CopiableSetDict(SetDict):
def __copy__(self):
import copy
# This version gives infinite recursion, but conveys what we
# intend to do.
#
# First, create a shallow copy of this instance
other = copy.copy(self)
# Then create a separate shallow copy of the d
# attribute
other.d = copy.copy(self.d)
return other
我不太确定应该如何正确地重写 copy.copy
(或 copy.deepcopy
)的行为来实现这个目标。我也不太确定应该重写哪个,是 copy.copy
还是 copy.deepcopy
。我该如何实现我想要的复制行为呢?
3 个回答
根据aaronsterling的解决方案,我想出了以下方法,我觉得这个方法更灵活,特别是当实例有其他属性时:
class CopiableSetDict(SetDict):
def __copy__(self):
# Create an uninitialized instance
other = self.__class__.__new__(self.__class__)
# Give it the same attributes (references)
other.__dict__ = self.__dict__.copy()
# Create a copy of d dict so other can have its own
other.d = self.d.copy()
return other
另一种选择是让 __init__
方法接受一个默认参数 copying=False
。如果 copying
是 True
,那么这个方法就可以直接返回。这大概是这样的:
class Foo(object):
def __init__(self, value, copying=False):
if copying:
return
self.value = value
def __copy__(self):
other = Foo(0, copying=True)
other.value = self.value
return other
我不太喜欢这个方法,因为在你想要复制的时候,就必须给 __init__
方法传递一些无意义的参数。我更喜欢那种 __init__
方法,它的唯一目的是初始化一个实例,而不是决定这个实例是否应该被初始化。
类是可以被调用的。当你调用 SetDict(3)
时,首先会执行 SetDict.__call__
,然后它会调用构造函数 SetDict.__new__(SetDict)
,接着如果返回的结果是 SetDict
的实例,就会调用初始化方法 __init__(3)
。所以,你可以直接调用构造函数来获得一个新的 SetDict
实例,而不需要先调用它的初始化方法。
之后,你就得到了你所定义的类型的一个实例,你可以简单地添加任何容器属性的常规副本并返回它。像这样应该就可以实现你的需求。
import collections
import copy
class SetDict(object):
def __init__(self, size):
self.d = collections.defaultdict(set)
# Do some initialization; if size is large, this is expensive
for i in range(size):
self.d[i].add(1)
def __copy__(self):
other = SetDict.__new__(SetDict)
other.d = self.d.copy()
return other
__new__
是一个静态方法,它的第一个参数需要是要构造的类。除非你重写了 __new__
来做其他事情,否则应该简单得多。如果你确实重写了 __new__
,那么你应该展示一下具体做了什么,这样才能进行修改。下面是测试代码,用来演示你想要的行为。
t = SetDict(3)
print t.d # defaultdict(<type 'set'>, {0: set([1]), 1: set([1]), 2: set([1])})
s = copy.copy(t)
print s.d # defaultdict(<type 'set'>, {0: set([1]), 1: set([1]), 2: set([1])})
t.d[3].add(1)
print t.d # defaultdict(<type 'set'>, {0: set([1]), 1: set([1]), 2: set([1]), 3: set([1])})
print s.d # defaultdict(<type 'set'>, {0: set([1]), 1: set([1]), 2: set([1])})
s.d[0].add(2)
print t.d[0] # set([1, 2])
print s.d[0] # set([1, 2])