Details zu Objekten, Variablen, und Mutability#

Veränderliche Datentypen können nach ihrer Erstellung geändert werden, während unveränderliche Datentypen nicht geändert werden können.

  • unveränderlich (immutable): Integer, Float, String, Boolean, Tupel

  • veränderlich (mutable): Listen, Mengen, Wörterbücher

In Python unterscheidet man zwischen

  • Objekten

    • mit einem Datentyp und Wert

    • z.B. 3, "Hello", [1, 2, 3]

  • Variablen

    • Namen, die auf Objekte verweisen

    • Man sagt auch, dass Variablen auf Objekte “zeigen” oder “verweisen”

    • Die Variable ist eine Referenz auf das Objekt

Man sollte sich vorstellen, dass eine Variable wie ein Namensschild ist, das an ein Objekt angehängt wird. Das Objekt selbst liegt irgendwo im Speicher des Computers.

Mutability und Zuweisung#

Bei der Zuweisung

a = 3
a = 4

wird das Namensschild a zuerst an das Objekt 3 gehängt und dann an das Objekt 4. Das Objekt 3 bleibt unverändert im Speicher, aber a verweist jetzt auf das Objekt 4. Python verwaltet den Speicher automatisch und entfernt ungenutzte Objekte, wenn keine Variablen mehr darauf verweisen.

Etwas komplizierter wird es bei veränderlichen Datentypen wie Listen:

my_list = [1, 2, 3]
my_list[0] = 4

Hier zeigt das Namensschild my_list auf eine Liste im Speicher. Wenn wir my_list[0] = 4 ausführen, ändern wir das Objekt, auf das my_list zeigt, indem wir das erste Element der Liste von 1 auf 4 ändern. Das Namensschild my_list bleibt jedoch auf dem selben Listenobjekt im Speicher. Wenn wir allerdings

my_list = [4, 5, 6]

ausführen, wird das Namensschild my_list einem neuen Listenobjekt [4, 5, 6] im Speicher umgehängt. Das ursprüngliche Listenobjekt [4, 2, 3] bleibt unverändert im Speicher (und wird eventuell gelöscht), aber my_list verweist jetzt auf das neue Objekt.

id#

Die Funktion id() gibt die eindeutige Identifikationsnummer eines Objekts zurück, die dessen Speicheradresse repräsentiert. Diese ID bleibt während der Lebensdauer des Objekts konstant. Wenn eine Variable auf ein neues Objekt zeigt, ändert sich die ID der Variable, da sie jetzt auf ein anderes Objekt verweist. Dies eignet sich gut, um zu verstehen, wie Variablen und Objekte in Python zusammenhängen. Im folgenden Beispiel sieht man, dass sich die ID des Objekts, auf das x und y zeigen nicht ändert wenn die Liste verlängert wird (Listen sind veränderlich). Die ID von y ändert sich aber, wenn y auf eine neue Liste zeigt (obwohl die Liste die selben Elemente enthält).

x = [1, 2, 3]
y = x
print("id(x):", id(x))
print("id(y):", id(y))

y.append(4)
print("After modifying y:")
print("x:", x)
print("y:", y)
print("id(x):", id(x))
print("id(y):", id(y))

y = [1, 2, 3, 4]
print("x:", x)
print("y:", y)
print("id(x):", id(x))
print("id(y):", id(y))
id(x): 4522401536
id(y): 4522401536
After modifying y:
x: [1, 2, 3, 4]
y: [1, 2, 3, 4]
id(x): 4522401536
id(y): 4522401536
x: [1, 2, 3, 4]
y: [1, 2, 3, 4]
id(x): 4522401536
id(y): 4522402880

Kleine Zahlen werden in Python oft zwischengespeichert, daher haben sie manchmal die gleiche ID, auch wenn sie unterschiedliche Variablen sind.

a = 3
b = 3
print("id(a):", id(a))
print("id(b):", id(b))
a = 1200
b = 1200
print("id(a):", id(a))
print("id(b):", id(b))
id(a): 4377575728
id(b): 4377575728
id(a): 4521874384
id(b): 4521873936

Der is Operator prüft, ob zwei Variablen auf dasselbe Objekt im Speicher verweisen. Es wird einfach die IDs der beiden Variablen verglichen. Das heißt:

a is b

ist äquivalent zu

id(a) == id(b)

Zuweisung, Shallow Copy, Deep Copy#

