Flaches vs tiefes Kopieren von Python-Objekten

Flaches vs tiefes Kopieren von Python-Objekten

Assignment statements in Python do not create copies von Objekten binden sie nur Namen an ein Objekt. Bei unveränderlichen Objekten macht dies normalerweise keinen Unterschied.

Wenn Sie jedoch mit veränderlichen Objekten oder Sammlungen veränderlicher Objekte arbeiten möchten, suchen Sie möglicherweise nach einer Möglichkeit, „echte Kopien“ oder „Klone“ dieser Objekte zu erstellen.

Im Wesentlichen möchten Sie manchmal Kopien, bei denen Siewithout ändern können, wobei das Original gleichzeitig automatisch geändert wird. In diesem Artikel werde ich Ihnen einen Überblick über das Kopieren oder "Klonen" von Objekten in Python 3 und einige der damit verbundenen Einschränkungen geben.

Note: Dieses Tutorial wurde unter Berücksichtigung von Python 3 geschrieben, es gibt jedoch kaum Unterschiede zwischen Python 2 und 3, wenn es um das Kopieren von Objekten geht. Wenn es Unterschiede gibt, werde ich sie im Text hervorheben.

Schauen wir uns zunächst an, wie Sie die in Python integrierten Sammlungen kopieren. Pythons integrierte veränderbare Sammlungen wielists, dicts, and setskönnen kopiert werden, indem ihre Factory-Funktionen für eine vorhandene Sammlung aufgerufen werden:

new_list = list(original_list)
new_dict = dict(original_dict)
new_set = set(original_set)

Diese Methode funktioniert jedoch nicht für benutzerdefinierte Objekte und erstellt darüber hinaus nurshallow copies. Bei zusammengesetzten Objekten wie Listen, Diktaten und Mengen gibt es einen wichtigen Unterschied zwischen dem Kopieren vonshallow unddeep:

  • Einshallow copy bedeutet, ein neues Sammlungsobjekt zu erstellen und es dann mit Verweisen auf die im Original gefundenen untergeordneten Objekte zu füllen. Im Wesentlichen beträgt eine flache Kopie nurone level deep. Der Kopiervorgang wird nicht wiederholt und erstellt daher keine Kopien der untergeordneten Objekte.

  • Adeep copy macht den Kopiervorgang rekursiv. Es bedeutet, zuerst ein neues Sammlungsobjekt zu erstellen und es dann rekursiv mit Kopien der im Original gefundenen untergeordneten Objekte zu füllen. Wenn Sie ein Objekt auf diese Weise kopieren, wird der gesamte Objektbaum durchlaufen, um einen vollständig unabhängigen Klon des ursprünglichen Objekts und aller seiner untergeordneten Objekte zu erstellen.

Ich weiß, das war ein bisschen mundvoll. Schauen wir uns also einige Beispiele an, um diesen Unterschied zwischen tiefen und flachen Kopien zu verdeutlichen.

Free Bonus:Click here to get access to a chapter from Python Tricks: The Book zeigt Ihnen die Best Practices von Python anhand einfacher Beispiele, die Sie sofort anwenden können, um schöneren + Pythonic-Code zu schreiben.

Flache Kopien erstellen

Im folgenden Beispiel erstellen wir eine neue verschachtelte Liste und kopieren sie dann mitshallowly mit der Factory-Funktion vonlist():

Dies bedeutet, dassys jetzt ein neues und unabhängiges Objekt mit demselben Inhalt wiexs ist. Sie können dies überprüfen, indem Sie beide Objekte untersuchen:

Um zu bestätigen, dassys wirklich unabhängig vom Original ist, lassen Sie uns ein kleines Experiment entwickeln. Sie können versuchen, dem Original eine neue Unterliste hinzuzufügen (xs) und dann überprüfen, ob diese Änderung keine Auswirkungen auf die Kopie hat (ys):

>>>

Wie Sie sehen, hatte dies den erwarteten Effekt. Das Ändern der kopierten Liste auf einer "oberflächlichen" Ebene war überhaupt kein Problem.

Da wir jedoch nur eineshallow-Kopie der ursprünglichen Liste erstellt haben, enthältys weiterhin Verweise auf die ursprünglichen untergeordneten Objekte, die inxs gespeichert sind.

Diese Kinder wurdennot kopiert. Sie wurden lediglich in der kopierten Liste erneut referenziert.

Wenn Sie eines der untergeordneten Objekte inxs ändern, wird diese Änderung daher auch inys wiedergegeben. Dies liegt daran, dassboth lists share the same child objects. Die Kopie ist nur eine flache, einstufige, tiefe Kopie:

