PCPP1 Block 1 (Teil 1b): Container, Attributzugriff, Vererbung und Komposition

Track Python · PCPP1 Block 1 (35 %, 15 Fragen laut Quelle) · Ziele 1.2 (zweite Hälfte) und 1.3 · ca. 40 Min.

Was du aus Teil a brauchst

Aus Teil 1a solltest du wissen, dass Python Operatoren auf Spezialmethoden abbildet (a + b ruft a.__add__(b)) und dass NotImplemented ein Rückgabewert ist. Hier machst du dasselbe mit Wahrheitswert, Iteration und Attributzugriff und schließt mit Vererbung gegen Komposition ab.

Worum es geht

Dieser Teil deckt die zweite Hälfte von Ziel 1.2 (Wahrheitswert, Container, Attributzugriff) und Ziel 1.3 (Vererbung, Polymorphismus, Komposition, inheritance, polymorphism, composition) von PCPP1 Block 1 ab (laut Quelle 35 % der Punkte, 15 Fragen). Mit __bool__, __len__, __getitem__ und __getattr__ lässt du eigene Klassen auf if obj:, len(), for, in und obj.name reagieren. Mit Komposition entscheidest du, wann eine Klasse etwas hat, statt etwas zu sein.

Von JS/TS her gedacht

Thema JS/TS Python
Wahrheitswert if (obj) ist bei Objekten immer wahr __bool__, sonst __len__, sonst wahr
Iterierbar [Symbol.iterator]() __iter__, ersatzweise __getitem__ mit Index 0, 1, 2, …
Fehlendes Attribut abfangen Proxy mit get-Trap __getattr__ (nur wenn nichts gefunden wird)
Jeden Zugriff abfangen Proxy mit get-Trap __getattribute__ (bei jedem Zugriff)
Strukturelles Typing TS prüft zur Compile-Zeit, ob die Form passt Duck Typing: zur Laufzeit zählt, ob die Methode da ist
Wiederverwendung extends, Mixins, Objekt als Feld Vererbung (class A(B)) oder Komposition (Attribut)

Der größte Denkunterschied: In JS ist ein Objekt in if (obj) immer wahr. In Python entscheidet die Klasse über __bool__ und __len__, und eine leere Box ist falsy. Den Proxy mit get-Trap gibt es in Python als zwei getrennte Haken: __getattr__ (nur Notfall) und __getattribute__ (immer).

Konzept in kleinen Schritten

Schritt 1: Wahrheitswert und Container (Ziel 1.2)

  • bool(obj): erst __bool__, fehlt es, dann __len__ (Länge 0 ist False), fehlt auch das, ist das Objekt wahr.
  • len(obj) braucht __len__.
  • for x in obj und list(obj): erst __iter__. Fehlt es, aber es gibt __getitem__, ruft Python obj[0], obj[1], … bis ein IndexError kommt.
  • x in obj: erst __contains__, sonst Iteration (wie oben).

Schritt 2: __getattr__ gegen __getattribute__ (Ziel 1.2)

  • __getattribute__(self, name) läuft bei jedem Attributzugriff.
  • __getattr__(self, name) läuft nur, wenn der normale Weg (Instanz, Klasse, Basisklassen) nichts gefunden hat. Fehlt dann auch dort etwas, muss die Methode AttributeError auslösen, sonst funktionieren hasattr und getattr(obj, name, default) nicht mehr.
  • In __getattribute__ (und __setattr__) musst du den eigentlichen Zugriff über super() oder object.__getattribute__ machen. Ein self.xyz darin ruft wieder __getattribute__: Endlosrekursion (infinite recursion, RecursionError).

Schritt 3: Vererbung, Komposition, Duck Typing, Polymorphismus (Ziel 1.3)

  • “ist ein” (is a) ist Vererbung: Hund(Tier). “hat ein” (has a) ist Komposition: Ein Auto hat einen Motor als Attribut.
  • Komposition vor Vererbung (composition over inheritance): Wenn die Beziehung nicht eindeutig “ist ein” heißt, ist das enthaltene Objekt flexibler. Du erbst sonst die gesamte Schnittstelle der Oberklasse, auch Methoden, die nicht passen.
  • Duck Typing: Es zählt, ob das Objekt die nötigen Methoden hat, nicht seine Klasse. Wie in TS bei strukturellem Typing, nur zur Laufzeit.
  • Polymorphismus (polymorphism): derselbe Aufruf, je nach Klasse anderes Verhalten.
  • Inkonsistente MRO: Wenn sich die Reihenfolge nicht widerspruchsfrei bilden lässt, gibt es schon beim class-Statement einen TypeError.
  • super() in Mehrfachvererbung ruft den nächsten in der MRO der Instanz auf. Rufen alle Klassen konsequent super().__init__() auf (kooperative Vererbung, cooperative inheritance), läuft jede Basisklasse genau einmal (Details in Lektion 07).

