R

ReflexKR

Reflex-Dokumentation

Der Etiketten-Flughafen: Pythons Objektmodell

·

Kursüberblick

1

Das Etikett ist nicht der Koffer: Namen und Objekte

2

Versiegelte Koffer: veränderlich und unveränderlich

3

Koffer im Koffer: Container und Kopien

4

Wenn ein Koffer übergeben wird: Funktionen und Argumente

5

Zonen und Ausweise: Gültigkeitsbereiche

6

Handgepäck: Closures

7

Der Koffer-Bauplan: Klassen und Instanzen

8

Flughafen-Standardverfahren: Dunder-Methoden

9

Der Bauplan hinter dem Bauplan: Vererbung und MRO

10

Die Kontrolle: das Geheimnis des Attributzugriffs

11

Herrenlose Koffer: Speicher und GC

12

Schließfachnummern: Hash, Gleichheit und Gesamtbild

Der Etiketten-Flughafen: Pythons Objektmodell
Lektion 11

Zeit 8:20–9:10

Herrenlose Koffer: Speicher und GC

Verlustmeldung

LOST-011

Die Geisterkoffer auf Band 13

Gemeldet von

Yoon, nächtliche Anlageninspektion

Was fehlt

Zwei Koffer ohne ein einziges Etikett, die trotzdem nicht entsorgt werden. Seit elf Tagen drehen sie ihre Runden auf Band 13.

Erledigt, wenn

Klären, warum die beiden Koffer nicht entsorgt werden, und sie so wegräumen, dass so etwas nicht wieder entsteht.

Internationaler Flughafen Haneulseom · Gepäckzentrum

Die beiden Koffer auf Band 13, über die seit dem ersten Tag berichtet wird, drehen auch heute ihre Runden. Ein Etikett hat keiner von beiden. Nach Vorschrift müsste ein Koffer ohne Etikett noch am selben Tag zur Entsorgung.

Schichtleiterin Do Miran breitet drei alte Aufzeichnungen auf dem Tisch aus: das Röntgenbild aus Lektion 3, die Notiz aus Lektion 6 und den partner-Eintrag aus Lektion 7. „Heute klären wir, was es mit den beiden auf sich hat. Vorher müssen wir die Entsorgungsregeln genau kennen.“

11-1. Etiketten zählen: Referenzzählung (9 min)

Jedes Objekt in CPython trägt eine Zahl, die festhält, wie viele Referenzen auf es zeigen. Das ist die Referenzzählung (reference count). Kommt ein Etikett dazu, steigt sie um 1, fällt eins weg, sinkt sie um 1, und in dem Moment, in dem sie 0 erreicht, wird das Objekt auf der Stelle entsorgt.

import sys

a = []
print(sys.getrefcount(a))
# 2
b = a
print(sys.getrefcount(a))
# 3
c = [a, a]
print(sys.getrefcount(a))
# 5
del b, c
print(sys.getrefcount(a))
# 2
  • Der Anfangswert ist 2 statt 1, weil auch die Referenz mitgezählt wird, die beim Aufruf von sys.getrefcount(a) als Argument übergeben wird.
  • c = [a, a] steckt zwei Etiketten in die Liste, also steigt die Zahl um 2. Auch ein Etikett, das in einem Koffer steckt, ist ein Etikett.
  • Mit del b, c fällt der Name b weg, und weil die Liste c entsorgt wird, verschwinden auch die beiden Etiketten darin.

Verlass dich nicht auf die Zahl selbst. Seit 3.14 überspringt der Interpreter als Optimierung manche Erhöhungen und Verringerungen. Führt man denselben Code in einer Funktion aus, ist der Anfangswert in CPython 3.14 gleich 1, in 3.13 gleich 2. Worauf es ankommt, ist der Unterschied: „ein Etikett, eins mehr“.

