Wie überschreibe ich die Kopier- / Deepcopy-Operationen für ein Python-Objekt?

100

Ich verstehe den Unterschied zwischen copyvs. deepcopyim Kopiermodul. Ich habe copy.copyund copy.deepcopyzuvor erfolgreich verwendet, aber dies ist das erste Mal, dass ich die __copy__und __deepcopy__Methoden überladen habe . Ich habe schon gegoogelt um und sah durch die integrierten Python - Module für Instanzen des aussehen __copy__und __deepcopy__Funktionen (zB sets.py, decimal.pyund fractions.py), aber ich bin immer noch nicht zu 100% sicher , dass ich es richtig habe.

Hier ist mein Szenario:

Ich habe ein Konfigurationsobjekt. Zunächst werde ich ein Konfigurationsobjekt mit einem Standardwertsatz instanziieren. Diese Konfiguration wird an mehrere andere Objekte übergeben (um sicherzustellen, dass alle Objekte mit derselben Konfiguration beginnen). Sobald die Benutzerinteraktion beginnt, muss jedes Objekt seine Konfigurationen unabhängig voneinander anpassen, ohne die Konfigurationen des anderen zu beeinflussen (was bedeutet, dass ich meine ursprüngliche Konfiguration gründlich kopieren muss, um sie weiterzugeben).

Hier ist ein Beispielobjekt:

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 

Was ist der richtige Weg, um die copyund deepcopyMethoden für dieses Objekt zu implementieren, um sicherzustellen copy.copyund copy.deepcopymir das richtige Verhalten zu geben?

Brent schreibt Code
quelle
Funktioniert es? Gibt es Probleme?
Ned Batchelder
Ich dachte, ich hätte immer noch Probleme mit gemeinsamen Referenzen, aber es ist durchaus möglich, dass ich es anderswo vermasselt habe. Ich werde es anhand des Beitrags von @ MortenSiebuhr überprüfen, wenn ich die Gelegenheit dazu bekomme und die Ergebnisse aktualisiere.
Brent schreibt Code
Nach meinem derzeit begrenzten Verständnis würde ich erwarten, dass copy.deepcopy (ChartConfigInstance) eine neue Instanz zurückgibt, die keine gemeinsamen Referenzen mit dem Original hat (ohne Deepcopy selbst neu zu implementieren). Ist das falsch
Emschorsch

Antworten:

81

Die Empfehlungen zum Anpassen finden Sie ganz am Ende der Dokumentenseite :

Klassen können dieselben Schnittstellen zum Steuern des Kopierens verwenden, die sie zum Steuern des Beizens verwenden. Informationen zu diesen Methoden finden Sie in der Beschreibung der Modulauswahl. Das Kopiermodul verwendet nicht das Registrierungsmodul copy_reg.

Damit eine Klasse ihre eigene Kopierimplementierung definieren kann, kann sie spezielle Methoden __copy__()und definieren __deepcopy__(). Ersteres wird aufgerufen, um die flache Kopieroperation zu implementieren; Es werden keine zusätzlichen Argumente übergeben. Letzteres wird aufgerufen, um die Deep-Copy-Operation zu implementieren. Es wird ein Argument übergeben, das Memo-Wörterbuch. Wenn die __deepcopy__() Implementierung eine tiefe Kopie einer Komponente erstellen muss, sollte sie die deepcopy()Funktion mit der Komponente als erstem Argument und dem Memo-Wörterbuch als zweitem Argument aufrufen .

Da Sie sich anscheinend nicht um die Anpassung von Beizen kümmern, scheint das Definieren __copy__und __deepcopy__definitiv der richtige Weg für Sie zu sein.

Insbesondere __copy__(die flache Kopie) ist in Ihrem Fall ziemlich einfach ...:

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

__deepcopy__wäre ähnlich (würde auch ein memoArgument akzeptieren ), aber vor der Rückgabe müsste self.foo = deepcopy(self.foo, memo)jedes Attribut aufgerufen werden, self.foodas gründlich kopiert werden muss (im Wesentlichen Attribute, die Container sind - Listen, Diktate, nicht-primitive Objekte, die andere Dinge durch ihre __dict__s enthalten).

