Как переопределить операции копирования / deepcopy для объекта Python? - PullRequest
77 голосов
/ 01 октября 2009

Я понимаю разницу между 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 давали мне правильное поведение?

Ответы [ 7 ]

74 голосов
/ 03 апреля 2013

Соединяя ответ Алекса Мартелли и комментарий Роба Янга, вы получите следующий код:

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, чтобы избежать избыточного копирования в случае ссылки на сам объект из его члена.

62 голосов
/ 01 октября 2009

Рекомендации по настройке приведены в самом конце страницы документации :

Классы могут использовать одни и те же интерфейсы для контролировать копирование, которое они используют для контроль травления. Смотрите описание модуля pickle для получения информации о эти методы. Модуль копирования делает не использовать регистрацию copy_reg модуль.

Чтобы класс мог определить свой собственный скопировать реализацию, он может определить специальные методы __copy__() и __deepcopy__(). Первый призван реализовать мелкую копию операция; никаких дополнительных аргументов нет прошло. Последний призван реализовать операцию глубокого копирования; Это передается один аргумент, памятка толковый словарь. Если __deepcopy__() реализация должна сделать глубокий копия компонента, она должна вызывать deepcopy() функция с компонент в качестве первого аргумента и тому словарь памятки как второй аргумент.

Поскольку вам, кажется, не нужно заботиться о настройке травления, определение __copy__ и __deepcopy__ определенно кажется правильным выбором для вас.

В частности, __copy__ (мелкая копия) довольно прост в вашем случае ...:

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

__deepcopy__ будет аналогичным (принимая также memo arg), но перед возвратом он должен будет вызвать self.foo = deepcopy(self.foo, memo) для любого атрибута self.foo, который требует глубокого копирования (по сути, атрибуты, которые являются контейнерами - списками, Дикты, не примитивные объекты, которые удерживают другие вещи через свои __dict__ с.

10 голосов
/ 08 ноября 2016

После Отличный ответ Питера , чтобы реализовать собственную глубокую копию с минимальным изменением реализации по умолчанию (например, просто изменив поле, как мне нужно):

class Foo(object):
    def __deepcopy__(self, memo):
        deepcopy_method = self.__deepcopy__
        self.__deepcopy__ = None
        cp = deepcopy(self, memo)
        self.__deepcopy__ = deepcopy_method

        # custom treatments
        # for instance: cp.id = None

        return cp
5 голосов
/ 08 июля 2014

Из вашей проблемы неясно, почему вам нужно переопределить эти методы, поскольку вы не хотите вносить какие-либо изменения в методы копирования.

В любом случае, если вы хотите настроить глубокое копирование (например, путем совместного использования одних атрибутов и копирования других), вот решение:

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
5 голосов
/ 01 октября 2009

Я, возможно, немного разбираюсь в деталях, но здесь идет;

Из copy документов ;

  • Мелкая копия создает новый составной объект, а затем (насколько это возможно) вставляет в него ссылки на объекты, найденные в оригинале.
  • Глубокая копия создает новый составной объект, а затем рекурсивно вставляет в него копии объектов, найденных в оригинале.

Другими словами: copy() скопирует только верхний элемент и оставит остальные как указатели в исходную структуру. deepcopy() будет рекурсивно копировать все.

То есть deepcopy() - это то, что вам нужно.

Если вам нужно сделать что-то действительно конкретное, вы можете переопределить __copy__() или __deepcopy__(), как описано в руководстве. Лично я, вероятно, реализовал бы простую функцию (например, config.copy_config() или такую), чтобы было ясно, что это не стандартное поведение Python.

1 голос
/ 21 марта 2018

Модуль copy постоянно использует __getstate__() / __setstate__() протокол протравливания , поэтому они также являются действительными целями для переопределения.

Реализация по умолчанию просто возвращает и устанавливает __dict__ класса, поэтому вам не нужно вызывать super() и беспокоиться о хитрых уловках Эйно Гурдина, выше .

0 голосов
/ 31 января 2018

Основываясь на чистом ответе Энтони Хэтчкинса, вот моя версия, где рассматриваемый класс происходит от другого пользовательского класса (например, нам нужно вызвать 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...