Dass bei 0 sofort entsorgt wird, lässt sich mit __del__ beobachten. Diese Dunder-Methode wird aufgerufen, kurz bevor ein Objekt entsorgt wird (warum man ihr nicht trauen sollte, steht in 11-4).

class Bag:
    def __init__(self, label):
        self.label = label
    def __del__(self):
        print("discard", self.label)

a = Bag("A")
b = a
del a
print("after del a")
del b
print("after del b")
# after del a
# discard A
# after del b

def check_in():
    tmp = Bag("T")
    print("inside")

check_in()
print("back")
# inside
# discard T
# back
  • Nach del a hängt noch das Etikett b, also wird nichts entsorgt. Erst beim del b, das das letzte Etikett abnimmt, erscheint sofort „discard A“.
  • Endet eine Funktion, verschwindet ihr Aufrufrahmen und mit ihm der lokale Name tmp. Deshalb wird noch vor „back“ entsorgt.
  • Das meinte die Schichtleiterin in Lektion 6: Hält die Zelle einer Closure die Sachen einer beendeten Funktion fest, erreicht der Referenzzähler nie 0, und sie bleiben erhalten.

Der GC-Beobachtungsstand unten erzeugt in einem Arbeitsprozess echte Objekte und lässt echtes gc laufen. An jedem Koffer steht seine „Refs“-Zahl (Namensschilder + Etiketten in anderen Koffern). Koffer, die man von keinem Namensschild aus über die Kofferinhalte erreichen kann, werden rot gestrichelt gezeichnet. Ein gepunktetes Etikett zeigt mit w() → …, worauf es gerade zeigt.

GC-Beobachtungsstand

Python

Bei tags sinkt die Zahl der Refs mit jedem abgenommenen Etikett um 1, und beim Schritt, der sie auf 0 bringt, wird entsorgt. Im 4. Schritt von chain hat b kein Namensschild mehr, steckt aber in a und hat deshalb noch 1 Ref. Es wird zusammen mit a entsorgt.

Pin sagt

→ Die Zahl von sys.getrefcount hängt von der Version und vom Ort ab (Modul oder Funktion). Achte nicht auf den absoluten Wert, sondern auf den Unterschied von 1, wenn du ein Etikett anhängst oder abnimmst.

11-2. Zwei Koffer, die einander enthalten: Referenzzyklen und GC (12 min)

Legt man die drei Aufzeichnungen nebeneinander, zeigt sich, was los ist. Das [[...]] auf dem Röntgenbild aus Lektion 3 war ein Koffer, der sich selbst enthält. Laut Lektion 7 zeigten die beiden Koffer über das Attribut partner aufeinander. In Code übertragen sieht das so aus:

Etikettentafel

Schritt 1 / 0

Als Nächstes läuft: 0

Labor wird vorbereitet…

Gerade geändert

Versiegelter Koffer (unveränderlich)

Nach Zeile 9 zeigen die beiden Koffer über partner aufeinander. Dass sie nach Zeile 10 von der Tafel verschwinden, liegt daran, dass die Tafel nur Koffer zeichnet, die von einem Namen aus erreichbar sind.

Mit del a, b sind alle Namensschilder abgenommen. In A steckt aber noch ein Etikett, das auf B zeigt, und in B eines, das auf A zeigt. Beide haben den Referenzzähler 1, und der wird nie 0. Das ist ein Referenzzyklus (reference cycle).

Objekte, die von keinem Namen aus erreichbar sind, kann das Programm nie wieder benutzen. Sie sind Müll. Deshalb sind sie auch von der Etikettentafel verschwunden. Mit der Referenzzählung allein lässt sich dieser Müll aber nicht wegräumen.

