Як замінити операції копіювання / глибокого копіювання для об'єкта Python?


101

Я розумію різницю між модулем копіювання 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забезпечити мені належну поведінку?


Це працює? Чи є проблеми?
Нед Батчелдер,

Я думав, що все ще маю проблеми зі спільними посиланнями, але цілком можливо, я псував деінде. Я ще раз перевірю, базуючись на дописі @ MortenSiebuhr, коли отримаю шанс і оновити результати.
Брент пише код

З мого обмеженого розуміння, я б очікував, що copy.deepcopy (ChartConfigInstance) поверне новий екземпляр, який не матиме жодних спільних посилань з оригіналом (без повторної реалізації глибокої копії самостійно). Це неправильно?
emschorsch

Відповіді:


82

Рекомендації щодо налаштування знаходяться в самому кінці сторінки документів :

Класи можуть використовувати ті самі інтерфейси для керування копіюванням, що і для контролю маринування. Інформацію про ці методи див. В описі розсолу модуля. Модуль копіювання не використовує модуль реєстрації 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який потребує глибокого копіювання (по суті, атрибути, які є контейнерами - списки, дикти, непримітивні об'єкти, які містять інші речі через свої __dict__s).


1
@kaizer, вони чудово можуть налаштувати травлення / депікірування, а також копіювання, але якщо вам не байдуже маринування, це простіше і безпосередніше у використанні __copy__/ __deepcopy__.
Алекс Мартеллі,

4
Здається, це не прямий переклад копіювання / глибокої копії. Ні копіювання, ні глибока копія не викликають конструктор об'єкта, що копіюється. Розглянемо цей приклад. клас Test1 (об'єкт): def init __ (self): друк "% s.% s"% (self .__ class .__ name__, " init ") клас Test2 (Test1): def __copy __ (self): new = type (self) () повернути новий t1 = Test1 () copy.copy (t1) t2 = Test2 () copy.copy (t2)
Роб Янг

12
Я думаю, що замість type (self) (), ви повинні використовувати cls = self .__ class__; cls .__ new __ (cls), щоб бути нечутливим до інтерфейсу конструкторів (особливо для підкласу). Однак це насправді не важливо.
Juh_

11
Чому self.foo = deepcopy(self.foo, memo)...? Ви насправді не маєте на увазі newone.foo = ...?
Alois Mahdal

4
Коментар @ Juh_ є на місці. Ви не хочете телефонувати __init__. Це не те, що робить копія. Також дуже часто трапляється варіант використання, коли соління та копіювання повинні бути різними. Насправді, я навіть не знаю, чому copy намагається використовувати протокол травлення за замовчуванням. Копіювання призначене для маніпулювання в пам'яті, соління - для збереження перехресної епохи; це абсолютно різні речі, які мало пов’язані між собою.
Німрод,

96

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

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дикт, щоб уникнути надмірного копіювання у випадку, якщо на сам об’єкт посилається його член.


2
@bytestorm що це Transporter?
Antony Hatchkins

@AntonyHatchkins Transporter- це ім'я мого класу, яке я пишу. Для цього класу я хочу замінити поведінку копіювання.
bytestorm

1
@bytestorm що таке вміст Transporter?
Antony Hatchkins

1
Я думаю, __deepcopy__слід включити тест, щоб уникнути нескінченної рекурсії: <! - language: lang-python -> d = id (self) result = memo.get (d, None), якщо результат не None: return result
Antonín Hoskovec

@AntonyHatchkins З вашого допису не одразу видно, де memo[id(self)] насправді використовується для запобігання нескінченної рекурсії. Я зібрав короткий приклад, який передбачає, що copy.deepcopy()внутрішньо перериває виклик об'єкта, якщо він id()є ключем memo, правильно? Варто також зазначити, що, deepcopy()схоже, це робиться самостійно за замовчуванням , що ускладнює уявлення випадку, коли __deepcopy__насправді потрібно визначати вручну ...
Джонатан Х

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,

2
Ні. Якщо __deepcopy__метод не знайдено (або obj.__deepcopy__повертає None), тоді deepcopyповертається до стандартної функції глибокого копіювання. Це можна побачити тут
Петро,

1
Але тоді b не матиме можливості копіювати з обміном? c = deepcopy (a) відрізнятиметься від d = deepcopy (b), оскільки d буде типовою копією за замовчуванням, де c матиме спільні атрибути з a.
flutefreak7

1
Ах, тепер я бачу, що ти кажеш. Гарна думка. Я виправив це, я думаю, видаливши підроблений __deepcopy__=Noneатрибут із клону. Див. Новий код.
Пітер

1
можливо, зрозуміло для експертів python: якщо ви використовуєте цей код у python 3, змініть "для attr, val в shared_attributes.iteritems ():" with "для 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

Я ще раз перевірю свій код, дякую. Я відчуватиму німоту, якби це була проста помилка в іншому місці:
Brent пише код

@MortenSiebuhr Ви маєте рацію. Мені було не зовсім зрозуміло, що копіювання / глибока копія буде робити що-небудь за замовчуванням, не перевизначаючи ці функції. Хоча я шукав фактичний код, який я можу налаштувати пізніше (наприклад, якщо я не хочу копіювати всі атрибути), тому я проголосував за вас, але я збираюся відповісти @ AlexMartinelli. Дякую!
Брент пише код

2

copyМодуль використовує в кінцевому рахунку , до __getstate__()/ травленню протоколу , таким чином , вони також є допустимими об'єктами перевизначити.__setstate__()

Реалізація за замовчуванням просто повертається і встановлює __dict__клас, тому вам не доведеться телефонувати super()і турбуватися про розумний трюк Ейно Гурдіна вище .


1

Спираючись на чітку відповідь Ентоні Хеткінса, ось моя версія, де відповідний клас походить від іншого користувацького класу (st, який нам потрібно зателефонувати 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.