>>>

>>> xs[1][0] = 'X'
>>> xs
[[X', 5, 6], [7, 8, 9], ['new sublist']]
>>> ys
[[X', 5, 6], [7, 8, 9]]

Im obigen Beispiel haben wir (scheinbar) nur eine Änderung anxs vorgenommen. Es stellt sich jedoch heraus, dassboth Unterlisten bei Index 1 inxsandys geändert wurden. Dies geschah erneut, weil wir nur eineshallow-Kopie der ursprünglichen Liste erstellt hatten.

Hätten wir im ersten Schritt einedeep-Kopie vonxs erstellt, wären beide Objekte völlig unabhängig gewesen. Dies ist der praktische Unterschied zwischen flachen und tiefen Kopien von Objekten.

Jetzt wissen Sie, wie Sie flache Kopien einiger der integrierten Sammlungsklassen erstellen, und Sie kennen den Unterschied zwischen flachem und tiefem Kopieren. Die Fragen, auf die wir noch Antworten wünschen, sind:

  • Wie können Sie tiefe Kopien von integrierten Sammlungen erstellen?

  • Wie können Sie Kopien (flach und tief) von beliebigen Objekten erstellen, einschließlich benutzerdefinierter Klassen?

Die Antwort auf diese Fragen liegt im Modulcopy in der Python-Standardbibliothek. Dieses Modul bietet eine einfache Schnittstelle zum Erstellen flacher und tiefer Kopien beliebiger Python-Objekte.

Tiefe Kopien erstellen

Wiederholen wir das vorherige Beispiel zum Kopieren von Listen, jedoch mit einem wichtigen Unterschied. Dieses Mal erstellen wir stattdessen einedeep-Kopie mit derdeepcopy()-Funktion, die imcopy-Modul definiert ist:

>>>

>>> import copy
>>> xs = [[zs = copy.deepcopy(xs)

Wenn Siexs und den Klonzs untersuchen, den wir mitcopy.deepcopy() erstellt haben, werden Sie feststellen, dass beide wieder identisch aussehen - genau wie im vorherigen Beispiel:

>>>

Wenn Sie jedoch eine Änderung an einem der untergeordneten Objekte im Originalobjekt (xs) vornehmen, werden Sie feststellen, dass diese Änderung keine Auswirkungen auf die Tiefenkopie (zs) hat.

Beide Objekte, das Original und die Kopie, sind diesmal völlig unabhängig. xs wurde rekursiv geklont, einschließlich aller untergeordneten Objekte:

>>>

Vielleicht möchten Sie sich etwas Zeit nehmen, um sich mit dem Python-Interpreter zusammenzusetzen und diese Beispiele jetzt durchzuspielen. Das Kopieren von Objekten ist einfacher, wenn Sie die Beispiele aus erster Hand erleben und damit spielen.

Übrigens können Sie mit einer Funktion im Modulcopyauch flache Kopien erstellen. Die Funktioncopy.copy()erstellt flache Kopien von Objekten.

Dies ist nützlich, wenn Sie klar kommunizieren müssen, dass Sie irgendwo in Ihrem Code eine flache Kopie erstellen. Mitcopy.copy() können Sie diese Tatsache anzeigen. Bei integrierten Sammlungen wird es jedoch als pythonischer angesehen, einfach die Liste zu verwenden, zu diktieren und die Factory-Funktionen festzulegen, um flache Kopien zu erstellen.

Kopieren beliebiger Python-Objekte

Die Frage, die wir noch beantworten müssen, ist, wie wir Kopien (flach und tief) von beliebigen Objekten erstellen, einschließlich benutzerdefinierter Klassen. Schauen wir uns das jetzt an.

Wieder kommt das Modulcopyzu unserer Rettung. Mit den Funktionencopy.copy() undcopy.deepcopy() können beliebige Objekte dupliziert werden.

Der beste Weg, um zu verstehen, wie man diese verwendet, ist ein einfaches Experiment. Ich werde dies auf das vorherige Beispiel zum Kopieren von Listen stützen. Beginnen wir mit der Definition einer einfachen 2D-Punktklasse:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Point({self.x!r}, {self.y!r})'

Ich hoffe, Sie stimmen zu, dass dies ziemlich einfach war. Ich habe die Implementierung von__repr__()hinzugefügt, damit wir Objekte, die aus dieser Klasse erstellt wurden, im Python-Interpreter problemlos überprüfen können.

Note: Im obigen Beispiel wird einPython 3.6 f-string verwendet, um die von__repr__ zurückgegebene Zeichenfolge zu erstellen. Unter Python 2 und Versionen von Python 3 vor 3.6 verwenden Sie einen anderen Ausdruck für die Zeichenfolgenformatierung, z.

def __repr__(self):
    return 'Point(%r, %r)' % (self.x, self.y)

Als Nächstes erstellen wir einePoint-Instanz und kopieren sie dann (flach) mit demcopy-Modul:

>>>

>>> a = Point(23, 42)
>>> b = copy.copy(a)

Wenn wir den Inhalt des ursprünglichenPoint-Objekts und seines (flachen) Klons untersuchen, sehen wir, was wir erwarten würden:

>>>

>>> a
Point(23, 42)
>>> b
Point(23, 42)
>>> a is b
False

Hier ist noch etwas zu beachten. Da unser Punktobjekt unveränderliche Typen (Ints) für seine Koordinaten verwendet, gibt es in diesem Fall keinen Unterschied zwischen einer flachen und einer tiefen Kopie. Aber ich werde das Beispiel gleich erweitern.

Kommen wir zu einem komplexeren Beispiel. Ich werde eine andere Klasse definieren, um 2D-Rechtecke darzustellen. Ich mache das so, dass wir eine komplexere Objekthierarchie erstellen können. Meine Rechtecke verwendenPoint-Objekte, um ihre Koordinaten darzustellen:

class Rectangle:
    def __init__(self, topleft, bottomright):
        self.topleft = topleft
        self.bottomright = bottomright

    def __repr__(self):
        return (f'Rectangle({self.topleft!r}, '
                f'{self.bottomright!r})')

Wieder werden wir zunächst versuchen, eine flache Kopie einer Rechteckinstanz zu erstellen:

rect = Rectangle(Point(0, 1), Point(5, 6))
srect = copy.copy(rect)

Wenn Sie das ursprüngliche Rechteck und seine Kopie untersuchen, werden Sie sehen, wie gut die Überschreibung von__repr__()funktioniert und ob der flache Kopiervorgang wie erwartet funktioniert hat:

>>>

>>> rect
Rectangle(Point(0, 1), Point(5, 6))
>>> srect
Rectangle(Point(0, 1), Point(5, 6))
>>> rect is srect
False

Erinnern Sie sich, wie das vorherige Listenbeispiel den Unterschied zwischen tiefen und flachen Kopien veranschaulicht hat? Ich werde hier den gleichen Ansatz verwenden. Ich werde ein Objekt tiefer in der Objekthierarchie ändern. Diese Änderung wird dann auch in der (flachen) Kopie angezeigt:

>>>

>>> rect.topleft.x = 999
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))

Ich hoffe, das hat sich so verhalten, wie Sie es erwartet haben. Als Nächstes erstelle ich eine tiefe Kopie des ursprünglichen Rechtecks. Dann werde ich eine weitere Änderung vornehmen und Sie werden sehen, welche Objekte betroffen sind:

>>>

>>> drect = copy.deepcopy(srect)
>>> drect.topleft.x = 222
>>> drect
Rectangle(Point(222, 1), Point(5, 6))
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))

Voila! Diesmal ist die tiefe Kopie (drect) völlig unabhängig vom Original (rect) und der flachen Kopie (srect).

Wir haben hier viel unternommen, und es gibt noch einige Feinheiten beim Kopieren von Objekten.

Es lohnt sich, sich eingehend mit diesem Thema zu befassen (ha!), Daher sollten Sie sich mit dencopy module documentation befassen. Beispielsweise können Objekte steuern, wie sie kopiert werden, indem sie die speziellen Methoden__copy__() und__deepcopy__() definieren.

3 Dinge, an die man sich erinnern sollte

  • Wenn Sie eine flache Kopie eines Objekts erstellen, werden untergeordnete Objekte nicht geklont. Daher ist die Kopie nicht vollständig unabhängig vom Original.

  • Eine tiefe Kopie eines Objekts klont rekursiv untergeordnete Objekte. Der Klon ist völlig unabhängig vom Original, das Erstellen einer tiefen Kopie ist jedoch langsamer.

  • Sie können beliebige Objekte (einschließlich benutzerdefinierter Klassen) mit dem Modulcopy kopieren.

Wenn Sie sich eingehender mit anderen Python-Programmiertechniken für Fortgeschrittene befassen möchten, sehen Sie sich diesen kostenlosen Bonus an:

Free Bonus:Click here to get access to a chapter from Python Tricks: The Book zeigt Ihnen die Best Practices von Python anhand einfacher Beispiele, die Sie sofort anwenden können, um schöneren + Pythonic-Code zu schreiben.