36. aliasing

Im Entwurf des Konstruktors für Word oben stand

      self.symList = word.symList[:]

Der “Alles-umfassende-Slice” sorgt dafür, dass wir eine Kopie der Liste erzeugen. Bei einfacher Zuweisung hätten wir einen klassischen Fall von aliasing – die beiden Word-Instanzen würden sich eine symList teilen, Änderungen am einen Wort würden Änderungen am anderen nach sich ziehen.

>>> l1 = range(5)
>>> l2 = l1
>>> l3 = l1[:]
>>> l1.append("ha!")
>>> l1,l2,l3
([0, 1, 2, 3, 4, 'ha!'], [0, 1, 2, 3, 4, 'ha!'],
  [0, 1, 2, 3, 4])

Über aliasing muss man sich bei veränderbaren Datenstrukturen (Listen, Dictionaries) Gedanken machen, für Strings, Tupel und Zahlen kann aliasing nie ein Problem sein.

Schlimmer:

>>> l = [range(2)]*3
>>> l
[[0, 1], [0, 1], [0, 1]]
>>> l[0].append(4)
>>> l
[[0, 1, 4], [0, 1, 4], [0, 1, 4]]
>>> l1 = l[:]
>>> l.append("bla")
>>> del l[0][0]
>>> l1, l
([[1, 4], [1, 4], [1, 4]], [[1, 4], [1, 4],
  [1, 4], 'bla'])

Anmerkung: Mit del kann man generell beliebige Referenzen (und nicht nur, wie oben erwähnt, Namen) löschen. Tatsächlich ist die Verwendung beim Entfernen von Elemente aus Listen oder Dictionaries viel typischer als der Einsatz zur Manipulation von Namensräumen.

Das erzeugen einer Kopie durch Slices ist shallow: Es werden keine Kopien der darunterliegenden Datenstruktur gemacht, sondern nur die eigentliche Sequenz kopiert. Das rekursive, “tiefe” Kopieren ist im allgemeinen ein ziemlich kompliziertes Problem. Python bietet dafür (und für Kopien von Nicht-Sequenzen) ein Modul copy an:

>>> l = []
>>> for i in range(3): l.append(range(2))
...
>>> l
[[0, 1], [0, 1], [0, 1]]
>>> l[0].append(4)
>>> l
[[0, 1, 4], [0, 1], [0, 1]]
>>> import copy
>>> l2 = copy.deepcopy(l)
>>> l[1].append(4)
>>> l, l2
([[0, 1, 4], [0, 1, 4], [0, 1]], [[0, 1, 4],
  [0, 1], [0, 1]])

Moral: Wer mit veränderbaren Objekten umgeht, muss gut verstanden haben, wie das mit den Werten und den Referenzen funktioniert.

Euer Freund beim Untersuchen von aliasing ist die in Aufgaben schon öfter verwendete eingebaute Funktion id. Sie gibt eine Zahl aus, die jedes Objekt eindeutig identifiziert (d.h. gibt id verschiedene Zahlen zurück, gehen die Referenzen auf verschiedene Werte, gibt es gleiche Zahlen zurück, gehen die Referenzen auf gleiche Werte). Im Beispiel oben könnt ihr gleich sehen, was schief geht:

>>> l = [range(2)]*3
>>> map(id, l)
[1076954988, 1076954988, 1076954988]

Außerdem gibt es noch den bool’schen Operator is. Er ist vergleichbar mit ==, vergleicht aber, ob die Werte identisch und nicht einfach nur gleich sind, ob also ihre ids gleich sind:

>>> a = range(2)
>>> b = range(2)
>>> c = a
>>> a is b, a is c
(False, True)

Häufig sieht man in Python-Code das Idiom

if a is None:
  ...

oder ähnliches. Hier geht es weniger darum, die Identität von a zu prüfen (None gibt es in Python nur einmal, und ihr könnt None auch nicht kopieren – probiert es!), als vielmehr um einen geringen Geschwindigkeitsgewinn. Der is-Operator ist schneller als == (warum wohl?). In diesem Fall ist der Geschwindkeitsgewinn allerdings recht marginal. Im Fall großer Listen mag es allerdings schon viel ausmachen, nur dass man dann eigentlich meistens an Gleichheit und nicht an Identität interessiert ist. Fazit: is ist für Menschen, die den Referenzenkram sicher verstanden haben, für alle anderen ist es erstmal verzichtbar.

Übungen zu diesem Abschnitt

Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen

(1)

Probiert die Beispiele auf dieser Seite durch, bis ihr verstanden habt, was vorgeht. Benutzt id, wo nötig. Malt euch Bilder der Python-Universen (Vorsicht: in manchen Beispielen gibt es Referenzen aus dem Python-Universum ins Python-Universum und nicht nur aus dem Namensraum ins Universum rein).


Markus Demleitner

Copyright Notice