14 Speicherverhalten, Referenzen und Mutability
Die folgenden Konzepte sind zentral für das Verständnis von Speicherverhalten und Referenzen. Dieses ist wichtig, um Seiteneffekte und unerwartetes Verhalten zu vermeiden.
Python verwaltet den Speicher auf zwei Ebenen:
Private Heap
Speicherallokatoren (Memory Manager)
pymalloc).Garbage Collection bedeutet, dass nicht mehr benötigte Objekte automatisch aus dem Speicher entfernt werden.
Das ist die primäre Methode der Speicherfreigabe:
import sys
x = [] # Liste wird erstellt
print(sys.getrefcount(x)) # 2
y = x # Jetzt gibt es 2 Referenzen
print(sys.getrefcount(x)) # 3
del x # Eine Referenz weniger
print(sys.getrefcount(y)) # 2
Referenzzählung reicht nicht aus, wenn zwei Objekte sich gegenseitig referenzieren:
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b
b.ref = a # Zyklus
del a
del b # → Referenzzähler wird nicht null
Daher hat Python ein zusätzliches Garbage Collector-Modul, das Zyklen erkennen und auflösen kann:
import gc
gc.collect() # Startet manuelle Garbage Collection
gc: Garbage-Collector-Modulsys.getrefcount(obj): Anzahl der Referenzen auf ein Objektid(obj): Gibt die Speicheradresse des Objekts zurückhex(id(obj)): Speicher Adresse als HEX| Feature | Beschreibung |
|---|---|
| Heap | Speicherort aller Objekte |
| Referenzzählung | Basis-Mechanismus zur Speicherfreigabe |
| Zyklische GC | Entfernt Objektzyklen, die Referenzzähler nicht abfängt |
gc-Modul |
Ermöglicht Kontrolle über die Garbage Collection |
In Python sind Variablen keine Container, sondern Namen, die auf Objekte im Speicher verweisen. Das Verständnis von Variablenbindung und Referenzen ist essenziell, um Seiteneffekte und unerwartetes Verhalten zu vermeiden.
Variablen binden Namen an Objekte:
Eine Variable in Python ist ein Name, der auf ein Objekt zeigt. Es wird keine Kopie erzeugt, sondern eine Referenz.
a = [1, 2, 3]
b = a # b referenziert dasselbe Objekt wie a
a.append(4)
print(b) # [1, 2, 3, 4] -> b wurde mit verändert
Referenzen und id():
Jedes Objekt hat eine eindeutige ID (Speicheradresse), die mit id() abgefragt werden kann.
x = 'hello'
y = x
print(id(x) == id(y)) # True -> beide zeigen auf dasselbe Objekt
x = x + ' world' # neues Objekt
print(id(x) == id(y)) # False -> x zeigt jetzt auf etwas anderes
Zuweisung erzeugt keine Kopie:
data = {'key': 'value'}
copy = data # keine Kopie, nur neue Referenz
copy['key'] = 'new value'
print(data) # {'key': 'new value'}
Wird ein Objekt an eine Funktion übergeben, wird nur eine Referenz übergeben – keine Kopie.
def change_list(lst):
lst.append(42)
numbers = [1, 2, 3]
change_list(numbers)
print(numbers) # [1, 2, 3, 42]
Bei unveränderlichen Objekten (z. B. int, str) wirkt sich das nicht aus:
def change_value(x):
x = x + 1 # neue Referenz innerhalb der Funktion
value = 10
change_value(value)
print(value) # 10
is vs. ==is prüft, ob zwei Variablen auf dasselbe Objekt zeigena = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a == c) # True -> Inhalt gleich
print(a is c) # False -> unterschiedliche Objekte
print(a is b) # True -> gleiche Referenz
Mutability beeinflusst das Verhalten bei Referenzen stark:
def reset_list(l):
l.clear() # verändert Originalobjekt
my_list = [9, 8, 7]
reset_list(my_list)
print(my_list) # []
Bei immutable Typen wie int passiert das nicht:
def reset_number(n):
n = 0 # neue Referenz, Original bleibt gleich
my_number = 123
reset_number(my_number)
print(my_number) # 123
Variablen sind innerhalb des jeweiligen Gültigkeitsbereichs (Scope) sichtbar. Eine Zuweisung innerhalb einer Funktion überschreibt nicht die äußere Variable.
name = 'outer'
def test():
name = 'inner'
print(name) # 'inner'
test()
print(name) # 'outer'
is prüft Identität, == prüft Gleichheit.In Python unterscheidet man zwischen veränderlichen (mutable) und unveränderlichen (immutable) Datentypen. Dieses Konzept ist zentral für das Verständnis von Speicherverhalten, Referenzen und Seiteneffekten.
Ein mutable Objekt kann nach seiner Erstellung verändert werden. Änderungen wirken sich auf alle Referenzen auf dieses Objekt aus.
Ein immutable Objekt kann nicht verändert werden. Jede Änderung erzeugt ein neues Objekt.
id()Die id()-Funktion zeigt die Speicheradresse eines Objekts. So erkennt man, ob eine Änderung ein neues Objekt erzeugt hat.
s = 'abc'
print(id(s)) # 4380616160
s += 'd'
print(id(s)) # 4934619072 (Neue ID)
intfloatstrtuplefrozensetboolx = 10
print(id(x)) # 4381074208
y = x
print(id(y)) # 4381074208
x = x + 1 # neues Objekt wird erstellt
print(id(x)) # 4381074240
print(x) # 11
print(y) # 10
a = 'hello'
print(id(a)) # 4843587312
b = a
print(id(b)) # 4843587312
a += ' world' # neuer String, neue Referenz
print(id(a)) # 4934764784
print(a) # 'hello world'
print(b) # 'hello'
listdictsetbytearrayitems = [1, 2, 3]
copy = items
items.append(4) # verändert das Objekt selbst
print(items) # [1, 2, 3, 4]
print(copy) # [1, 2, 3, 4] -> auch verändert
settings = {'theme': 'dark'}
ref = settings
settings['theme'] = 'light' # Änderung wirkt auf beide Referenzen
print(ref) # {'theme': 'light'}
Bei eigenen Klassen ist das Verhalten abhängig vom verwendeten Datentyp:
class User:
def __init__(self, name):
self.name = name # str ist immutable
self.tags = [] # list ist mutable
u1 = User('Alice')
u2 = u1
u1.tags.append('admin') # wirkt auch auf u2
print(u2.tags) # ['admin']
Mutable Objekte können in Funktionen verändert werden, was außerhalb der Funktion sichtbar ist.
def modify_list(lst):
lst.append(99) # verändert die übergebene Liste
nums = [1, 2, 3]
modify_list(nums)
print(nums) # [1, 2, 3, 99]
Bei immutable Objekten passiert das nicht:
def modify_number(n):
n += 1 # neues Objekt innerhalb der Funktion
x = 10
modify_number(x)
print(x) # 10
| Datentyp | Mutable |
|---|---|
int |
Nein |
str |
Nein |
bool |
Nein |
tuple |
Nein (aber Inhalt kann mutable sein) |
list |
Ja |
dict |
Ja |
set |
Ja |
user class |
Abhänigig von den verwendeten Datentypen |
Mutability beeinflusst, wie sich Daten in Speicher, Referenzen und Funktionen verhalten. Ein gutes Verständnis hilft, Bugs und unerwartetes Verhalten zu vermeiden.
In-Place Operationen ändern ein Objekt direkt, ohne eine neue Kopie zu erstellen. Das spart Speicher, kann aber zu Seiteneffekten führen, wenn mehrere Referenzen auf das gleiche Objekt existieren.
Listen verändern:
numbers = [1, 2, 3]
numbers.append(4) # verändert das Objekt selbst
print(numbers) # [1, 2, 3, 4]
+= bei Listen:
data = [1, 2]
other = data
data += [3, 4] # in-place Änderung
print(data) # [1, 2, 3, 4]
print(other) # [1, 2, 3, 4] -> auch verändert
+= kann auch eine neue Referenz erzeugen (bei immutable Typen)Bei immutable Typen wie int, str, tuple wird keine In-Place-Änderung durchgeführt.
Beispiel:
x = 10
y = x
x += 5 # erzeugt neues Objekt
print(x) # 15
print(y) # 10
Eine flache Kopie kopiert nur die äußere Struktur, nicht die enthaltenen Objekte.
Beispiel mit copy.copy():
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
shallow[0][0] = 99 # verändert auch 'original'
print(original) # [[99, 2], [3, 4]]
shallow und original teilen sich die inneren Listen.Eine tiefe Kopie erstellt eine vollständige Kopie aller Ebenen, rekursiv.
Beispiel mit copy.deepcopy():
import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 99
print(original) # [[1, 2], [3, 4]]
deep ist vollständig unabhängig von original.Vergleich Shallow vs. Deep Copy:
import copy
data = [{'a': 1}, {'b': 2}]
shallow = copy.copy(data)
deep = copy.deepcopy(data)
shallow[0]['a'] = 99
print(data) # [{'a': 99}, {'b': 2}]
print(deep) # [{'a': 1}, {'b': 2}]
Beim Kopieren durch Slicing oder die Verwendung eines Konstruktors werden nur flache Kopien erzeugt:
original = [1, 2, 3]
copy1 = original[:] # flache Kopie
copy2 = list(original) # ebenfalls flach
copy1.append(4)
print(original) # [1, 2, 3]
print(copy1) # [1, 2, 3, 4]
| Situation | Empfehlung |
|---|---|
| Nur äußere Struktur kopieren | copy.copy() |
| Vollständig unabhängig kopieren | copy.deepcopy() |
| Performance wichtig, Inhalt flach | list(), Slicing ([:]) |
deepcopy den Unterschied.