Es können auch mehrere Variablen auf dasselbe Objekt verweisen. Ändert man eine Variable die auf ein veränderliches Objekt zeigt, so ändert sich auch der Wert der anderen Variablen, die auf dasselbe Objekt zeigen. Das Objekt [1, 2, 3] wird nur einmal im Speicher abgelegt und beide Variablen a und b verweisen auf dasselbe Objekt. Das Objekt hat also zwei verschiedene Namen.

a = [1, 2, 3]
b = a
a.append(4)
print("a:",a)
print("b:",b)
a: [1, 2, 3, 4]
b: [1, 2, 3, 4]

Will man eine echte Kopie eines veränderlichen Objekts erstellen, so dass zwei verschiedene Objekte im Speicher existieren, die unabhängig voneinander verändert werden können, so spricht man von Shallow Copy. Dies kann man in Python mit dem Modul copy erreichen:

import copy
a = [1, 2, 3]
b = copy.copy(a)
a.append(4)
print("a:",a)
print("b:",b)
a: [1, 2, 3, 4]
b: [1, 2, 3]

Alternativ kann man auch die Methode list.copy() oder einfach Slicing verwenden

a = [1, 2, 3]
b = a.copy()
c = a[:]
a.append(4)
print("a:",a)
print("b:",b)
print("c:",c)
a: [1, 2, 3, 4]
b: [1, 2, 3]
c: [1, 2, 3]

Falls das veränderliche Objekt selbst wieder veränderliche Objekte enthält (z.B. eine Liste von Listen), so wird bei einer Shallow Copy nur die äußere Ebene kopiert. Die inneren Objekte werden weiterhin von beiden Kopien gemeinsam genutzt.

import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
a[2].append(5)
a.append(6)
print("a:",a)
print("b:",b)
a: [1, 2, [3, 4, 5], 6]
b: [1, 2, [3, 4, 5]]

Will man eine völlig unabhängige Kopie eines veränderlichen Objekts erstellen, einschließlich aller darin enthaltenen veränderlichen Objekte, so spricht man von Deep Copy

import copy
a = [1, 2, [3, 4]]
b = copy.deepcopy(a)
a[2].append(5)
a.append(6)
print("a:",a)
print("b:",b)
a: [1, 2, [3, 4, 5], 6]
b: [1, 2, [3, 4]]

Mutability und Funktionen#

Ein wichtiger Aspekt von Variablen ist ihr Verhalten in Funktionen. Beim Funktionsaufruf wird die Variable als Argument übergeben. Das heißt, es wird nur die Referenz (der Name) auf eine lokale Variable kopiert, nicht das eigentliche Objekt.

Dieses Vorgehen impliziert, dass veränderliche Objekte (wie Listen) innerhalb der Funktion verändert werden können, und diese Änderungen auch außerhalb der Funktion sichtbar sind. Unveränderliche Objekte (wie Integer oder Strings) können innerhalb der Funktion nicht verändert werden, da jede Änderung ein neues Objekt erzeugt. Nur die lokale Variable innerhalb der Funktion wird aktualisiert und zeigt auf das neue Objekt.

def change_list(input_list):
    input_list[0] = 99

my_list = [1, 2, 3]
print("my_list before function call:", my_list)
change_list(my_list)
print("my_list after function call:", my_list)

def change_integer(input_int):
    input_int = 99

my_int = 1
print("my_int before function call:", my_int)
change_integer(my_int)
print("my_int after function call:", my_int)
my_list before function call: [1, 2, 3]
my_list after function call: [99, 2, 3]
my_int before function call: 1
my_int after function call: 1

Beispiel: Zuweisung von Listen#

Achtung: Auch bei veränderlichen Objekten führt eine Zuweisung eines neuen Objekts innerhalb der Funktion dazu, dass die lokale Variable auf das neue Objekt zeigt, ohne das ursprüngliche Objekt zu verändern (wie oben bei ‘Mutability und Zuweisung’ beschrieben).

def change_list(input_list):
    input_list = [99, 100]

my_list = [1, 2, 3]
print("my_list before function call:", my_list)
change_list(my_list)
print("my_list after function call:", my_list)
my_list before function call: [1, 2, 3]
my_list after function call: [1, 2, 3]

Achtung: Die genauen Regeln, wann ein neues Objekt erstellt wird, sind manchmal subtil und hängen vom Datentyp und der Operation ab. Es ist wichtig, diese Konzepte zu verstehen, um unerwartetes Verhalten in Programmen zu vermeiden.

