Snake Mutants from Outer Space

Python gliedert seine Objekte in mutable und immutable Typen.

In der Dokumentation Kapitel 3 Data model steht:

The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable.

Alles klar, soweit. Weiter im Text:

(The value of an immutable container object that contains a reference to a mutable object can change when the latter’s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.)

Au weia, klingt gefährlich – In der Tat ist dies auch ein Quell von vielen Missverständnissen und Bugs. Deshalb dieser Artikel.

Noch schnell den Rest:

An object’s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.

Leider gibt es keine offizielle Tabelle, was nun welcher Typ ist. Deshalb hier eine von mir zusammenrecherchierte (ohne Gewähr):

immutable mutable
object  
byte array  
bytes ✌︎  
complex ✌︎  
dict  
float ✌︎  
frozen set ✌︎  
int ✌︎  
list  
long ✌︎  
named tuple ✌︎  
range ✌︎  
set  
str ✌︎  
tuple ✌︎  

Das Problem

Objekte die immutable sind verhalten sich so:

>>> x = 42
>>> y = x
>>> x += 23
>>> x
65
>>> y
42

Bei mutable schaut das aber so aus:

>>> x = list()
>>> y = x
>>> x.append('spam')
>>> y.append('eggs')
>>> x
['spam', 'eggs']
>>> y
['spam', 'eggs']

Bitte was? - In der Tabelle steht, dass mutable Typen nur Container Objekte betreffen (also Objekte, die andere Objekte beinhalten).

Das liegt daran, wie Python intern die Daten speichert und erreichbar macht. Die Details dazu können andere besser erklären, deshalb sind diese Artikel sehr empfohlen:

Und im Handbuch:

Was ist nun oben passiert? Da die Liste mutable ist, wird in der Zeile y = x nicht der Inhalt, sondern die Adresse von x nach y zugewiesen.. Dies Bedeutet:

mutable Objekte
  • sind Container mit vielen anderen Sachen drin ☞ speicherintensiv.
  • werden nur einmal im Speicher gehalten
  • jede Referenz ist sozusagen nur ein Pointer darauf.
immutable Objekte
  • werden bei jeder Zuweisung neu erstellt,
  • können also auch einen “Pointer” (auf ein anderes immutable Objekt) sein.
  • Wird jedoch der Wert geändert (Zuweisung ☞ Objekt wird neu erstellt), zeigt die Variable auf das neue Objekt.

Hier ein paar Beispiele wie einem das alles auf die Füße fallen kann, und wie man damit umgeht.

Listen

Hier eine leicht andere Variante des Beispiels von oben:

>>> x = y = ['spam', 'eggs']
>>> x.append('bacon')
>>> y.append('chips')
>>> x
['spam', 'eggs', 'bacon', 'chips']
>>> y
['spam', 'eggs', 'bacon', 'chips']

Listen muss man also referenzfrei kopieren. Das geht auf mehrere Arten:

>>> x = ['spam', 'eggs']
>>> y = list(x)
>>> z = z[:]
>>> x.append('bacon')
>>> y.append('chips')
>>> z.append('sausage')
>>> x
['spam', 'eggs', 'bacon']
>>> y
['spam', 'eggs', 'chips']
>>> z
['spam', 'eggs', 'sausage']
>>>

Egal ob list() oder [:] - beide iterieren über den Inhalt und erzeugen so neue Referenzen.

Verschachtelte Listen

Zum Beispiel brauchen wir eine Liste mit drei unter-Listen. Hämdsärmlich geht man da ran und wundert sich über folgende Ausgabe:

>>> x = [[]] * 3
>>> x
[[], [], []]
>>> x[0].append('spam')
>>> x[1].append('eggs')
>>> x
[['spam', 'eggs'], ['spam', 'eggs'], ['spam', 'eggs']]

Ja klar, wir erstellen ja eine Liste mit drei Zeigern auf das selbe innere Listen-Objekt. Das löst man das so:

>>> x = [[] for _ in range(3)]
>>> x
[[], [], []]
>>> x[0].append('spam')
>>> x[1].append('eggs')
>>> x
[['spam'], ['eggs'], []]

Durch das iterieren (for _ in range()) werden drei neue, innere Listen erstellt. Hat mir schon mal tagelang Kopfzerbrechen bereitet.

Dictionaries & Sets

Das ist eigentlich der Klassiker:

>>> x = dict()
>>> y = x
>>> y
{}
>>> x.update(spam='eggs')
>>> x
{'spam': 'eggs'}
>>> y
{'spam': 'eggs'}

Was also tun? Dictionaries & Sets haben eine copy()-Methode!

>>> x = dict(spam='eggs')
>>> y = x.copy()
>>> x.update(fish='chips')
>>> x
{'fish': 'chips', 'spam': 'eggs'}
>>> y
{'spam': 'eggs'}

Objekte

Was tun mit sonstigen Objekten?

>>> class Cls:
...     pass
...
>>> x = Cls()
>>> y = x
>>> x.spam = 'eggs'
>>> y.crispy = 'bacon'
>>> vars(x)
{'spam': 'eggs', 'crispy': 'bacon'}
>>> vars(y)
{'spam': 'eggs', 'crispy': 'bacon'}

Na Toll. Eigenen Code kann man ändern, damit es klappt, ansonsten kann man copy.copy() oder copy.deepcopy() einsetzen:

>>> x = Cls()
>>> y = copy(x)
>>> x.spam = 'eggs'
>>> y.crispy = 'bacon'
>>> vars(x)
{'spam': 'eggs'}
>>> vars(y)
{'crispy': 'bacon'}