Falle: Prüfungs- und Praxisklassiker

  1. bool(obj) bei Containerklassen: Ohne __bool__ entscheidet __len__. Eine leere Box ist falsy.
  2. __getattr__ ist nicht __getattribute__: das eine nur im Notfall, das andere immer. Falsche Wahl ergibt überraschende Rangfolge oder Rekursion.
  3. Endlosrekursion: In __getattr__ auf ein Attribut zugreifen, das es noch nicht gibt (z. B. vor __init__, etwa beim Kopieren), ruft __getattr__ erneut auf.
  4. Vererbung nur zum Wiederverwenden: Wer von list erbt, weil append bequem ist, erbt auch sort und insert. Wenn nicht eindeutig “ist ein” gilt, nimm Komposition.
  5. Inkonsistente MRO: class C(A, B) mit B(A) ist ein TypeError schon bei der Klassendefinition, nicht erst beim Aufruf.

Übungen

Übung 1: Wahrheitswert und Iteration vorhersagen

class Reihe:
    def __getitem__(self, i):
        if i >= 4:
            raise IndexError
        return i * 10

class Stapel:
    def __init__(self, *e):
        self.e = list(e)
    def __len__(self):
        return len(self.e)

class Flagge(Stapel):
    def __bool__(self):
        return True

Trage das Tupel der sechs Werte ein:

(list(Reihe()), 20 in Reihe(), bool(Stapel()), bool(Flagge()), bool(Stapel(0)), len(Flagge(1, 2)))

Ohne __iter__ und __contains__ gibt es einen Ersatzweg über den Index. Bei bool(...) gilt eine feste Reihenfolge der Methoden, und bei Stapel(0) zählt die Länge, nicht der Inhalt.

antwort = ([0, 10, 20, 30], True, False, True, True, 2)
antwort

Übung 2: Attribute aus einem Dict liefern

Die Klasse Einstellungen speichert ihre Werte in self._werte. Schreibe __getattr__ so, dass e.farbe den Wert aus dem Dict liefert. Anforderungen:

  • Gibt es ein echtes Attribut (z. B. nach e.groesse = 99), gewinnt dieses vor dem Dict-Wert.
  • Fehlt der Name, kommt ein AttributeError (damit hasattr(e, "x") zu False wird und getattr(e, "x", "d") den Default "d" liefert).
  • copy.copy(e) und copy.deepcopy(e) funktionieren. Beim Kopieren wird das Objekt ohne __init__ erzeugt, _werte existiert dann zunächst nicht.

Zur Falle dahinter (das Modul copy lernst du in Lektion 13a genauer kennen): Liest __getattr__ selbst self._werte, und _werte fehlt, ruft Python wieder __getattr__ auf, für _werte, dann wieder, und so weiter bis zum RecursionError. Die Methode darf also nur auf Wege zugreifen, die __getattr__ nicht erneut auslösen.

__getattr__ läuft nur, wenn der normale Weg scheitert. Was passiert, wenn du darin self._werte liest und _werte gerade nicht existiert? Und wie kommst du an das Instanz-Dict, ohne erneut __getattr__ auszulösen?

class Einstellungen:
    def __init__(self, **werte):
        self._werte = werte

    def __getattr__(self, name):
        werte = self.__dict__.get("_werte", {})
        if name in werte:
            return werte[name]
        raise AttributeError(name)

Einstellungen

Übung 3: Vererbung oder Komposition?

Ein Warenkorb soll Artikel enthalten, den Gesamtpreis berechnen und mit for durchlaufbar sein. Er soll nicht alles können, was eine Liste kann (kein sort, kein insert an beliebiger Stelle). Welche Umsetzung ist die beste?

  • a) class Warenkorb(list): Die Basisklasse bringt append, len und Iteration schon mit.
  • b) class Warenkorb(Artikel): Der Korb erbt von Artikel, weil beide einen Preis haben.
  • c) Eine Liste als Attribut halten und __iter__ und __len__ selbst anbieten (Komposition).
  • d) Eine Funktion mit einer globalen Liste, weil Klassen für Container unnötig sind.

Trage den Buchstaben als String ein, z. B. "a".

Prüfe jede Option mit dem Satz “Ein Warenkorb ist ein …” beziehungsweise “Ein Warenkorb hat ein …”. Frage dich außerdem, welche Methoden der Nutzer des Korbs dann plötzlich aufrufen kann, und ob sich der Korb mehrfach sauber erzeugen lässt.

antwort = "c"
antwort

Merksatz und Prüfstein

Merksatz: Für bool gilt erst __bool__, dann __len__, ohne __iter__ zählt der Index, __getattr__ ist der Notnagel und __getattribute__ der Türsteher, und wenn es nicht eindeutig “ist ein” heißt, nimm Komposition.

Prüfstein (offene Frage): Eine Klasse Konfig soll fehlende Attribute aus einem Dict liefern und trotzdem kopierbar sein. Warum ist self._werte in __getattr__ gefährlich, und wie umgehst du das? Und wann würdest du statt class Korb(list) lieber eine Liste als Attribut halten?

Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “PCPP1 Block 1: Fortgeschrittene OOP” mit “1.2 Spezialmethoden (Magic Methods)” und “1.3 Vererbung, Polymorphismus, Komposition”. Nicht in der Quelle, aber mit Python 3.13 ausgeführt und geprüft: die Reihenfolge __bool__, __len__, die Iteration über __getitem__, die Rekursion in __getattr__ beim Kopieren und die Fehlermeldung bei inkonsistenter MRO (bitte prüfen, ob sie in der PCPP1 so geprüft werden). Die Aussage “15 Fragen” und “35 %” ist die Angabe der Quelle (bitte prüfen).