Deshalb hat CPython einen zweiten Reinigungsdienst: den zyklischen Garbage Collector (Modul gc).

  • Er kümmert sich um Objekte, die andere Objekte enthalten können, etwa Listen, dicts und Instanzen.
  • In einer solchen Gruppe zählt er die Referenzen, ohne die gegenseitigen Referenzen innerhalb der Gruppe mitzuzählen. Eine Gruppe ohne eine einzige Referenz von außen hält sich nur noch gegenseitig fest und ist Müll.
  • Solche Gruppen entsorgt er als Ganzes. gc.collect() lässt ihn das sofort tun und gibt die Zahl der weggeräumten Objekte zurück.
import gc
gc.disable()   # automatischen GC kurz abschalten, damit das Beispiel stabil bleibt

class Bag:
    def __init__(self, label):
        self.label = label
        self.partner = None
    def __del__(self):
        print("discard", self.label)

a = Bag("A")
b = Bag("B")
a.partner = b
b.partner = a
del a, b
print("tags gone")
print(gc.collect() > 0)
gc.enable()
# tags gone
# discard A
# discard B
# True
  • Nach del a, b wird gar nichts entsorgt. „discard“ erscheint erst innerhalb von gc.collect(). Die Reihenfolge der beiden Zeilen ist nicht festgelegt.
  • Im Alltag musst du gc.collect() nicht aufrufen. Wenn viele Objekte entstehen, startet der Interpreter ihn von selbst. gc.disable() dient hier nur dem Beispiel.
  • Wann und in wie vielen Stufen der automatische GC läuft, ist von Version zu Version verschieden und hat sich sogar innerhalb von 3.14 schon geändert. Schreib keinen Code, der sich auf einen bestimmten Zeitpunkt oder bestimmte Zahlen verlässt.

Die Liste aus Lektion 3, die sich selbst enthält, ist derselbe Fall:

import gc

a = []
a.append(a)
print(a, a[0] is a)
# [[...]] True
del a
print(gc.collect())
# z. B.: 23

Die zurückgegebene Zahl zählt auch anderen Müll mit, der sich bis dahin angesammelt hat, und hängt deshalb von der Situation und der Version ab. Auf diesem Rechner kam bei derselben Datei mit 3.13.9 eine 1 heraus, mit 3.14.2 eine 23. Der Beobachtungsstand unten räumt beim Start einmal auf und zählt erst danach, deshalb kommt dort jedes Mal derselbe Wert heraus.

GC-Beobachtungsstand

Python

Bei cycle bleibt nach dem Abnehmen aller Namensschilder je 1 Ref übrig, und gc.collect() räumt insgesamt 4 Objekte weg: die 2 Koffer und die 2 Listen darin. Bei break wird der Zyklus vorher aufgebrochen, und alles wird ohne gc entsorgt (gc.collect() ergibt 0). self ist das [[...]] aus Lektion 3.

Zyklen werden am Ende also doch weggeräumt. Ist das kein Problem? Zwei Dinge sind anders:

  • Die Referenzzählung entsorgt in genau der Zeile, in der der Zähler 0 wird. Der zyklische GC räumt später alles auf einmal weg. Bis dahin belegen die Objekte weiter Speicher.
  • Hält ein Objekt im Zyklus eine Datei oder eine Verbindung, wird auch diese erst geschlossen, wenn der GC läuft.

Einen Zyklus beseitigt man auf zwei Arten: Man nimmt nach Gebrauch ein Etikett auf einer Seite selbst ab (wie bei break), oder man macht eine Seite von Anfang an zu einer schwachen Referenz.

11-3. Gepunktete Etiketten: schwache Referenzen (8 min)

Eine schwache Referenz (weak reference) ist ein gepunktetes Etikett, das nicht in den Referenzzähler eingeht. Es zeigt auf einen Koffer, hält ihn aber nicht fest. Wird der Koffer entsorgt, leert sich das gepunktete Etikett von selbst.

import weakref

class Bag:
    def __init__(self, label):
        self.label = label

