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
Zeit 7:30–8:20
Verlustmeldung
LOST-010
Gemeldet von
Oh, Wartung der Waagensysteme
Was fehlt
Die Aufzeichnungen der Waage. Obwohl eine Gewichtsprüfung eingebaut ist, steht darin ein Koffer mit -10 kg.
Erledigt, wenn
Den Weg finden, auf dem die Prüfung umgangen wurde, und dafür sorgen, dass jeder Weg durch die Kontrolle führt.
Internationaler Flughafen Haneulseom · Gepäckzentrum
Oh hat Bag so gebaut, dass beim Erzeugen das Gewicht geprüft wird, damit keine negativen Gewichte vorkommen. In den Aufzeichnungen steht trotzdem ein Koffer mit -10 kg. Schichtleiterin Do Miran blättert durch das Zugangsprotokoll. „Am Haupteingang steht eine Kontrolle, aber der Koffer kam durch die Seitentür.“
Heute sehen wir uns die Kontrolle an, durch die Python geht, wenn obj.x gelesen oder geschrieben wird. Das ist zugleich das vollständige Bild der Leseregel, die wir in Lektion 7 als „erst die Instanz, dann die Klasse“ kennengelernt haben.
@property: eine Kontrolle am Attribut (9 min)Das ist der Code aus der Meldung. Geprüft wird nur in __init__.
class Bag:
def __init__(self, kg):
if kg < 0:
raise ValueError("negative weight")
self.kg = kg
b = Bag(20)
b.kg -= 30
print(b.kg)
# -10Beim Erzeugen ging es durch die Prüfung, aber das spätere b.kg -= 30 schreibt den Wert direkt ins __dict__ der Instanz. Das ist die Seitentür. Soll die Prüfung am Schreiben des Attributs selbst hängen, nimmt man @property.
class Bag:
def __init__(self, kg):
self.kg = kg
@property
def kg(self):
return self._kg
@kg.setter
def kg(self, value):
if value < 0:
raise ValueError(f"negative weight: {value}")
self._kg = value
b = Bag(20)
print(b.kg, vars(b))
# 20 {'_kg': 20}
b.kg -= 30
# ValueError: negative weight: -10b.kg wird die erste Funktion kg aufgerufen, beim Schreiben mit b.kg = v die Funktion unter @kg.setter. Von außen benutzt man es wie ein ganz normales Attribut.self.kg = kg in __init__ geht durch den Setter. Die Prüfung muss also nur an einer Stelle stehen._kg. Der Name kg ist ein property-Objekt, und zwar nicht in der Instanz, sondern in der Klasse.Ohne Setter wird das Attribut schreibgeschützt. Das passt für Werte, die aus anderen Attributen berechnet werden.
class Box:
def __init__(self, w, h):
self.w, self.h = w, h
@property
def area(self):
return self.w * self.h
x = Box(3, 4)
print(x.area, type(Box.__dict__["area"]).__name__)
# 12 property
x.area = 5
# AttributeError: property 'area' of 'Box' object has no setterPin sagt
→ Ein einzelner Unterstrich wie bei _kg ist nur eine Abmachung („bitte nicht anfassen“), kein Schloss. b._kg = -5 kannst du nicht verhindern. Die Kontrolle schützt den regulären Weg (b.kg = …).
Der Koffer aus der Meldung hat nicht nur ein Gewicht (kg), sondern auch eine Länge (cm). Kopiert man für jedes Attribut ein property, vervielfacht sich der Prüfcode. Stattdessen kann man eine einzige Kontrolle bauen und vor mehrere Attribute stellen. Das ist ein Deskriptor.
Ein Deskriptor ist ein Objekt mit mindestens einer der Methoden __get__ und __set__ (oder __delete__). Steht er als Klassenattribut in der Klasse, ruft Python diese Methoden auf, sobald der Name in einer Instanz gelesen oder geschrieben wird.
class Positive:
def __set_name__(self, owner, name):
print(f"set_name: {owner.__name__}.{name}")
self.name = name
def __get__(self, obj, owner=None):
if obj is None:
return self
return obj.__dict__[self.name]
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} must be >= 0: {value}")
obj.__dict__[self.name] = value
class Bag:
kg = Positive()
cm = Positive()
def __init__(self, kg, cm):
self.kg, self.cm = kg, cm
# set_name: Bag.kg
# set_name: Bag.cm
b = Bag(20, 55)
print(b.kg, b.cm, vars(b))
# 20 55 {'kg': 20, 'cm': 55}
print(type(Bag.kg).__name__)
# Positive
b.cm = -1
# ValueError: cm must be >= 0: -1| Wann | Was Python aufruft |
|---|---|
| wenn die Klasse fertig gebaut ist | Positive.__set_name__(kontrolle, Bag, "kg") |
beim Lesen von b.kg | Positive.__get__(kontrolle, b, Bag) |
beim Lesen von Bag.kg | Positive.__get__(kontrolle, None, Bag) |
beim Schreiben von b.kg = 20 | Positive.__set__(kontrolle, b, 20) |
__set_name__ (3.6) teilt der Kontrolle in dem Moment, in dem die class-Anweisung endet, den Namen mit, unter dem sie hängt. Deshalb kann dasselbe Positive sowohl vor kg als auch vor cm stehen.__dict__ des jeweiligen Koffers."kg" im __dict__ steht, geht das Lesen nicht über __dict__, sondern durch die Kontrolle. Um diese Reihenfolge geht es in 10-4.@property ist ein Deskriptor, den Python fertig mitbringt.Etikettentafel
Schritt 1 / 0
Als Nächstes läuft: 0
Labor wird vorbereitet…
Gerade geändert
Versiegelter Koffer (unveränderlich)
In Lektion 7 haben wir gesehen, dass bag.m is bag.m False ergibt. Der Grund: Auch Funktionen sind Deskriptoren. Eine Funktion hat nur __get__ und kein __set__, sie ist also ein Nicht-Daten-Deskriptor.
class Bag:
def weigh(self):
return 20
b = Bag()
f = Bag.__dict__["weigh"]
print(type(f).__name__, hasattr(f, "__get__"), hasattr(f, "__set__"))
# function True False
m = f.__get__(b, Bag)
print(type(m).__name__, m.__self__ is b, m())
# method True 20
print(b.weigh is b.weigh, b.weigh == b.weigh, Bag.weigh is f)
# False True TrueBei jedem Lesen von b.weigh ruft Python f.__get__(b, Bag) auf und erzeugt eine neue gebundene Methode: ein kleines Objekt, das die Funktion und b zusammenbindet. Deshalb ergibt is False und == True. Dass man b.weigh() ohne self aufrufen kann, verdankt man ebenfalls diesem Bündel. Liest man über die Klasse (Bag.weigh), gibt es keine Instanz zum Binden, und die Funktion kommt unverändert heraus.
staticmethod und classmethod sind Deskriptoren, deren __get__ etwas anderes bindet.
class Bag:
@staticmethod
def kg_to_lb(kg):
return round(kg * 2.2046, 1)
@classmethod
def empty(cls):
return cls()
class Suitcase(Bag):
pass
print(Bag.kg_to_lb(20), Bag().kg_to_lb(20))
# 44.1 44.1
print(type(Suitcase.empty()).__name__, Suitcase.empty.__self__ is Suitcase)
# Suitcase True
print(type(Bag.__dict__["empty"]).__name__)
# classmethodWas __get__ bindet | Erstes Argument | |
|---|---|---|
| normale Funktion | die Instanz | self |
staticmethod | nichts | keines |
classmethod | die Klasse (type(obj)) | cls |
classmethod bekommt die Klasse, über die aufgerufen wird, deshalb erzeugt Suitcase.empty() ein Suitcase. Man nutzt das für „alternative Konstruktoren“, die auch bei Vererbung den passenden Bauplan erzeugen.
Beim Lesen von obj.x schaut Python (genauer: object.__getattribute__) in dieser Reihenfolge nach:
| Reihenfolge | Wo nachgesehen wird | Beispiel |
|---|---|---|
| 1 | ein Daten-Deskriptor in der Klasse (MRO), also einer mit __set__ oder __delete__ | property, Positive |
| 2 | das __dict__ der Instanz | b.label = "ICN" |
| 3 | ein Nicht-Daten-Deskriptor in der Klasse (nur __get__) | Funktionen, staticmethod, classmethod |
| 4 | ein normales Attribut der Klasse | kind = "bag" |
| 5 | __getattr__ (falls vorhanden) | |
| 6 | AttributeError |
Daten-Deskriptoren stehen vor dem __dict__ der Instanz, deshalb wird die Kontrolle vor dem Wert im Koffer befragt. Darum war es kein Problem, dass Positive aus 10-2 den Wert unter demselben Namen ins __dict__ legt. Funktionen stehen dagegen hinter dem __dict__, und ein gleichnamiger Eintrag in der Instanz verdeckt die Methode.
Wähl im Ablaufdiagramm unten nacheinander die Attribute aus. Es zeigt, in welchem Schritt die Antwort kommt.
Kontrollablauf
1
class Positive:
2
def __set_name__(self, owner, name):
3
self.name = name
4
def __get__(self, obj, owner):
5
return obj.__dict__[self.name]
6
def __set__(self, obj, value):
7
obj.__dict__[self.name] = value
8
class Bag:
9
kg = Positive()
10
kind = "bag"
11
def weigh(self):
12
return self.kg
13
def __repr__(self):
14
return "Bag()"
15
b = Bag()
16
b.kg = 20
17
b.label = "ICN"
b.
Daten-Deskriptor in der Klasse (__get__ + __set__)?
Im __dict__ der Instanz?
Nicht-Daten-Deskriptor in der Klasse (nur __get__, wie eine Funktion)?
Ein normales Klassenattribut?
Gibt es __getattr__?
AttributeError
Ergebnis: b. →
Diesmal legen wir absichtlich gleichnamige Einträge ins __dict__ der Instanz. Die letzte Zeile ist die „Seitentür“: ein direkter Schreibzugriff auf __dict__, um die Kontrolle zu umgehen.
Kontrollablauf
1
class Bag:
2
kind = "bag"
3
def weigh(self):
4
return 20
5
@property
6
def tag(self):
7
return "T-" + self.kind
8
def __getattr__(self, name):
9
return f"<no {name}>"
10
b = Bag()
11
b.weigh = "shadowed"
12
b.kind = "suitcase"
13
b.__dict__["tag"] = "fake"
b.
Daten-Deskriptor in der Klasse (__get__ + __set__)?
Im __dict__ der Instanz?
Nicht-Daten-Deskriptor in der Klasse (nur __get__, wie eine Funktion)?
Ein normales Klassenattribut?
Gibt es __getattr__?
AttributeError
Ergebnis: b. →
property ohne Setter ist ein Daten-Deskriptor. property hat immer ein __set__, und ohne Setter löst es darin einen AttributeError aus. Deshalb lässt es sich nicht über __dict__ verdecken.b.x = v) ist der Weg kürzer. Gibt es in der Klasse einen Deskriptor mit __set__, wird er aufgerufen, sonst wird ins __dict__ der Instanz geschrieben.Pin sagt
→ In einer Zeile zum Merken: Daten-Deskriptor > __dict__ der Instanz > Nicht-Daten-Deskriptor wie eine Funktion > Klassenattribut > __getattr__.
__getattr__, __getattribute__ und type (5 min)Die beiden Griffe mit ähnlichem Namen werden zu verschiedenen Zeitpunkten aufgerufen.
__getattribute__: wird bei jedem Lesen eines Attributs immer aufgerufen. Die gesamte Reihenfolge aus 10-4 steckt in object.__getattribute__. Wer es selbst überschreibt, landet leicht in einer Endlosrekursion, deshalb tut man es fast nie.__getattr__: die letzte Rettung, die nur aufgerufen wird, wenn nichts gefunden wurde.class Loud:
def __init__(self):
self.kg = 20
def __getattribute__(self, name):
print("getattribute", name)
return super().__getattribute__(name)
def __getattr__(self, name):
print("getattr", name)
return None
x = Loud()
v = x.kg
# getattribute kg
v = x.color
# getattribute color
# getattr colorZum Schluss: Wenn auch Klassen Objekte sind, welchen Typ hat dann eine Klasse? type. Die class-Anweisung entspricht eigentlich einem Aufruf von type(name, bases, dict), also Name, Elternklassen und ein dict mit den Attributen.
class Bag:
kind = "bag"
print(type(Bag()).__name__, type(Bag), type(type))
# Bag <class 'type'> <class 'type'>
Dyn = type("Dyn", (Bag,), {"kind": "dyn"})
print(Dyn.kind, [c.__name__ for c in Dyn.__mro__])
# dyn ['Dyn', 'Bag', 'object']Erbt man von type, um den Vorgang der Klassenerzeugung selbst zu verändern, spricht man von einer Metaklasse. Für fast alles reichen __init_subclass__ (Lektion 9) und Deskriptoren, merk dir in diesem Kurs also nur den Namen.
Verlegt man die Prüfung in die Kontrolle Positive, geht alles durch dieselbe Kontrolle: das Erzeugen, das Ändern und das Verringern mit -=.
class Positive:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, owner=None):
return self if obj is None else obj.__dict__[self.name]
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} must be >= 0: {value}")
obj.__dict__[self.name] = value
class Bag:
kg = Positive()
cm = Positive()
def __init__(self, kg, cm):
self.kg, self.cm = kg, cm
b = Bag(20, 55)
for change in (lambda: setattr(b, "kg", -1), lambda: Bag(-3, 40)):
try:
change()
except ValueError as e:
print("blocked:", e)
# blocked: kg must be >= 0: -1
# blocked: kg must be >= 0: -3
b.kg -= 30
# ValueError: kg must be >= 0: -10Ob mit setattr, beim Neuerzeugen oder mit -=: Alles wird abgefangen. b.kg -= 30 besteht aus zwei Schritten, dem Lesen von b.kg (__get__) und dem Zurückschreiben des verringerten Werts (__set__), und kommt deshalb nicht an der Kontrolle vorbei.
Oh lässt die Waagenaufzeichnung noch einmal laufen. Negative Gewichte tauchen nicht mehr auf. Die Schichtleiterin sagt: „Besser als Türen zu verriegeln ist es, jeden Weg durch die Kontrolle zu führen.“
Abschlussbericht — LOST-010. Die Gewichtsprüfung stand nur in init, also umging das spätere b.kg -= 30 die Prüfung. Gelöst, indem die Prüfung in einen Daten-Deskriptor (Positive) verlegt wurde, sodass jeder Schreibzugriff durch die Kontrolle geht.
Positive so, dass es eine Untergrenze entgegennimmt (kg = AtLeast(0), cm = AtLeast(10)). Benutze dafür in der Prüfung den Wert, den __init__ bekommen hat.b.weigh = "x". In welchem Schritt kommt dann die Antwort für b.weigh? Und bei b.kg = "x"?print bei einer Klasse, die nur __getattr__ definiert: Wird __getattr__ aufgerufen, wenn du ein Attribut liest, das in __init__ gesetzt wurde?Das Notizbuch der Schichtleiterin
Mit @property hängt man eine Prüfung an das Lesen und Schreiben eines Attributs. Geprüft wird an einer Stelle, im Setter. Ohne Setter ist es schreibgeschützt.
Ein Deskriptor ist ein Klassenattribut mit __get__/__set__. Über __set_name__ kennt er seinen Namen und lässt sich für mehrere Attribute wiederverwenden.
Eine Funktion ist ein Nicht-Daten-Deskriptor mit nur __get__. Bei jedem Lesen von b.m entsteht eine neue gebundene Methode.
staticmethod bindet nichts, classmethod bindet die Klasse.
Lesereihenfolge: Daten-Deskriptor > __dict__ der Instanz > Nicht-Daten-Deskriptor > Klassenattribut > __getattr__.
__getattribute__ immer, __getattr__ nur, wenn nichts gefunden wurde. Die Klasse einer Klasse ist type.
← Zurück
Lektion 9 · Der Bauplan hinter dem Bauplan: Vererbung und MRO
Weiter →
Lektion 11 · Herrenlose Koffer: Speicher und GC
Über neue Lektionen informiert werden
Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.
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© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Fragen & Antworten
Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.