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.

1 Speicherverwaltung und Garbage Collection

1.1 Speicherverwaltung

Python verwaltet den Speicher auf zwei Ebenen:

  1. Private Heap

    • Alles, was in Python anlegt wird (Variablen, Objekte, Listen usw.), wird im sogenannten private heap gespeichert.
    • Man hat keinen direkten Zugriff auf diesen Heap, sondern nur über Python-Objekte.
  2. Speicherallokatoren (Memory Manager)

    • Python hat einen internen Mechanismus, der entscheidet, wann wie viel Speicher angefordert wird.
    • Objekte kleiner als 512 Bytes werden z. B. in einem speziellen Arena-basierten System verwaltet (über das Modul pymalloc).

1.2 Garbage Collection

Garbage Collection bedeutet, dass nicht mehr benötigte Objekte automatisch aus dem Speicher entfernt werden.

1.2.1 Referenzzählung

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

1.2.2 Zyklische Garbage Collection

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

1.3 Tools & Module

  • gc: Garbage-Collector-Modul
  • sys.getrefcount(obj): Anzahl der Referenzen auf ein Objekt
  • id(obj): Gibt die Speicheradresse des Objekts zurück
  • hex(id(obj)): Speicher Adresse als HEX

1.4 Zusammenfassung

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

2 Variablen und Referenzen

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'}

2.1 Referenzen in Funktionen

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

2.2 is vs. ==

  • == prüft Gleichheit des Inhalts
  • is prüft, ob zwei Variablen auf dasselbe Objekt zeigen
a = [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

2.3 Objektidentität bei Mutability

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

2.4 Namen und Gültigkeit (Scope)

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'

2.5 Zusammenfassung

  • Variablen sind Namen, keine Container.
  • Zuweisung erzeugt Referenzen, keine Kopien.
  • Mutable Objekte können über mehrere Referenzen verändert werden.
  • Unveränderliche Objekte bleiben von außen unbeeinflusst.
  • is prüft Identität, == prüft Gleichheit.

3 Mutability von Datentypen

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.

mutable vs immutable

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.

Mutability prüfen mit 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)

3.1 Beispiele für immutable Typen

  • int
  • float
  • str
  • tuple
  • frozenset
  • bool
x = 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'

3.2 Beispiele für mutable Typen

  • list
  • dict
  • set
  • bytearray
  • user-defined classes (standardmäßig)
items = [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'}

3.3 Typen in Klassenattributen

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']

3.4 Auswirkungen auf Funktionsparameter

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

3.5 Zusammenfassung

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.

4 In-Place Operations und Shallow bzw. Deep Copy

4.1 In-Place Operations

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

4.2 Shallow Copy (flache Kopie)

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.
  • Nur die erste Ebene wird kopiert.

4.3 Deep Copy (tiefe Kopie)

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.
  • Änderungen wirken sich nicht auf das Original aus.

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}]
Kopieren mit Slicing oder Konstruktor

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]

4.4 Wann welche Kopie verwenden?

Situation Empfehlung
Nur äußere Struktur kopieren copy.copy()
Vollständig unabhängig kopieren copy.deepcopy()
Performance wichtig, Inhalt flach list(), Slicing ([:])

4.5 Zusammenfassung

  • In-Place Operationen ändern das Objekt direkt.
  • Shallow Copies duplizieren nur die oberste Ebene.
  • Deep Copies duplizieren rekursiv die komplette Struktur.
  • Vorsicht bei verschachtelten Strukturen – dort macht deepcopy den Unterschied.