Alex Martelli
quelle
1
@kaizer, es ist in Ordnung, das Beizen / Entpicken sowie das Kopieren anzupassen, aber wenn Sie sich nicht für das Beizen interessieren, ist es einfacher und direkter, __copy__/ zu verwenden __deepcopy__.
Alex Martelli
4
Das scheint keine direkte Übersetzung von Kopie / Deepcopy zu sein. Weder copy noch deepcopy rufen den Konstruktor des zu kopierenden Objekts auf. Betrachten Sie dieses Beispiel. Klasse Test1 (Objekt): def init __ (Selbst): drucke "% s.% s"% (Selbst .__ Klasse .__ Name__, " init ") Klasse Test2 (Test1): def __copy __ (Selbst): new = Typ (Selbst) () return new t1 = Test1 () copy.copy (t1) t2 = Test2 () copy.copy (t2)
Rob Young
12
Ich denke, anstelle von type (self) () sollten Sie cls = self .__ class__ verwenden. cls .__ new __ (cls) ist unempfindlich gegenüber der Konstruktorschnittstelle (insbesondere für Unterklassen). Dies ist hier jedoch nicht wirklich wichtig.
Juh_
11
Warum self.foo = deepcopy(self.foo, memo)...? Meinst du nicht wirklich newone.foo = ...?
Alois Mahdal
4
@ Juh_s Kommentar ist genau richtig. Du willst nicht anrufen __init__. Das macht die Kopie nicht. Es gibt auch sehr oft einen Anwendungsfall, in dem Beizen und Kopieren unterschiedlich sein müssen. Tatsächlich weiß ich nicht einmal, warum copy standardmäßig versucht, das Beizprotokoll zu verwenden. Das Kopieren dient der In-Memory-Manipulation, das Beizen der epochenübergreifenden Persistenz. es sind ganz andere Dinge, die wenig miteinander zu tun haben.
Nimrod
96

Wenn Sie die Antwort von Alex Martelli und den Kommentar von Rob Young zusammenfassen, erhalten Sie den folgenden Code:

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

druckt

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

Hier wird __deepcopy__das memoDiktat ausgefüllt, um übermäßiges Kopieren zu vermeiden, falls auf das Objekt selbst von seinem Mitglied verwiesen wird.

Antony Hatchkins
quelle
2
@bytestorm was ist das Transporter?
Antony Hatchkins
@AntonyHatchkins Transporterist der Name meiner Klasse, die ich schreibe. Für diese Klasse möchte ich das Deepcopy-Verhalten überschreiben.
Bytestorm
1
@bytestorm was ist der Inhalt von Transporter?
Antony Hatchkins
1
Ich denke, __deepcopy__sollte einen Test enthalten, um eine unendliche Rekursion zu vermeiden: <! - Sprache: lang-python -> d = id (self) result = memo.get (d, None), wenn das Ergebnis nicht None ist: Ergebnis zurückgeben
Antonín Hoskovec
@AntonyHatchkins Aus Ihrem Beitrag geht nicht sofort hervor, wo memo[id(self)] tatsächlich verwendet wird, um eine unendliche Rekursion zu verhindern. Ich habe ein kurzes Beispiel zusammengestellt, das darauf hinweist, dass copy.deepcopy()der Aufruf eines Objekts intern abgebrochen wird, wenn id()es ein Schlüssel von ist memo, richtig? Es ist auch erwähnenswert, dass deepcopy()dies standardmäßig von selbst zu tun scheint , was es schwierig macht, sich einen Fall vorzustellen, in dem eine __deepcopy__manuelle Definition tatsächlich erforderlich ist ...
Jonathan H
14

Nach Peters hervorragender Antwort , um eine benutzerdefinierte Deepcopy mit minimalen Änderungen an der Standardimplementierung zu implementieren (z. B. nur ein Feld so zu ändern, wie ich es brauchte):

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
quelle
1
ist dies der Verwendung delattr(self, '__deepcopy__')dann vorzuziehen setattr(self, '__deepcopy__', deepcopy_method)?
Joel
Nach dieser Antwort sind beide gleichwertig; setattr ist jedoch nützlicher, wenn Sie ein Attribut festlegen, dessen Name zum Zeitpunkt der Codierung dynamisch / nicht bekannt ist.
Eino Gourdin
8

