Python Language
Mutable vs. Immutable (und Hashable) in Python
Suche…
Veränderlich vs. unveränderlich
Es gibt zwei Arten von Typen in Python. Unveränderliche Typen und veränderliche Typen.
Unveränderliche
Ein Objekt eines unveränderlichen Typs kann nicht geändert werden. Bei jedem Versuch, das Objekt zu ändern, wird eine Kopie erstellt.
Diese Kategorie umfasst: Ganzzahlen, Floats, Komplexe, Zeichenfolgen, Bytes, Tupel, Bereiche und Frozensets.
Um diese Eigenschaft hervorzuheben, spielen wir mit der eingebauten id
. Diese Funktion gibt die eindeutige Kennung des als Parameter übergebenen Objekts zurück. Wenn die ID gleich ist, ist dies dasselbe Objekt. Wenn es sich ändert, ist dies ein anderes Objekt. (Einige sagen, dass dies tatsächlich die Speicheradresse des Objekts ist, aber hüten Sie sich vor ihnen, sie stammen von der dunklen Seite der Kraft ...)
>>> a = 1
>>> id(a)
140128142243264
>>> a += 2
>>> a
3
>>> id(a)
140128142243328
Okay, 1 ist nicht 3 ... Aktuelle Nachrichten ... Vielleicht nicht. Dieses Verhalten wird jedoch häufig vergessen, wenn es sich um komplexere Typen handelt, insbesondere um Strings.
>>> stack = "Overflow"
>>> stack
'Overflow'
>>> id(stack)
140128123955504
>>> stack += " rocks!"
>>> stack
'Overflow rocks!'
Aha! Sehen? Wir können es ändern!
>>> id(stack)
140128123911472
Nein . Auch wenn es scheint , können wir die Zeichenfolge durch die Variable mit dem Namen ändern stack
, was wir tatsächlich tun, ist die Schaffung eines neuen Objekts das Ergebnis der Verkettung enthalten. Wir werden getäuscht, weil das alte Objekt dabei nirgendwohin geht und somit zerstört wird. In einer anderen Situation wäre das offensichtlicher gewesen:
>>> stack = "Stack"
>>> stackoverflow = stack + "Overflow"
>>> id(stack)
140128069348184
>>> id(stackoverflow)
140128123911480
In diesem Fall ist es klar, dass wir eine Kopie benötigen, wenn wir die erste Zeichenfolge beibehalten möchten. Aber ist das für andere Typen so offensichtlich?
Übung
Nun, wenn Sie wissen, wie unveränderliche Typen funktionieren, was würden Sie mit dem folgenden Code sagen? Ist es weise
s = ""
for i in range(1, 1000):
s += str(i)
s += ","
Mutables
Ein Objekt eines wandelbaren Typ kann geändert werden, und es wird in situ verändert. Es werden keine impliziten Kopien erstellt.
Diese Kategorie umfasst: Listen, Wörterbücher, Bytearrays und Sets.
Lass uns weiter mit unserer kleinen id
Funktion spielen.
>>> b = bytearray(b'Stack')
>>> b
bytearray(b'Stack')
>>> b = bytearray(b'Stack')
>>> id(b)
140128030688288
>>> b += b'Overflow'
>>> b
bytearray(b'StackOverflow')
>>> id(b)
140128030688288
(Als Randbemerkung verwende ich Bytes, die ASCII-Daten enthalten, um meinen Standpunkt klarer zu machen, bedenke jedoch, dass Bytes nicht für Textdaten ausgelegt sind.
Was haben wir? Wir erstellen ein Bytearray, ändern es und verwenden die id
. Wir können sicherstellen, dass es sich um dasselbe Objekt handelt, das geändert wurde. Keine Kopie davon.
Wenn ein Objekt häufig geändert wird, ist ein veränderlicher Typ natürlich viel besser als ein unveränderlicher Typ. Leider wird die Realität dieser Immobilie oft vergessen, wenn sie am meisten schmerzt.
>>> c = b
>>> c += b' rocks!'
>>> c
bytearray(b'StackOverflow rocks!')
Okay...
>>> b
bytearray(b'StackOverflow rocks!')
Waiiit eine Sekunde ...
>>> id(c) == id(b)
True
Tatsächlich. c
ist keine Kopie von b
. c
ist b
.
Übung
Nun verstehen Sie besser, welche Nebenwirkung ein veränderlicher Typ impliziert. Können Sie erklären, was in diesem Beispiel schief läuft?
>>> ll = [ [] ]*4 # Create a list of 4 lists to contain our results
>>> ll
[[], [], [], []]
>>> ll[0].append(23) # Add result 23 to first list
>>> ll
[[23], [23], [23], [23]]
>>> # Oops...
Veränderlich und unveränderlich als Argumente
Ein Hauptanwendungsfall, in dem ein Entwickler die Veränderlichkeit berücksichtigen muss, ist das Übergeben von Argumenten an eine Funktion. Dies ist sehr wichtig, da dies die Fähigkeit der Funktion zum Ändern von Objekten bestimmt, die nicht zu ihrem Gültigkeitsbereich gehören, oder mit anderen Worten, wenn die Funktion Nebeneffekte hat. Dies ist auch wichtig, um zu verstehen, wo das Ergebnis einer Funktion verfügbar gemacht werden muss.
>>> def list_add3(lin):
lin += [3]
return lin
>>> a = [1, 2, 3]
>>> b = list_add3(a)
>>> b
[1, 2, 3, 3]
>>> a
[1, 2, 3, 3]
Hier besteht der Fehler darin zu glauben, dass lin
als Parameter der Funktion lokal geändert werden kann. Stattdessen referenzieren lin
und a
und dasselbe Objekt. Da dieses Objekt veränderbar ist, wird die Änderung an Ort und Stelle vorgenommen, was bedeutet, dass das von lin
und a
referenzierte Objekt geändert wird. lin
braucht nicht wirklich zurück, weil wir in Form eines Verweises auf dieses Objekt haben bereits a
. a
und b
enden auf dasselbe Objekt.
Dies gilt nicht für Tupel.
>>> def tuple_add3(tin):
tin += (3,)
return tin
>>> a = (1, 2, 3)
>>> b = tuple_add3(a)
>>> b
(1, 2, 3, 3)
>>> a
(1, 2, 3)
Zu Beginn der Funktion können tin
und a
Objekt auf dasselbe Objekt verweisen. Dies ist jedoch ein unveränderliches Objekt. Also , wenn die Funktion , es zu ändern versucht, tin
ein neues Objekt mit der Modifikation erhalten, während a
einen Verweis auf das ursprüngliche Objekt hält. In diesem Fall ist die Rücksendung von tin
obligatorisch, oder das neue Objekt würde verloren gehen.
Übung
>>> def yoda(prologue, sentence):
sentence.reverse()
prologue += " ".join(sentence)
return prologue
>>> focused = ["You must", "stay focused"]
>>> saying = "Yoda said: "
>>> yoda_sentence = yoda(saying, focused)
Hinweis: Der reverse
arbeitet direkt.
Was denkst du über diese Funktion? Hat es Nebenwirkungen? Ist die Rückgabe notwendig? Was ist es wert, nach dem Anruf zu saying
? Von focused
? Was passiert, wenn die Funktion mit den gleichen Parametern erneut aufgerufen wird?