Как переопределить операции копирования / глубокого копирования для объекта Python?

101

Я понимаю разницу между copyvs. 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 самостоятельно). Это неправильно?
emschorsch

Ответы:

82

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

Классы могут использовать те же интерфейсы для управления копированием, которые они используют для управления травлением. См. Описание модуля 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тоже принимает аргумент), но перед возвратом он должен будет вызвать self.foo = deepcopy(self.foo, memo)любой атрибут, self.fooкоторый требует глубокого копирования (по сути, атрибуты, которые являются контейнерами - списки, dicts, непримитивные объекты, которые содержат другие данные через свои __dict__s).

Алекс Мартелли
источник
1
@kaizer, они могут настраивать травление / распаковку, а также копирование, но если вас не волнует травление, это проще и удобнее использовать __copy__/ __deepcopy__.
Алекс Мартелли,
4
Это не похоже на прямой перевод copy / deepcopy. Ни copy, ни deepcopy не вызывают конструктор копируемого объекта. Рассмотрим этот пример. class Test1 (объект): def init __ (self): print "% s.% s"% (self .__ class .__ name__, " init ") class Test2 (Test1): def __copy __ (self): new = type (self) () return new t1 = Test1 () copy.copy (t1) t2 = Test2 () copy.copy (t2)
Роб Янг
12
Я думаю, что вместо type (self) () вы должны использовать cls = self .__ class__; cls .__ new __ (cls), чтобы быть нечувствительным к интерфейсу конструкторов (особенно для подклассов). Однако здесь это не так важно.
Juh_ 05
11
Почему self.foo = deepcopy(self.foo, memo)...? Вы действительно имеете в виду newone.foo = ...?
Alois Mahdal
4
Комментарий @Juh_ уместен. Вы не хотите звонить __init__. Это не то, что делает копия. Также очень часто бывает, что травление и копирование должны отличаться. На самом деле, я даже не знаю, почему copy пытается использовать протокол травления по умолчанию. Копирование предназначено для манипуляций в памяти, травление - для сохраняемости между эпохами; это совершенно разные вещи, которые мало связаны друг с другом.
Нимрод
97

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

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

Энтони Хэтчкинс
источник
2
@bytestorm что это такое Transporter?
Энтони Хэтчкинс
@AntonyHatchkins Transporter- это имя моего класса, который я пишу. Для этого класса я хочу переопределить поведение deepcopy.
bytestorm
1
@bytestorm что это за содержимое Transporter?
Энтони Хэтчкинс
1
Я думаю, __deepcopy__следует включить тест, чтобы избежать бесконечной рекурсии: <! - language: lang-python -> d = id (self) result = memo.get (d, None), если результат не равен None: return result
Антонин Хосковец
@AntonyHatchkins Из вашего сообщения не сразу понятно, где на memo[id(self)] самом деле используется, чтобы предотвратить бесконечную рекурсию. Я собрал короткий пример, который предполагает, что copy.deepcopy()внутренне прерывает вызов объекта, если id()это ключ memo, правильно? Также стоит отметить , что , deepcopy()кажется, делает это самостоятельно , по умолчанию , что делает его трудно представить себе случай , когда Defining __deepcopy__вручную фактически необходимо ...
Jonathan H
14

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

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
Эйно Гурден
источник
1
это предпочтительнее использовать delattr(self, '__deepcopy__')тогда setattr(self, '__deepcopy__', deepcopy_method)?
Джоэл
Согласно этому ответу , оба они эквивалентны; но setattr более полезен при установке атрибута, имя которого является динамическим / неизвестным во время кодирования.
Эйно Гурден,
8

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

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

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?
flutefreak7 06
2
Нет. Если __deepcopy__метод не найден (или obj.__deepcopy__возвращает None), то deepcopyвозвращается к стандартной функции глубокого копирования. Это можно увидеть здесь
Питер
1
Но тогда у b не будет возможности глубокого копирования с совместным использованием? c = deepcopy (a) будет отличаться от d = deepcopy (b), потому что d будет глубокой копией по умолчанию, где c будет иметь некоторые общие атрибуты с a.
flutefreak7
1
Ах, теперь я понимаю, о чем вы говорите. Хорошая точка зрения. Я исправил это, я думаю, удалив фейковый __deepcopy__=Noneатрибут из клона. Смотрите новый код.
Питер
1
может быть ясно для специалистов по питону: если вы используете этот код в Python 3, измените "на attr, val в shared_attributes.iteritems ():" на "для attr, val в shared_attributes.items ():"
complexM
6

Я могу быть немного не в деталях, но вот оно;

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

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

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

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

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

Мортен Зибур
источник
3
Чтобы класс мог определить свою собственную реализацию копии, он может определять специальные методы __copy__() и __deepcopy__(). docs.python.org/library/copy.html
SilentGhost,
Я перепроверяю свой код, спасибо. Я буду чувствовать себя глупо, если бы это была простая ошибка где-то еще :-P
Брент пишет код
@MortenSiebuhr Вы правы. Мне не совсем ясно, что copy / deepcopy будет делать что-нибудь по умолчанию без того, чтобы я переопределил эти функции. Я искал реальный код, который я могу настроить позже (например, если я не хочу копировать все атрибуты), поэтому я дал вам голосование, но я собираюсь пойти с ответом @ AlexMartinelli. Спасибо!
Брент пишет код
2

В copyконечном итоге модуль использует протокол__getstate__() / pickling , так что это также допустимые цели для переопределения.__setstate__()

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

Анкостис
источник
1

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