Aus Ihrem Problem geht nicht hervor, warum Sie diese Methoden überschreiben müssen, da Sie die Kopiermethoden nicht anpassen möchten.

Wenn Sie die Deep Copy anpassen möchten (z. B. indem Sie einige Attribute freigeben und andere kopieren), finden Sie hier eine Lösung:

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
Peter
quelle
Muss der Klon nicht auch die __deepcopy__Methode zurücksetzen, da er __deepcopy__= Keine hat?
Flutefreak7
2
Nee. Wenn die __deepcopy__Methode nicht gefunden wird (oder obj.__deepcopy__None zurückgibt), wird deepcopyauf die Standard-Tiefkopierfunktion zurückgegriffen. Dies kann hier gesehen werden
Peter
1
Aber dann wird b nicht die Fähigkeit haben, mit Teilen tief zu kopieren? c = Deepcopy (a) würde sich von d = Deepcopy (b) unterscheiden, da d eine Standard-Deepcopy wäre, bei der c einige gemeinsame Attribute mit a hätte.
Flutefreak7
1
Ah, jetzt verstehe ich, was du sagst. Guter Punkt. Ich habe das Problem behoben, indem ich das gefälschte __deepcopy__=NoneAttribut aus dem Klon gelöscht habe . Siehe neuen Code.
Peter
1
Vielleicht den Python-Experten klar: Wenn Sie diesen Code in Python 3 verwenden, ändern Sie "für attr, val in shared_attributes.iteritems ():" mit "für attr, val in shared_attributes.items ():"
complexM
6

Ich bin vielleicht ein bisschen daneben, aber hier geht es weiter.

Aus den copyDokumenten ;

  • Eine flache Kopie erstellt ein neues zusammengesetztes Objekt und fügt dann (soweit möglich) Verweise auf die im Original gefundenen Objekte ein.
  • Eine tiefe Kopie erstellt ein neues zusammengesetztes Objekt und fügt dann rekursiv Kopien der im Original gefundenen Objekte ein.

Mit anderen Worten: copy()Kopiert nur das oberste Element und belässt den Rest als Zeiger in der ursprünglichen Struktur. deepcopy()wird rekursiv über alles kopieren.

Das ist, deepcopy()was Sie brauchen.

Wenn Sie etwas wirklich Spezifisches tun müssen, können Sie es überschreiben __copy__()oder __deepcopy__(), wie im Handbuch beschrieben. Persönlich würde ich wahrscheinlich eine einfache Funktion (z. B. config.copy_config()oder so) implementieren , um deutlich zu machen, dass es sich nicht um Python-Standardverhalten handelt.

Morten Siebuhr
quelle
3
Um für eine Klasse eine eigene Kopie Implementierung zu definieren, kann es spezielle Methoden definieren __copy__() und __deepcopy__(). docs.python.org/library/copy.html
SilentGhost
Ich werde meinen Code noch einmal überprüfen, danke. Ich werde mich dumm fühlen, wenn dies anderswo ein einfacher Fehler war :-P
Brent schreibt Code
@MortenSiebuhr Du bist richtig. Mir war nicht ganz klar, dass Copy / Deepcopy standardmäßig alles tun würde, ohne dass ich diese Funktionen überschreibe. Ich habe nach tatsächlichem Code gesucht, den ich später anpassen kann (z. B. wenn ich nicht alle Attribute kopieren möchte), also habe ich Ihnen eine Abstimmung gegeben, aber ich werde mit der Antwort von @ AlexMartinelli fortfahren. Vielen Dank!
Brent schreibt Code
2

Das copyModul verwendet schließlich das __getstate__()/ pickling-Protokoll , daher sind dies auch gültige Ziele, die überschrieben werden müssen.__setstate__()

Die Standardimplementierung gerade Rückkehr und setzt die __dict__von der Klasse, so dass Sie müssen nicht nennen super()cleveren Trick und Sorgen über Eino Gourdin der, oben .

Ankostis
quelle
1

Aufbauend auf der klaren Antwort von Antony Hatchkins ist hier meine Version, in der die betreffende Klasse von einer anderen benutzerdefinierten Klasse stammt (st müssen wir anrufen 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
quelle