Jak zastąpić operacje kopiowania / głębokiego kopiowania dla obiektu Pythona?

101

Rozumiem różnicę między copya deepcopyw module kopiowania. Używałem copy.copyi copy.deepcopywcześniej z powodzeniem, ale to jest pierwszy raz, kiedy faktycznie przeładowałem metody __copy__i __deepcopy__. Już Googled się i spojrzał przez wbudowany w modułach Pythona szukać wystąpień __copy__i __deepcopy__funkcji (np sets.py, decimal.pyi fractions.py), ale nadal nie jestem w 100% pewien, że mam rację.

Oto mój scenariusz:

Mam obiekt konfiguracyjny. Początkowo zamierzam utworzyć wystąpienie jednego obiektu konfiguracyjnego z domyślnym zestawem wartości. Ta konfiguracja zostanie przekazana do wielu innych obiektów (aby zapewnić, że wszystkie obiekty będą miały taką samą konfigurację). Jednak po rozpoczęciu interakcji z użytkownikiem każdy obiekt musi niezależnie dostosować swoje konfiguracje, nie wpływając na wzajemne konfiguracje (co mówi mi, że będę musiał wykonać głębokie kopie mojej początkowej konfiguracji).

Oto przykładowy obiekt:

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 

Jaki jest właściwy sposób wdrożenia metod copyi deepcopyna tym obiekcie, aby zapewnić copy.copyi copy.deepcopyzapewnić mi właściwe zachowanie?

Brent pisze kod
źródło
Czy to działa? Czy są jakieś problemy?
Ned Batchelder
Myślałem, że nadal mam problemy ze współdzielonymi odniesieniami, ale jest całkiem możliwe, że zawaliłem gdzie indziej. Dokładnie sprawdzę na podstawie posta @ MortenSiebuhr, kiedy będę miał szansę i zaktualizuję wyniki.
Brent pisze kod
Z mojego obecnie ograniczonego zrozumienia spodziewałbym się, że copy.deepcopy (ChartConfigInstance) zwróci nową instancję, która nie miałaby żadnych wspólnych odniesień z oryginałem (bez ponownego zaimplementowania deepcopy samodzielnie). Czy to jest niepoprawne?
emschorsch

Odpowiedzi:

82

Zalecenia dotyczące dostosowywania znajdują się na samym końcu strony z dokumentami :

Klasy mogą używać tych samych interfejsów do sterowania kopiowaniem, których używają do kontrolowania wytrawiania. Zobacz opis modułu pikle, aby uzyskać informacje na temat tych metod. Moduł kopiowania nie korzysta z modułu rejestracji copy_reg.

Aby klasa mogła zdefiniować własną implementację kopii, może zdefiniować specjalne metody __copy__()i __deepcopy__(). Pierwsza jest wezwana do wykonania operacji płytkiego kopiowania; żadne dodatkowe argumenty nie są przekazywane. Ten ostatni jest wywoływany w celu wykonania operacji głębokiego kopiowania; jest przekazywany jeden argument, słownik notatek. Jeśli __deepcopy__() implementacja musi wykonać głęboką kopię komponentu, powinna wywołać deepcopy()funkcję z komponentem jako pierwszym argumentem i słownikiem memo jako drugim argumentem.

Ponieważ wydaje się, że nie zależy Ci na dostosowywaniu marynowania, zdefiniowanie __copy__i __deepcopy__zdecydowanie wydaje się być właściwą drogą dla Ciebie.

W szczególności __copy__(płytka kopia) jest w twoim przypadku dość łatwa ...:

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

__deepcopy__byłoby podobnie (akceptując również memoargument), ale przed powrotem musiałby wywołać self.foo = deepcopy(self.foo, memo)dowolny atrybut, self.fooktóry wymaga głębokiego kopiowania (zasadniczo atrybuty, które są kontenerami - listy, dykty, nieprymitywne obiekty, które przechowują inne rzeczy przez swoje __dict__).

