Mutable und Immutable Objects

Eine bedeutende Unterscheidung von Objekten in Python liegt in ihrer Fähigkeit zur Veränderung. Es werden sog. Mutable- (veränderliche) von Immutable- (unveränderliche) Objects unterschieden. Was diese Objekttypen auszeichnet, erfahren Sie in diesem Beitrag.

Inhalt

Eine Einordnung der Python-Objekte in Mutable und Immutable

Zu den Immutable-Objects zählen:

  • int, float, string, bool, decimal, complex
  • tuple
  • range
  • frozenset
  • bytes

Zu den Mutable-Objects zählen:

  • list
  • dictionary
  • set
  • User-Definierte Objekte

Objektkennungen in Python

Dies sei an den Anfang gestellt: In Python wird ein Objekt durch 4 zentrale Kennungen beschrieben:

  1. Name – Der Bezeichner, mit dem der Wert aus der Session angesprochen werden kann
  2. Wert – Der im Speicher abgelegte Wert
  3. Typ – Die Klasse des Objekts
  4. ID – Der physikalische Speicherort des Objekts

Wird beispielsweise ein Objekt x in dieser Form erstellt,

x = 100

dann trägt das resultierende Objekt folgende Eigenschaften:

Immutable-Objects

In Python ist ein Objekt immutable wenn es, sobald es einmal im Speicher abgelegt ist, nicht wieder verändert werden kann – also alle oben beschriebenen Objekteigenschaften konstant bleiben. Manchmal wird davon gesprochen, das Objekt sei „hashable„. Dies bedeutet, dass die Klasse über eine Methode __hash__ verfügt, deren Rückgabewert über die gesamte Zeit der Session besteht. Dictionaries nutzen die Methode __hash__ für die interne Key-Value Zuordnung. Da nur Immutable-Objects über eine solche Methode verfügen, können auch nur sie als Keys in Dictionaries verwendet werden.

In einem Beispiel wird das Verhalten von Immutable-Objects deutlich. Erzeugen wir uns dafür wieder das Objekt x mit der Wertzuweisung 100 und lassen Sie uns die weiteren Objekteigenschaften ausgeben:

x = 100
print('Klasse: ', type(x), '\n',
      'Speicherort: ', id(x))
>>> Klasse:  <class 'int'> 
>>> Speicherort: 1635454544

Python wird es nun nicht zulassen, dass wir x in irgendeiner Weise verändern. Wenn wir es dennoch versuchen, wird Python eine Kopie von x erstellen. Dieser Mechanismus wird als Copy-On-Modify bezeichnet. Probieren wir es aus, indem wir x einen neuen Wert zuweisen:

x = 200
print('Klasse: ', type(x), '\n',
      'Speicherort: ', id(x))
>>> Klasse:  <class 'int'> 
>>> Speicherort: 1635457744

Das x welches wir nun betrachten, hat einen anderen Speicherort als das x welches den Wert 100 enthält. Und tatsächlich handelt es sich physikalisch um 2 verschiedene Objekte. Das Objekt x mit dem Wert 100 wurde bei der erneuten Zuweisung kopiert und schließlich verändert. Während die Python-Session noch aktiv ist, wird sich am Speicherort 1635454544 weiterhin der Wert 100 befinden. Aus der Session ist er allerdings nicht mehr Ansprechbar, da wir seinen Bezeichner nun für eine andere Stelle im Speicher verwenden.

Gleiches Verhalten sehen wir erwartungsgemäß auch dann, wenn wir das Objekt einer anderen Klasse zuordnen wollen:

x = float(x)
print('Klasse: ', type(x), '\n',
      'Speicherort: ', id(x))
>>> Klasse:  <class 'float'> 
>>> Speicherort: 2293442244136

Mutable-Objects

Ein Mutable-Object kann einzelne Eigenschaftswerte ändern und dabei seine Speicheridentität beibehalten. Erzeugen wir uns dazu eine Liste und schauen uns ihr Verhalten an, wenn wir ihre Werte bearbeiten:

myList = [1,2,3]
print('Klasse: ', type(myList), '\n',
      'Speicherort: ', id(myList))
>>> Klasse:  <class 'list'> 
>>> Speicherort: 2293459844808

myList[2] = 100
print('Klasse: ', type(myList), '\n',
      'Speicherort: ', id(myList))
>>> Klasse:  <class 'list'> 
>>> Speicherort: 2293459844808

print(myList)
>>> [1, 2, 100]

Wie wir sehen, hat sich der Speicherort der Liste nicht geändert. Wir haben tatsächlich myList bearbeitet und nicht eine Kopie von ihr.

In-Place Operationen

