Tiefes und flaches Kopieren
Einführung
Im letzten Kapitel "Datentypen und Variablen" haben wir dargestellt, dass sich Python beim Kopieren einfacher Datentypen wie Integer und Strings ungewöhnlich im Vergleich zu anderen Programmiersprachen verhält.
In diesem Kapitel geht es nun um das Kopieren von Listen und vor allem um das Kopieren von verschachtelten Listen. Dabei kann es vor allem bei Anfängern, falls sie die genauen Zusammenhänge nicht kennen, zu verblüffenden und verwirrenden Erfahrungen kommen.
Mit dem folgenden Beispiel möchten wir uns ein paar Erkenntnisse des vorigen Kapitels nochmals in Erinnerung rufen. Die Variable y zeigt zunächst auf den gleichen Speicherplatz wie x, was wir mittels der Funktion id() erkennen können. Aber im Gegensatz zu "echten" Zeigern wie in C oder C++ erhält y einen eigenen Speicherplatz, wenn wir der Variable y einen neuen Wert zuweisen. x behält dabei selbstverständlich seinen alten Wert:
>>> x = 3 >>> y = x >>> print(id(x), id(y)) 9251744 9251744 >>> y = 4 >>> print(id(x), id(y)) 9251744 9251776 >>> print(x,y) 3 4 >>>Aber auch wenn das obige Verhalten ungewöhlich im Vergleich zu anderen Programmiersprachen wie C, C++, Perl und anderen ist, so entsprechen die Resultate der Zuweisungen dennoch unseren Erwartungen. Kritisch wird es jedoch, wenn wir mutable Objekte wie Listen und Dictionarys kopieren wollen. Python legt nur dann echte Kopien an, wenn es unbedingt muss, d.h. dass es der Anwender, also der Programmierer, explizit verlangt. In diesem Kapitel wollen wir einige Probleme aufzeigen, die beim Kopieren von mutablen Objekten entstehen können, also z.B. beim kopieren von Listen und Dictionaries.
Kopieren einer Liste
>>> colours1 = ["red", "green"] >>> colours2 = colours1 >>> print(colours1) ['red', 'green'] >>> print(colours2) ['red', 'green'] >>> print(id(colours1),id(colours2)) 43444416 43444416 >>> colours2 = ["rouge", "vert"] >>> print(colours1) ['red', 'green'] >>> print(colours2) ['rouge', 'vert'] >>> print(id(colours1),id(colours2)) 43444416 43444200 >>>
Nun weisen wir colours2 eine neue Liste zu.
Es erstaunt wenig, dass die Werte von colours1 dabei unverändert bleiben. Wie im obigen Beispiel mit den Integer-Variablen wird ein neuer Speicherbereich für colours2 angelegt, wenn dieser Variablen eine komplett neue Liste (also ein neues Objekt) zugeordnet wird.
>>> colours1 = ["red", "green"] >>> colours2 = colours1 >>> print(id(colours1),id(colours2)) 14603760 14603760 >>> colours2[1] = "blue" >>> print(id(colours1),id(colours2)) 14603760 14603760 >>> print(colours1) ['red', 'blue'] >>> print(colours2) ['red', 'blue'] >>>
Um dies zu testen weisen wir dem zweiten Element von colours2, also dem Element mit dem Index 1, einen neuen Wert zu. Viele wird es nun erstauen, dass auch colours1 damit verändert wurde, obwohl man doch eine Kopie von colours1 gemacht zu haben glaubte.
Wenn wir uns die Speicherorte mittels der Funktion id() anschauen, sehen wir, dass beide Variablen weiterhin auf das selbe Listenobjekt zeigen.
Kopie mit Teilbereichsoperator
Mit dem Teilbereichsoperator (slicing) kann man flache Listenstrukturen komplett kopieren, ohne dass es zu Seiteneffekten kommt, wie man im folgenden Beispiel sehen kann:
>>> liste1 = ['a','b','c','d'] >>> liste2 = liste1[:] >>> liste2[1] = 'x' >>> print(liste2) ['a', 'x', 'c', 'd'] >>> print(liste1) ['a', 'b', 'c', 'd'] >>>Sobald jedoch auch Unterlisten in der zu kopierenden Liste vorkommen, werden nur Zeiger auf diese Unterlisten kopiert.
>>> lst1 = ['a','b',['ab','ba']] >>> lst2 = lst1[:]
Dieses Verhalten beim Kopieren veranschaulicht das folgende Bild:
Weist man nun zum Beispiel dem 0-ten Element einer der beiden Listen einen neuen Wert zu, führt dies nicht zu einem Seiteneffekt. Probleme gibt es erst, wenn man direkt eines der beiden Elemente der Unterliste verändert.
Um dies zu demonstrieren, ändern wir nun zwei Einträge in lst2:
>>> lst1 = ['a','b',['ab','ba']] >>> lst2 = lst1[:] >>> lst2[0] = 'c' >>> lst2[2][0] = 'd' >>> print(lst1) ['a', 'b', ['d', 'ba']]
Man erkennt, dass man aber nicht nur die Einträge in lst2 geändert hat, sondern auch den Eintrag von lst1[2][0].
Dies liegt daran, dass in beiden Listen, also lst1 und lst2, das jeweils dritte Element nur ein Link auf eine physikalisch gleiche Unterliste ist. Diese Unterliste wurde nicht mit [:] mitkopiert.
Kopie mit der Methode deepcopy aus dem Modul copy
Abhilfe für das eben beschriebene Problem schafft das Modul "copy". Dieses Modul stellt die Methode "copy" zur Verfügung, die das komplette Kopieren einer nicht flachen Listenstruktur erlaubt.Das folgende Skript kopiert unser obiges Beispiel nun mit Hilfe dieser Methode:
from copy import deepcopy lst1 = ['a','b',['ab','ba']] lst2 = deepcopy(lst1) lst2[2][1] = "d" lst2[0] = "c"; print(lst2) print(lst1)Speichern wir das Skript unter deep_copy.py ab und rufen es mit "python deep_copy.py" auf, so erhalten wir folgende Ausgabe:
$ python deep_copy.py ['c', 'b', ['ab', 'd']] ['a', 'b', ['ab', 'ba']]

