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:
- Facts and myths about Python names and values von Ned Batchelder
- Is Python call-by-value or call-by-reference? Neither. von Jeff Knupp
Und im Handbuch:
- 4.6.1. Common Sequence Operations
- 4.6.2. Immutable Sequence Types
- 4.6.3. Mutable Sequence Types
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
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'}
last update: 2024-01-28 17:42:17 +0100