Alex Martelli
źródło
1
@kaizer, można je dostosować do wytrawiania / otwierania, a także do kopiowania, ale jeśli nie zależy ci na wytrawianiu, jest to prostsze i bardziej bezpośrednie w użyciu __copy__/ __deepcopy__.
Alex Martelli
4
To nie wydaje się być bezpośrednim tłumaczeniem kopiowania / głębokiej kopiowania. Ani copy, ani deepcopy nie wywołują konstruktora kopiowanego obiektu. Rozważmy ten przykład. class Test1 (obiekt): 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)
Rob Young
12
Myślę, że zamiast type (self) () powinieneś użyć cls = self .__ class__; cls .__ new __ (cls), aby był niewrażliwy na interfejs konstruktora (szczególnie w przypadku podklas). Nie ma to jednak większego znaczenia.
Juh_
11
Dlaczego self.foo = deepcopy(self.foo, memo)...? Naprawdę nie masz na myśli newone.foo = ...?
Alois Mahdal
4
Komentarz @ Juh_ jest na miejscu. Nie chcesz dzwonić __init__. To nie jest to, co robi kopia. Ponadto bardzo często występuje przypadek użycia, w którym wytrawianie i kopiowanie muszą być inne. W rzeczywistości nawet nie wiem, dlaczego copy domyślnie próbuje używać protokołu wytrawiania. Kopiowanie służy do manipulacji w pamięci, wytrawianie służy do trwałości między epokami; są to zupełnie różne rzeczy, które mają ze sobą niewielki związek.
Nimrod,
97

Łącząc odpowiedź Alexa Martellego i komentarz Roba Younga, otrzymujemy następujący kod:

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

wydruki

init
11 [2, 3, 4, 5]
11 [2, 3, 4]

tutaj __deepcopy__wypełnia memodict, aby uniknąć nadmiernego kopiowania w przypadku odniesienia do samego obiektu z jego elementu członkowskiego.

Antony Hatchkins
źródło
2
@bytestorm co to jest Transporter?
Antony Hatchkins
@AntonyHatchkins Transporterto nazwa mojej klasy, którą piszę. Dla tej klasy chcę zastąpić zachowanie Deepcopy.
burza bajkowa
1
@bytestorm jaka jest zawartość Transporter?
Antony Hatchkins
1
Myślę, że __deepcopy__powinienem uwzględnić test, aby uniknąć nieskończonej rekurencji: <! - język: lang-python -> d = id (self) result = memo.get (d, None) jeśli wynik nie jest None: return result
Antonín Hoskovec
@AntonyHatchkins Z Twojego postu nie wynika od razu, gdzie memo[id(self)] faktycznie jest używany do zapobiegania nieskończonej rekurencji. Złożyłem krótki przykład, który sugeruje, że copy.deepcopy()wewnętrznie przerywa wywołanie obiektu, jeśli id()jest to klucz memo, prawda? Warto również zauważyć, że domyślniedeepcopy() wydaje się , że robi to samodzielnie, przez co trudno sobie wyobrazić przypadek, w którym __deepcopy__ręczne definiowanie jest faktycznie potrzebne ...
Jonathan H
14

Podążając za doskonałą odpowiedzią Petera , aby zaimplementować niestandardową głęboką kopię, z minimalnymi zmianami w domyślnej implementacji (np. Po prostu zmodyfikuj pole, tak jak potrzebowałem):

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
Eino Gourdin
źródło
1
jest to korzystne, do stosowania delattr(self, '__deepcopy__')wówczas setattr(self, '__deepcopy__', deepcopy_method)?
joel
Zgodnie z tą odpowiedzią oba są równoważne; ale setattr jest bardziej przydatne przy ustawianiu atrybutu, którego nazwa jest dynamiczna / nieznana w czasie kodowania.
Eino Gourdin
8

Z Twojego problemu nie wynika jasno, dlaczego musisz zastąpić te metody, ponieważ nie chcesz dostosowywać metod kopiowania.