Siehe folgendes Beispiel:

def duplicate_list(input_list):
    input_list += input_list

def duplicate_list2(input_list):
    input_list = input_list + input_list

my_list = [1, 2, 3]
print("my_list before function calls:", my_list)
duplicate_list2(my_list)
print("my_list after duplicate_list2 call:", my_list)
duplicate_list(my_list)
print("my_list after duplicate_list call:", my_list)
my_list before function calls: [1, 2, 3]
my_list after duplicate_list2 call: [1, 2, 3]
my_list after duplicate_list call: [1, 2, 3, 1, 2, 3]

Obwohl a+=b und a = a + b in vielen Fällen das gleiche Ergebnis liefern, gibt es hier Unterschiede im Verhalten bei veränderlichen Datentypen.

  • a += b modifiziert das Objekt, auf das a zeigt, direkt (sofern a veränderlich ist). Das bedeutet, dass das ursprüngliche Objekt verändert wird.

  • a = a + b erstellt ein neues Objekt und weist diese neue Objekt der Variable a zu. Das ursprüngliche Objekt bleibt unverändert.

Mutability sollte in Funktionen nicht ausgenutzt werden#

Es ist eine gute Praxis, Funktionen so zu gestalten, dass sie keine Seiteneffekte haben, d.h. sie sollten keine veränderlichen Argumente modifizieren. Stattdessen sollten Funktionen neue Objekte zurückgeben, wenn Änderungen erforderlich sind. Dies macht den Code vorhersehbarer und leichter zu verstehen.

Folgendes Beispiel zeigt zwei Versionen einer Funktion, die eine Liste der Länge 2 aufsteigend sortiert. Die erste Version modifiziert die übergebene Liste, die zweite Version gibt eine neue sortierte Liste zurück.

def sort_list1(input_list):
    if input_list[0] > input_list[1]:
        input_list[0], input_list[1] = input_list[1], input_list[0]

def sort_list2(input_list):
    if input_list[0] > input_list[1]:
        return [input_list[1], input_list[0]]
    else:
        return input_list
    

my_list = [2, 1]
sort_list1(my_list)
print("my_list after sort_list1 call:", my_list)

my_list = [2, 1]
my_list = sort_list2(my_list)
print("my_list after sort_list2 call:", my_list)
my_list after sort_list1 call: [1, 2]
my_list after sort_list2 call: [1, 2]

Negativbeispiel#

Eine Funktion soll das Minimum einer Liste zurückgeben. Die Funktion get_min bewerkstelligt dies, indem sie die Liste sortiert und dann das erste Element zurückgibt. Da Listen veränderlich sind, wird die ursprüngliche Liste außerhalb der Funktion ebenfalls verändert. Die Funktion hat also einen Seiteneffekt (das Sortieren der Liste), welcher unerwartet sein kann und später möglicherweise zu Fehlern führt.

def get_min(input_list):
    input_list.sort()
    return input_list[0]

student_grades = [2, 1, 4, 5, 3]
print("Grade on third exam:", student_grades[2])
print("Best grade:", get_min(student_grades))
print("Grade on third exam:", student_grades[2])
Grade on third exam: 4
Best grade: 1
Grade on third exam: 3

Besser ist es, die eingebaute Funktion min zu verwenden, die das Minimum einer Liste zurückgibt, ohne die ursprüngliche Liste zu verändern.

def get_min(input_list):
    return min(input_list)
student_grades = [2, 1, 4, 5, 3]
print("Grade on third exam:", student_grades[2])
print("Best grade:", get_min(student_grades))
print("Grade on third exam:", student_grades[2])
Grade on third exam: 4
Best grade: 1
Grade on third exam: 4

Wenn man die Funktion min nicht kennt, so könnte man auch eine Kopie der Liste erstellen, diese sortieren und dann das erste Element zurückgeben. So wird die ursprüngliche Liste nicht verändert.

import copy
def get_min(input_list):
    tmp = copy.copy(input_list)
    tmp.sort()
    return tmp[0]
student_grades = [2, 1, 4, 5, 3]
print("Grade on third exam:", student_grades[2])
print("Best grade:", get_min(student_grades))
print("Grade on third exam:", student_grades[2])
Grade on third exam: 4
Best grade: 1
Grade on third exam: 4