具有容器的实例的有限深拷贝

3 投票
3 回答
718 浏览
提问于 2025-04-16 07:42

我有一个类

  • 这个类的实例有一些属性,这些属性是容器
  • 这些容器里面又包含其他容器,每个容器里有很多项目
  • 初始化这些容器的过程比较耗费资源

我想创建这些实例的副本,要求是

  1. 容器属性要被复制,而不是共享引用,但
  2. 每个容器里的容器不需要深度复制,而是共享引用
  3. 如果可能的话,避免调用这个类耗费资源的 __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 个回答

1

根据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
1

另一种选择是让 __init__ 方法接受一个默认参数 copying=False。如果 copyingTrue,那么这个方法就可以直接返回。这大概是这样的:

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__ 方法,它的唯一目的是初始化一个实例,而不是决定这个实例是否应该被初始化。

3

类是可以被调用的。当你调用 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])

撰写回答