a = Bag("A")
w = weakref.ref(a)
print(w() is a)
# True
del a
print(w())
# None
  • Den Koffer bekommst du erst durch den Aufruf, also mit w(). Ist er schon entsorgt, kommt None heraus, also prüfst du das vor jeder Verwendung.
  • Nicht an jedes Objekt lässt sich ein gepunktetes Etikett hängen. weakref.ref([1, 2]) ergibt TypeError: cannot create weak reference to 'list' object. Mit int, str, tuple und dict ist es genauso, mit Instanzen eigener Klassen funktioniert es.

GC-Beobachtungsstand

Python

Das gepunktete Etikett w zählt nicht zu den Refs. Bei weak wird der Koffer in dem Moment entsorgt, in dem das letzte Namensschild abgenommen wird. Bei weak+cycle räumt gc.collect() den Zyklus weg, obwohl ein gepunktetes Etikett daran hängt.

Am häufigsten werden schwache Referenzen für Caches benutzt. Baut man einen Cache mit einem normalen dict, hält er die abgelegten Objekte für immer fest, und der Speicher schrumpft nie. Ein WeakValueDictionary hält seine Werte mit gepunkteten Etiketten. Werden sie anderswo nicht mehr benutzt, verschwinden die Einträge von selbst.

import weakref

class Bag:
    def __init__(self, label):
        self.label = label

desk = weakref.WeakValueDictionary()
c = Bag("C")
desk["C"] = c
print("C" in desk, len(desk))
# True 1
del c
print("C" in desk, len(desk))
# False 0

Es gibt auch WeakKeyDictionary, das die Schlüssel gepunktet hält, und WeakSet, das die Elemente gepunktet hält. Man nimmt sie, wenn man einem Objekt zusätzliche Informationen anhängen will, ohne seine Lebensdauer zu verlängern.

Soll etwas passieren, wenn ein Koffer entsorgt wird, hinterlegt man es mit weakref.finalize.

import weakref

class Bag:
    def __init__(self, label):
        self.label = label

d = Bag("D")
f = weakref.finalize(d, print, "discard D")
print(f.alive)
# True
del d
# discard D
print(f.alive)
# False

finalize wird höchstens einmal aufgerufen, und lebt der Koffer bis zum Programmende, wird es beim Beenden aufgerufen. Es ist zuverlässiger als das __del__ aus dem nächsten Abschnitt. Aber Vorsicht: Halten die übergebene Funktion oder ihre Argumente den Koffer selbst fest (etwa wenn man d.close übergibt), wird der Koffer nie entsorgt.

11-4. __del__ nicht trauen: aufräumen mit with (6 min)

__del__ scheint ein guter Ort für Aufräumcode zu sein, ist aber aus drei Gründen unzuverlässig.

  1. Man weiß nicht, wann es aufgerufen wird. Steckt das Objekt in einem Zyklus, wird es aufgeschoben, bis der GC läuft. In anderen Python-Implementierungen ohne Referenzzählung, etwa PyPy, kann es noch später kommen.
  2. Ausnahmen darin dringen nicht nach außen. Sie werden einmal auf die Standardfehlerausgabe geschrieben und dann ignoriert.
  3. Für Objekte, die beim Programmende noch leben, ist der Aufruf von __del__ nicht garantiert.
class Jam:
    def __del__(self):
        raise RuntimeError("belt jam")

x = Jam()
del x
print("still running")
# (stderr) z. B.: Exception ignored while calling deallocator <function Jam.__del__ at 0x...>:
# (stderr) ... RuntimeError: belt jam
# still running

Ressourcen, die unbedingt geschlossen werden müssen, etwa Dateien, Sockets oder Sperren, schließt du mit with. Sobald der Block verlassen wird, wird __exit__ aufgerufen, selbst wenn eine Ausnahme auftritt (Lektion 8).

class Belt:
    def __enter__(self):
        print("belt on")
        return self
    def __exit__(self, *exc):
        print("belt off")
        return False

with Belt() as belt:
    print("moving")
# belt on
# moving
# belt off