W każdym razie, jeśli chcesz dostosować głęboką kopię (np. Udostępniając niektóre atrybuty i kopiując inne), oto rozwiązanie:

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
Piotr
źródło
Czy klon nie potrzebuje również __deepcopy__resetowania metody, ponieważ będzie miał __deepcopy__= None?
flutefreak7
2
Nie. Jeśli __deepcopy__metoda nie zostanie znaleziona (lub obj.__deepcopy__zwróci wartość None), deepcopywraca do standardowej funkcji głębokiego kopiowania. Można to zobaczyć tutaj
Piotr
1
Ale wtedy b nie będzie miał możliwości głębokiego kopiowania z udostępnianiem? c = deepcopy (a) różni się od d = deepcopy (b), ponieważ d byłby domyślną głęboką kopią, gdzie c miałoby kilka wspólnych atrybutów z a.
flutefreak7
1
Ach, teraz rozumiem, co mówisz. Słuszna uwaga. Wydaje mi się, że naprawiłem to, usuwając fałszywy __deepcopy__=Noneatrybut z klona. Zobacz nowy kod.
Peter,
1
może jasne dla ekspertów od Pythona: jeśli używasz tego kodu w pythonie 3, zmień "for attr, val w shared_attributes.iteritems ():" with "for attr, val in shared_attributes.items ():"
complexM
6

Mogę być trochę odbiegający od szczegółów, ale proszę bardzo;

Z copydokumentów ;

  • Płytka kopia tworzy nowy obiekt złożony, a następnie (w miarę możliwości) wstawia do niego odniesienia do obiektów znalezionych w oryginale.
  • Głęboka kopia tworzy nowy obiekt złożony, a następnie rekurencyjnie wstawia do niego kopie obiektów znalezionych w oryginale.

Innymi słowy: copy()skopiuje tylko górny element, a resztę pozostawi jako wskaźniki do oryginalnej struktury. deepcopy()będzie rekurencyjnie kopiować wszystko.

To znaczy, deepcopy()czego potrzebujesz.

Jeśli chcesz zrobić coś naprawdę konkretnego, możesz zastąpić __copy__()lub __deepcopy__(), zgodnie z opisem w instrukcji. Osobiście prawdopodobnie zaimplementowałbym zwykłą funkcję (np. config.copy_config()Lub coś podobnego), aby wyjaśnić, że nie jest to standardowe zachowanie Pythona.

Morten Siebuhr
źródło
3
Aby klasa mogła zdefiniować własną implementację kopiowania, może zdefiniować specjalne metody __copy__() i __deepcopy__(). docs.python.org/library/copy.html
SilentGhost
Jeszcze raz sprawdzę kod, dzięki. Czuję się głupio, gdyby to był prosty błąd gdzie indziej :-P
Brent pisze kod
@MortenSiebuhr Masz rację. Nie byłem do końca pewien, że funkcja copy / deepcopy zrobiłaby cokolwiek domyślnie bez nadpisywania tych funkcji. Szukałem jednak rzeczywistego kodu, który mogę później poprawić (np. Jeśli nie chcę kopiować wszystkich atrybutów), więc oddałem Ci głos pozytywny, ale zamierzam pójść za odpowiedzią @ AlexMartinelli. Dzięki!
Brent pisze kod
2

copyModuł wykorzystuje ostatecznie __getstate__()/ protokół staliBejcowanie , więc są one również ważne cele przesłonić.__setstate__()

Domyślna implementacja po prostu zwraca i ustawia __dict__klasę, więc nie musisz dzwonić super()i martwić się sprytną sztuczką Eino Gourdina, powyżej .

ankostis
źródło
1

Opierając się na czystej odpowiedzi Antony'ego Hatchkinsa, oto moja wersja, w której dana klasa pochodzi z innej klasy niestandardowej (st, musimy zadzwonić 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
BoltzmannBrain
źródło