Sind Objekte veränderbar, muss bei der Verwendung vieler Methoden keine Zuweisung mehr auf das Objekt erfolgen. Man spricht davon, das Objekt In-Place zu bearbeiten. Das folgende Beispiel zeigt, dass eine Methode angewendet wird, ohne das eine Zuweisung erfolgt:

myList = [1,2,3]
print(myList)
>>> [1, 2, 3]

myList.append(4)
[1, 2, 3, 4]

Kopieren von Mutable-Objects

Im Umgang mit Mutable-Objects ist Vorsicht geboten. Eine Zuweisung auf ein neues Objekt erzeugt keine Kopie im Speicher, sondern lediglich eine Kopie der Referenz auf den Speicher. Mit jeder Referenz die in der Session auf einen bestimmten Speicherplatz zeigt, können Änderungen am physikalischen Objekt vorgenommen werden.

# Erzeugen einer Liste
myList = [1,2,3]

# Zuweisung
myNewList = myList

# Anhängen eines Wertes an die Liste myNewList
myNewList.append(4)


print(myNewList)
>>> [1, 2, 3, 4]

print(myList)
>>> [1, 2, 3, 4]

Wir in den Print-Ausgaben zu sehen ist, hat sich die myNewList erwartungsgemäß um das Element mit dem Wert 4 erweitert. Ebenso zeigt sich, dass myList um dieses Element erweitert wurde – tatsächlich sehen wir in den beiden print-Ausgaben das Selbe (im physikalischen Sinne) Objekt. Wir sprechen es allerdings mit 2 verschiedenen Referenzen an.

print(id(myList))
>>> 2293460480200

print(id(myNewList))
>>> 2293460480200

Das Schaubild zeigt schematisch, wie unterschiedlich die Zuweisungen von Mutable- und Immutable-Objects ablaufen: Für veränderliche Objekte wird lediglich eine neue Referenz erstellt, während unveränderliche Objekte eine „echte“ Kopie erstellen.

Copy

Verfügt eine Klasse über eine copy-Methode, ist dies ein sehr guter Hinweis darauf, dass es sich um einen Mutable-Objekttyp handelt. Mit der Methode erstellen wir explizit eine Kopie des Objekts (bei Immutable-Objekten wird ohnehin immer kopiert und es machte keinen Sinn, eine explizite Methode dafür bereitzustellen).

Hier kopieren wir die Liste myList:

myList = [1,2,3]
myNewList = myList.copy()

print(id(myList))
>>> 2293460002632

print(id(myNewList))
>>> 2293460483016

Mutable Objects in Funktionsdefinitionen

Mutable-Objects führen zu einem klassischen Einsteigerfehler von Python-Programmierern. Die Verwendung von Mutable-Objects als Argument in Funktionskonstrukten sollte in der Regel vermieden werden. Der unten aufgeführte Beispielcode zeigt das entstehende Phänomen, wenn wir ein Mutable-Object als als Default-Parameter in eine Funktionsdefinition aufnehmen:

def myFunction(x, default_param = [1,2,3]):
    default_param.append(x)
    print(default_param)

myFunction(x = 4)
>>> [1, 2, 3, 4]

myFunction(x = 4)
>>> [1, 2, 3, 4, 4]

myFunction(x = 4)
>>> [1, 2, 3, 4, 4, 4]

Unerfahrene Entwickler erwarten bei mehrmaligen Funktionsaufrufen von myFunction, dass diese bei gleichbleibenden Argumenten auch gleiche Ergebnisse hervorbringt. Tatsächlich allerdings wird die initiale Liste des Parameters default_param mit jedem Funktionsaufruf erweitert. Der Grund dafür ist der Umgang von Python mit Funktionsdefinitionen:

  1. Wird eine Funktion definiert, werden die Funktion als auch die Default-Parameter im Speicher abgelegt.
  2. Die Funktion wird in einer Session (sofern nicht explizit anders gehandhabt) einmalig definiert.
  3. Bei jedem Funktionsaufruf werden die bereits im Speicher existierenden Objekte als Default-Parameter verwendet – myFunction enthält beim ersten Aufruf die Liste [1, 2, 3] und erweitert diese um den Wert 4. Beim zweiten Aufruf von myFunction werden die Default-Parameter von myFunction nicht erneut evaluiert, sondern es werden die bestehenden Objekte für den Aufruf verwendert – also die Liste [1, 2, 3, 4] usw.

Um dieses Phänomen (oder manchmal auch Problematik) zu umgehen, kann folgende Syntax verwendet werden:

def myFunction(x, default_param = None):

    if(default_param is None):
        default_param = [1, 2, 3]

    default_param.append(4)
    print(default_param)

Durch diese Notation wird default_param bei der Funktionsdefinition mit None initiiert und anschließend bei jedem Funktionsaufruf neu erstellt. Damit ist sichergestellt, dass gleiche Argumente zu gleichen Rückgabewerten der Funktion führen.