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 10

Zeit 7:30–8:20

Die Kontrolle: das Geheimnis des Attributzugriffs

Verlustmeldung

LOST-010

Ein Koffer mit negativem Gewicht

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.

10-1. @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)
# -10

Beim 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: -10
  • Beim Lesen von b.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.
  • Auch self.kg = kg in __init__ geht durch den Setter. Die Prüfung muss also nur an einer Stelle stehen.
  • Der eigentliche Wert liegt in _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 setter

Pin 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 = …).

10-2. Deskriptoren: die Kontrolle im Bauplan (10 min)

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
WannWas Python aufruft
wenn die Klasse fertig gebaut istPositive.__set_name__(kontrolle, Bag, "kg")
beim Lesen von b.kgPositive.__get__(kontrolle, b, Bag)
beim Lesen von Bag.kgPositive.__get__(kontrolle, None, Bag)
beim Schreiben von b.kg = 20Positive.__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.
  • Das Kontrollobjekt gibt es nur einmal in der Klasse, und alle Koffer teilen es sich. Deshalb wird der Wert nicht in der Kontrolle notiert, sondern im __dict__ des jeweiligen Koffers.
  • Obwohl der Wert unter demselben Namen "kg" im __dict__ steht, geht das Lesen nicht über __dict__, sondern durch die Kontrolle. Um diese Reihenfolge geht es in 10-4.
  • Auch @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)

kg und cm im Kasten der Klasse Bag sind Positive-Kontrollen, die sich jeweils ihren name merken. In der letzten Zeile landet der Wert von kg im __dict__ des Koffers, und cm = -1 bleibt an der Kontrolle hängen.

10-3. Wie aus einer Funktion eine Methode wird (7 min)

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 True

Bei 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__)
# classmethod
Was __get__ bindetErstes Argument
normale Funktiondie Instanzself
staticmethodnichtskeines
classmethoddie 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.

10-4. Die Reihenfolge an der Kontrolle (9 min)

Beim Lesen von obj.x schaut Python (genauer: object.__getattribute__) in dieser Reihenfolge nach:

ReihenfolgeWo nachgesehen wirdBeispiel
1ein Daten-Deskriptor in der Klasse (MRO), also einer mit __set__ oder __delete__property, Positive
2das __dict__ der Instanzb.label = "ICN"
3ein Nicht-Daten-Deskriptor in der Klasse (nur __get__)Funktionen, staticmethod, classmethod
4ein normales Attribut der Klassekind = "bag"
5__getattr__ (falls vorhanden)
6AttributeError

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.

1

Daten-Deskriptor in der Klasse (__get__ + __set__)?

2

Im __dict__ der Instanz?

3

Nicht-Daten-Deskriptor in der Klasse (nur __get__, wie eine Funktion)?

4

Ein normales Klassenattribut?

5

Gibt es __getattr__?

6

AttributeError

Ergebnis: b. →

kg steht auch im __dict__, aber die Antwort kommt schon in Schritt 1 (Daten-Deskriptor). label kommt in Schritt 2, weigh in Schritt 3 (Funktion), kind in Schritt 4, und color gibt es nirgends, also AttributeError.

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.

1

Daten-Deskriptor in der Klasse (__get__ + __set__)?

2

Im __dict__ der Instanz?

3

Nicht-Daten-Deskriptor in der Klasse (nur __get__, wie eine Funktion)?

4

Ein normales Klassenattribut?

5

Gibt es __getattr__?

6

AttributeError

Ergebnis: b. →

Bei tag gewinnt das property (Schritt 1), obwohl im __dict__ 'fake' steht. Bei weigh und kind verdeckt das __dict__ (Schritt 2) die Funktion und das Klassenattribut. color gibt es nirgends, also antwortet __getattr__ (Schritt 5).
  • Auch ein 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.
  • Beim Schreiben (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__.

10-5. __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 color

Zum 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.

Fall abgeschlossen (10 min)

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: -10

Ob 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.

Übungen

  1. Ändere 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.
  2. Ergänze den Code des ersten Ablaufdiagramms aus 10-4 um b.weigh = "x". In welchem Schritt kommt dann die Antwort für b.weigh? Und bei b.kg = "x"?
  3. Prüfe mit 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.

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