Datentyp-Hierarchie (Wiederholung)

Zu Deiner besseren Orientierung zeige ich hier nochmals die Datentyp-Hierarchie von Python.

Python Datentyphierarchie
Datentyp-Hierarchie in Python

Aliasierung

Verweisen zwei oder mehr Variablen auf ein und dasselbe Objekt, so ist jede dieser Variablen ein Alias bezüglich der anderen. Das ist unkritisch bei unveränderlichen Objekten, kann bei veränderlichen Objekten jedoch zu Effekten führen, die Du nicht beabsichtigt hast.

>>> a = [1, 2, 3, 4]
>>> b = [1, 2, 3, 4]
>>> print("id(a): {}, id(b): {}".format(id(a), id(b)))
    id(a): 1479541330368, id(b): 1479541330880
>>> a[0] = 100
>>> a, b
    ([100, 2, 3, 4], [1, 2, 3, 4])

a und b sind voneinander unabhängige Referenzen auf unterschiedliche Listenobjekte, auch wenn diese die gleichen Werte enthalten. Innerhalb der jeweiligen Listen wird auf unveränderliche Datentypen verwiesen, weshalb die IDs der Elemente identisch sind, wenn Du sie untersuchst.

>>> a = [1, 2, 3, 4]
>>> b = a
>>> print("id(a): {}, id(b): {}".format(id(a), id(b)))
    id(a): 1479541330624, id(b): 1479541330624
>>> a[0] = 100
>>> a, b
    ([100, 2, 3, 4], [100, 2, 3, 4])

Äußerlich gar nicht so verschieden, haben wir jetzt einen vollkommen anderen Sachverhalt.

Oftmals möchte man mit einer Zuweisung b = a der Variablen b eine unabhängige Liste zuweisen, die jedoch wertgleich zu der Liste von a sein soll. – Das geht so in Python nicht. Weil Python stets mit Referenzen arbeitet, werden nicht die eigentlichen Werte zugewiesen, auch wenn wir das im üblichen Sprachgebrauch so formulieren. Tatsächlich weisen wir stets Referenzen, Speicheradressen von Objekten, zu. Zur Erzeugung eines unabhängigen Objekts können wir die Zuweisung nicht verwenden, sondern müssen das Objekt kopieren.

import copy
>>> a = [1, 2, 3, 4]
>>> a
    [1, 2, 3, 4]
>>> b = copy.copy(a)
>>> b
    [1, 2, 3, 4]
>>> a[3] = 400
>>> a
    [1, 2, 3, 400]
>>> b
    [1, 2, 3, 4]

Zum Kopieren können wir das Modul copy nutzen. Das stellt uns zwei Funktionen namens copy() und deepcoy() zur Verfügung. Die beiden Funktionen unterscheiden sich hinsichtlich dessen, in welcher Tiefe sie die Struktur eines Objektes kopieren. Wir sehen uns das, um den Sachverhalt leichter verstehen zu können, an einem Beispiel an. Stellvertretend für weitere Datentypen, auf die der Sachverhalt zutrifft, nämlich alle veränderlichen Datentypen wie zum Beispiel Dictionaries, betrachten wir hier Listen.

Soeben haben wir eine flache Kopie angefertigt. Das ist eine Kopie, welche nur eine Ebene des Objekts berücksichtigt. Haben wir ein weiter strukturiertes Objekt, benötigen wir eine Tiefenkopie. Wir betrachten den Sachverhalt anhand einer Liste, die weitere Listenobjekte beinhaltet. Zuerst sehen wir uns die flache Kopie und danach die tiefe Kopie an.

import copy
>>> a = [[1, 2], ['a', 'b', 'c']]
>>> b = copy.copy(a)
>>> print("id(a)   : {}, id(b)   : {}".format(id(a), id(b)))
    id(a)   : 2389498521920, id(b)   : 2389498596032
>>> print("id(a[1]): {}, id(b[1]): {}".format(id(a[1]), id(b[1])))
    id(a[1]): 2389498460992, id(b[1]): 2389498460992
>>> a[1][0] = 'A'
>>> a, b
    ([[1, 2], ['A', 'b', 'c']], [[1, 2], ['A', 'b', 'c']])

a und b sind zwar voneinander unabhängige Kopien, die kopierten Referenzen der Listenelemente, die selbst Listen sind, sind jedoch Aliase.

Um eine vollständige Unabhängigkeit von a und b zu erreichen, müssen „innere Referenzen“, so sie veränderliche Datentypen betreffen, ebenfalls kopiert werden, um daraus voneinander unabhängige Datenobjekte zu machen.

Hier kommt deepcopy() ins Spiel. Die Funktion erzeugt eine Tiefenkopie über alle Ebenen eines Objekts. Mit deepcopy() können wir eine Tiefenkopie anfertigen, unabhängig von der Anzahl der Ebenen, in denen ein Objekt andere Objekte geschachtelt hat.

import copy
>>> a = [[1, 2], ['a', 'b', 'c']]
>>> b = copy.deepcopy(a)
>>> print("id(a)   : {}, id(b)   : {}".format(id(a), id(b)))
    id(a)   : 2389498601024, id(b)   : 2389492324160
>>> print("id(a[1]): {}, id(b[1]): {}".format(id(a[1]), id(b[1])))
    id(a[1]): 2389453938752, id(b[1]): 2389498600768
>>> a[1][0] = 'A'
>>> a, b
    ([[1, 2], ['A', 'b', 'c']], [[1, 2], ['a', 'b', 'c']])

Bei der Tiefenkopie wird das Problem der Aliasierung komplett vermieden.

Eine Tiefenkopie ist aufwendiger als eine flache Kopie, kostet mehr Laufzeit. Als Programmierer sollten wir daher sinnvoll entscheiden, welche Art von Kopie erforderlich ist, wenn wir eine solche anfertigen.