try:
    with Belt():
        raise ValueError("stuck")
except ValueError as e:
    print("caught", e)
# belt on
# belt off
# caught stuck

Auch die Datei, die open() zurückgibt, benutzt man als with open(path) as f:. So steht der Zeitpunkt des Aufräumens sichtbar im Code.

Pin sagt

→ __del__ verspricht kein „Wann“. Was unbedingt geschlossen werden muss, schließt du mit with. Aufräumarbeiten, die an die Lebensdauer eines Objekts gebunden sind, erledigst du mit weakref.finalize.

11-5. Unsterbliche Objekte, das Gewicht eines Koffers und 3.14t (5 min)

Objekte, die überall benutzt werden, wie None, True oder kleine Ganzzahlen, sind unsterbliche Objekte (3.12, PEP 683). Ihr Referenzzähler ist fest, und so viele Etiketten man auch anhängt oder abnimmt, sie werden nie entsorgt.

import sys

print(sys.getrefcount(None))
# 3221225472
x = None
print(sys.getrefcount(None))
# 3221225472
print(sys._is_immortal(None), sys._is_immortal([]))
# True False
  • Die Zahl ist ein Markierungswert für „unsterblich“ und unterscheidet sich von Version zu Version. In CPython 3.14 ist sie 3221225472, in 3.13 war sie 4294967295.
  • sys._is_immortal gibt es seit 3.14. Der Name beginnt mit einem Unterstrich, es ist also ein Implementierungsdetail von CPython.

sys.getsizeof misst nur das Gewicht des Koffers selbst. Den Inhalt zählt es nicht mit.

import sys

print(sys.getsizeof([]), sys.getsizeof([1, 2, 3]))
# 56 88
big = [0] * 1000
print(sys.getsizeof(big), sys.getsizeof([big]))
# 8056 64

Unter 64-Bit-CPython besteht eine Liste aus einem leeren Rumpf von 56 Byte plus 8 Byte für jeden Platz, in dem ein Etikett stecken kann. Je nach Erzeugungsart wird großzügig Platz reserviert, deshalb hat [1, 2, 3] nicht 80, sondern 88 Byte. [big] ist ein Koffer mit einem einzigen Platz und hat 64 Byte. Dass das big darin 8056 Byte hat, wird nicht mitgezählt.

Ein Wort zum Free-Threading-Build (3.14t). In einem Build ohne GIL ändern mehrere Threads gleichzeitig Referenzzähler. Deshalb zählt der Thread, der ein Objekt erzeugt hat, seinen Anteil schnell, während andere Threads einen gemeinsamen Anteil separat und atomar zählen (biased reference counting). Die sichtbaren Regeln sind dieselben. Mit 3.14.3t (sys._is_gil_enabled() ergibt False) verhielten sich die Beispiele dieser Lektion genauso, von den Werten des Referenzzählers über das sofortige Entsorgen bis dazu, dass gc.collect() die Zyklen wegräumt. Anders war nur die Zahl, die gc.collect() zurückgab.

Fall abgeschlossen (10 min)

Die beiden Koffer waren ein Referenzzyklus: Über ihr partner hielten sie einander fest. Sie hatten kein Etikett, doch ihr Referenzzähler blieb bei je 1. Deshalb wurden sie nicht entsorgt und blieben auf dem Band, bis der zyklische GC lief. Als die Schichtleiterin gc.collect() laufen lässt, ist Band 13 leer.

Damit so etwas nicht wieder entsteht, merkt sich jeder Koffer seinen Partner mit einem gepunkteten Etikett. Verpackt in ein property aus Lektion 10, bleibt der Code beim Benutzen unverändert.

import weakref

class Bag:
    def __init__(self, label):
        self.label = label
        self._partner = None

    @property
    def partner(self):
        return self._partner() if self._partner is not None else None

    @partner.setter
    def partner(self, other):
        self._partner = weakref.ref(other)   # den Partner mit einem gepunkteten Etikett merken

