Flaches und tiefes Kopieren



Einführung

Deep Sea Squid
Im 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 insbesondere 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öhnlich 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 veränderliche Objekte wie Listen und Dictionaries kopieren wollen. Python legt nur dann echte Kopien an, wenn es unbedingt muss, d.h. wenn es der Anwender, also der Programmierer, explizit verlangt. In diesem Kapitel wollen wir einige Probleme aufzeigen, die beim Kopieren von veränderlichen 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
>>> 
Kopieren einer einfachen Liste In dem obigen kleinen Code-Beispiel legen wir als erstes eine flache Liste mit dem Namen colours1 an. Mit "flach" (englisch in diesem Zusammenhang: shallow) meinen wir, dass die Liste nicht verschachtelt ist, also keine weiteren Unterlisten enthält. Anschließend weisen wir die Liste colours1 einer Variablen colours2 zu. Mittels der print()-Funktion können wir uns überzeugen, dass beide Variablen den gleichen Inhalt haben und mittels id() sehen wir, dass beide Variablen auf das gleiche Listenobjekt zeigen.

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", "blue"]
>>> colours2 = colours1
>>> print(id(colours1),id(colours2))
14603760 14603760
>>> colours2[1] = "green"
>>> print(id(colours1),id(colours2))
14603760 14603760
>>> print(colours1)
['red', 'green']
>>> print(colours2)
['red', 'green']
>>> 
Kopieren einer einfachen Liste Im obigen Beispiel sind wir der Frage nachgegangen, was passiert, wenn wir einer Variablen nicht ein neues Element zuordnen, sondern nur ein einzelnes Element der Liste ändern?

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 erstaunen, 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 einem Problem 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:

Kopieren einer Liste, die Unterlisten enthält

Weist man nun zum Beispiel dem 0-ten Element einer der beiden Listen einen neuen Wert zu, führt dies nicht zu einer Änderung der jeweils anderen Liste.
>>> lst1 = ['a','b',['ab','ba']]
>>> lst2 = lst1[:]
>>> lst2[0] = 'c'
>>> print(lst1)
['a', 'b', ['ab', 'ba']]
>>> print(lst2)
['c', 'b', ['ab', 'ba']]

Flaches Kopieren von Listen

Probleme gibt es erst, wenn man direkt eines der beiden Elemente der Unterliste verändert.
Um dies zu demonstrieren, ändern wir nun einen Eintrag in der Unterliste über die Listenvariable lst2:
>>> lst2[2][1] = 'd'
>>> print(lst1)
['a', 'b', ['ab', 'd']]
>>> print(lst2)
['c', 'b', ['ab', 'd']]

Man erkennt, dass man aber nicht nur die Einträge in lst2 geändert hat, sondern auch den Eintrag von lst1[2][1].
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.

Kopieren einer Liste, die Unterlisten enthält. Effekt beim Ändern eines Elementes in der Unterliste

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 "deepcopy" zur Verfügung, die das komplette Kopieren einer nicht flachen Listenstruktur erlaubt.

Wir wollen nun das obige Beispiel mittels deepcopy realisieren:
>>> from copy import deepcopy
>>> 
>>> lst1 = ['a','b',['ab','ba']]
>>> 
>>> lst2 = deepcopy(lst1)
>>> 
>>> lst1
['a', 'b', ['ab', 'ba']]
>>> lst2
['a', 'b', ['ab', 'ba']]
>>> id(lst1)
139716507600200
>>> id(lst2)
139716507600904
>>> id(lst1[0])
139716538182096
>>> id(lst2[0])
139716538182096
>>> id(lst2[2])
139716507602632
>>> id(lst1[2])
139716507615880
>>> 
Indem wir die id-Funktion verwenden, können wir erkennen, dass die Unterliste kopiert worden ist, weil id(lst2[2]) verschieden von id(lst1[2]) ist. Interessant ist auch, dass die Strings nicht kopiert worden sind: lst1[0] und lst2[0] sind Referenzen auf den gleichen String. Dies gilt natürlich auch für lst1[1] und lst2[1].
Das folgende Diagramm zeigt die Situation nach dem Kopieren der Liste mit deepcopy:

Copy a list by using deepcopy from the module

>>> lst2[2][1] = "d"
>>> lst2[0] = "c"
>>> print(lst1)
['a', 'b', ['ab', 'ba']]
>>> print(lst2)
['c', 'b', ['ab', 'd']]
>>> 

Nach den obigen Änderngen sieht die Datenstruktur wie folgt aus:

Copy a list by using deepcopy from the module, changing things afterwards