2010-11-27 1 views
3

У меня есть классLimited глубокая копия экземпляра с контейнером контейнеров в качестве атрибута

  • , экземпляры которого имеют атрибуты, которые являются контейнерами
    • которые сами по себе содержат контейнеры, каждый из которых содержит много элементов
  • имеет дорогую инициализацию этих контейнеров

Я хочу создать копии экземпляров таких, что

  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

Класс является вызываемым. Когда вы звоните SetDict(3), SetDict.__call__ сначала вызывает конструктор SetDict.__new__(SetDict), а затем вызывает инициализатор __init__(3) на возвращаемое значение __new__, если это экземпляр SetDict. Таким образом, вы можете получить новый экземпляр класса 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__, чтобы что-то сделать, и в этом случае вы должны показать, что это такое, чтобы это можно было изменить. Вот тестовый код, демонстрирующий поведение, которое вы хотите.

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]) 
1

Другим вариантом является метод __init__ принять аргумент по умолчанию copying=False. Если копирование было 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__ метод, единственной целью которых является инициализировать экземпляр и не принимать решения о том, что экземпляр должен или не должен быть инициализирован.

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