a = Bag("A")
b = Bag("B")
a.partner = b
b.partner = a
print(a.partner.label, b.partner.label)
# B A
watch = weakref.ref(a)
del a
print(watch() is None, b.partner)
# True None

Mit der einen Zeile del a wird A sofort entsorgt, und das partner von B wird zu None. Einen Zyklus, auf den der GC warten müsste, gibt es von vornherein nicht.

Kurz vor Feierabend bringt Yoon noch eine Zeile aus den Aufzeichnungen. Geisterkoffer B lag letzten Monat schon einmal im Fundsachen-Schließfach, und damals konnte ihn niemand herausholen. Die Schichtleiterin legt den Schließfachschlüssel auf den Tisch: „Das sehen wir uns morgen zum Schluss an.“

Abschlussbericht — LOST-011. Die beiden Geisterkoffer waren ein Referenzzyklus, sie hielten einander über partner fest. Mit gc.collect() weggeräumt. Gegen einen Rückfall merkt sich jeder Koffer seinen Partner jetzt mit weakref.ref.

Übungen

  1. Sag voraus, was sys.getrefcount(a) nach a = [] und a.append(a) ergibt. Was kommt heraus, wenn du es im Modul ausführst, und warum ist es nicht 2?
  2. Leg einen String als Wert in ein weakref.WeakValueDictionary(). Welcher Fehler kommt, und was ändert sich, wenn du ihn in eine Instanz einer eigenen Klasse verpackst?
  3. Schreib eine Klasse, die eine Datei geöffnet hält und sie in __del__ schließt, und bau sie dann so um, dass sie mit __enter__ und __exit__ schließt. Prüfe, ob die Datei auch dann geschlossen wird, wenn im Block eine Ausnahme auftritt.

Das Notizbuch der Schichtleiterin

  • Referenzzähler = Zahl der Etiketten an einem Koffer. In der Zeile, in der er 0 wird, wird sofort entsorgt.

  • sys.getrefcount zählt auch die als Argument übergebene Referenz. Die Werte hängen von der Version ab, achte nur auf den Unterschied.

  • Koffer, die einander enthalten (Referenzzyklus), erreichen nie 0. Der zyklische GC räumt sie später weg. gc.collect() gibt die Zahl der weggeräumten Objekte zurück.

  • Zeitpunkt und Generationen des automatischen GC hängen von der Version ab. Darauf verlässt man sich nicht.

  • Eine schwache Referenz ist ein gepunktetes Etikett. Für Caches WeakValueDictionary, für Aufgaben beim Entsorgen weakref.finalize.

  • __del__ traut man nicht. Ressourcen, die geschlossen werden müssen, schließt man mit with.

  • Unsterbliche Objekte wie None werden nie entsorgt. getsizeof misst nur den Rumpf des Koffers.

← Zurück

Lektion 10 · Die Kontrolle: das Geheimnis des Attributzugriffs

Weiter →

Lektion 12 · Schließfachnummern: Hash, Gleichheit und Gesamtbild

Über neue Lektionen informiert werden

Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.

Erhoben: deine E-Mail-Adresse · Zweck: Hinweise auf neue Lektionen oder den Start dieses Kurses · Speicherdauer: bis du die Löschung verlangst. Keine andere Nutzung, keine Weitergabe. Antworte einfach auf eine Benachrichtigung, dann löschen wir sie sofort.

Fragen & Antworten

Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.

Wird als reiner Text angezeigt, ohne Links oder Formatierung.

R

ReflexKR

Python und Reflex zum Anfassen. Alle Beispiele wurden mit Reflex 0.10.0 ausgeführt und geprüft.

ReflexKR ist kostenlos und ohne Werbebanner. Wenn es dir geholfen hat, kannst du es mit einem Kaffee unterstützen. ☕ Spendier uns einen Kaffee

Hinweis zu Affiliate-Links
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex