PCPP1 Block 1 (Teil 1a): Reflexion, Vergleich, Hash und Rechenoperatoren

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

Worum es geht

Block 1 der PCPP1 (Fortgeschrittene OOP, Advanced OOP) ist laut Quelle der größte Block: 35 % der Punkte, 15 Fragen. Dieser Teil deckt die Reflexion (reflection, Ziel 1.1) und die erste Hälfte der Spezialmethoden (magic methods, auch Dunder-Methoden, Ziel 1.2) ab: Vergleich, Hash und Rechenoperatoren. Damit sorgst du dafür, dass eigene Klassen auf ==, +, * und sum() reagieren wie eingebaute Typen. Das ist die Grundlage für jede saubere Python-Bibliothek und für viele Prüfungsfragen.

Die Lektion 07 hat die Grundbegriffe, super(), MRO, __repr__, __eq__ und NotImplemented schon eingeführt. Hier vertiefen wir: Hash-Regeln, total_ordering, rechte Operanden, sum() und +=.

Teil 1b behandelt Wahrheitswert, Iteration, __getattr__ gegen __getattribute__, Vererbung, Komposition und Polymorphismus. Bewusst weggelassen (kommt später oder ist nur Randthema): __format__, __index__, __bytes__, __setattr__/__delattr__ im Detail, __instancecheck__/__subclasscheck__ (gehören in Metaklassen), __enter__/__exit__ (Kontextmanager, siehe Lektion 09), Dekoratoren (Ziel 1.4) und alles ab 1.5.

Von JS/TS her gedacht

Thema JS/TS Python
Operator überladen gar nicht möglich (a + b auf Objekten ergibt Unsinn oder String) __add__, __sub__, __mul__, …
String-Darstellung toString() __str__ (Anwender), __repr__ (Entwickler)
Gleichheit === prüft Identität, eigene Logik nur per Methode == ruft __eq__, is prüft Identität
Als Map-Schlüssel nutzbar nur Primitive oder dieselbe Referenz (Map) __hash__ zusammen mit __eq__
Typprüfung instanceof isinstance(obj, K), issubclass(A, B)

Der größte Denkunterschied: In JS ist + für eigene Objekte nicht definierbar. In Python ist a + b nur Zuckerguss für a.__add__(b), und wenn das nicht klappt, probiert Python b.__radd__(a).

Konzept in kleinen Schritten

Schritt 1: Reflexion mit isinstance und issubclass (Ziel 1.1)

Reflexion (reflection) heißt: Ein Programm befragt Objekte und Klassen über sich selbst. Laut Quelle geht es hier um zwei Funktionen:

  • isinstance(obj, K): Ist obj eine Instanz von K oder einer Unterklasse?
  • issubclass(A, B): Ist die Klasse A eine Unterklasse von B? Jede Klasse ist Unterklasse von sich selbst.
  • Beide akzeptieren als zweites Argument ein Tupel von Klassen: “irgendeine davon”.

Die letzte Zeile ist eine Klassikerfalle: bool ist eine Unterklasse von int. Darum ist True eine Instanz von int, aber type(True) is int ist False.

Wichtig für die Prüfung: Das erste Argument von isinstance ist ein Objekt, das erste Argument von issubclass eine Klasse. Vertauscht gibt es einen TypeError.

Schritt 2: Vergleich, __eq__ und __hash__ (Ziel 1.2)

Wenn du __eq__ selbst schreibst, setzt Python __hash__ auf None. Die Instanzen sind dann nicht mehr hashbar (hashable): Du kannst sie weder in ein set legen noch als dict-Schlüssel nutzen. Der Grund: Zwei Objekte, die gleich sind, müssen denselben Hashwert haben. Python kann das nicht für dich entscheiden.

Regel: Was == gleich nennt, braucht denselben hash().

Weitere Regeln aus der Quelle:

  • __ne__ (!=) wird in Python 3 automatisch aus __eq__ abgeleitet (siehe oben: p != Punkt(1, 2) ist False).
  • __lt__, __le__, __gt__, __ge__ werden nicht abgeleitet. Ohne sie gibt < einen TypeError. Abkürzung: functools.total_ordering ergänzt die übrigen, wenn du __eq__ und eine Ordnungsmethode (z. B. __lt__) lieferst.

Schritt 3: Rechenoperatoren, rechte Seite, NotImplemented (Ziel 1.2)

a + b ruft zuerst a.__add__(b). Gibt das NotImplemented zurück (ein Rückgabewert, kein raise), probiert Python b.__radd__(a). Erst wenn beide NotImplemented liefern, gibt es den TypeError. Das ist der Mechanismus hinter 5 + obj (__radd__) und 5 * obj (__rmul__).

Die eingebaute sum() startet mit dem Startwert 0 und rechnet 0 + erstes + zweites + .... Ohne __radd__, das die 0 akzeptiert, scheitert sum bei eigenen Klassen.

Und +=? Gibt es __iadd__ (in-place), wird es genutzt und darf das Objekt verändern. Ohne __iadd__ fällt Python auf __add__ zurück und bindet den Namen neu.

Falle: Prüfungs- und Praxisklassiker

  1. __eq__ ohne __hash__: Instanzen sind unhashable (TypeError bei set und dict-Schlüssel). Und: gleiche Objekte brauchen gleichen Hash. 5 == 5.0 ist True, darum ist hash(5) == hash(5.0).
  2. __radd__ fehlt: sum([...]) bricht ab, weil 0 + obj nicht definiert ist. Umgekehrt gilt: Wer in __radd__ alles akzeptiert, macht 5 + obj plötzlich erlaubt.
  3. NotImplemented ist kein Fehler: return NotImplemented, nicht raise NotImplemented und nicht return None. None wäre ein gültiges (falsches) Ergebnis.
  4. isinstance(True, int) ist True, type(True) is int ist False. Und issubclass(h, K) mit einer Instanz ist ein TypeError.

Übungen

Übung 1: Reflexion vorhersagen

class Tier: pass
class Hund(Tier): pass
class Welpe(Hund): pass
w = Welpe()

Trage das Tupel der sechs Ausdrücke ein:

(isinstance(w, (str, Tier)),
 issubclass(Tier, Welpe),
 issubclass(Hund, (Welpe, Hund)),
 isinstance(Welpe, Tier),
 type(w) is Hund,
 isinstance(False, (str, int)))

Gehe jeden Ausdruck einzeln durch. Frage dich bei jedem: Ist das erste Argument ein Objekt oder eine Klasse? In welche Richtung läuft die Vererbung? Und was ist Welpe selbst, als Objekt betrachtet?

antwort = (True, False, True, False, False, True)
antwort

Übung 2: Hashbar machen

Die Klasse Preis hat ein __eq__, aber keine Hash-Funktion. Ergänze die fehlende Methode so, dass:

  • Preis-Objekte in set und als dict-Schlüssel funktionieren,
  • zwei gleiche Preise im set nur einmal vorkommen. Beachte: Preis("EUR", 5) == Preis("EUR", 5.0) ist True,
  • == und != unverändert bleiben.

Der Hashwert muss aus genau den Daten entstehen, die auch __eq__ vergleicht, und zwar so, dass 5 und 5.0 zum selben Wert führen. Welche eingebaute Funktion kombiniert mehrere Werte zu einem Hash?

class Preis:
    def __init__(self, waehrung, betrag):
        self.waehrung, self.betrag = waehrung, betrag

    def __eq__(self, other):
        if not isinstance(other, Preis):
            return NotImplemented
        return (self.waehrung, self.betrag) == (other.waehrung, other.betrag)

    def __hash__(self):
        return hash((self.waehrung, self.betrag))

Preis

Übung 3: Rechnen mit eigener Klasse und sum()

Schreibe die Operatormethoden für Strecke (Länge in Metern, m):

  • Strecke + Strecke ergibt eine neue Strecke mit der Summe.
  • sum([Strecke(1), Strecke(2)]) funktioniert. Links darf nur die 0 (Startwert von sum) stehen: 5 + Strecke(1) bleibt ein TypeError.
  • Strecke * ganze_Zahl und ganze_Zahl * Strecke ergeben eine neue Strecke.
  • Mit fremden Typen (Strecke(1) + "x", Strecke(1) * "a") kommt ein TypeError, kein AttributeError und kein stilles Ergebnis.

Für den TypeError musst du nichts selbst auslösen: Wenn deine Methode bei fremden Typen den richtigen Rückgabewert liefert, entscheidet Python. Überlege, welche Zahl sum zuerst addiert und welche Methode dafür gebraucht wird, und welche Eingaben diese Methode nicht akzeptieren darf.

class Strecke:
    def __init__(self, m):
        self.m = m

    def __repr__(self):
        return f"Strecke({self.m})"

    def __add__(self, other):
        if not isinstance(other, Strecke):
            return NotImplemented
        return Strecke(self.m + other.m)

    def __radd__(self, other):
        if other == 0:
            return self
        return NotImplemented

    def __mul__(self, k):
        if not isinstance(k, int):
            return NotImplemented
        return Strecke(self.m * k)

    __rmul__ = __mul__

Strecke

Merksatz und Prüfstein

Merksatz: Gleich heißt gleicher Hash, und NotImplemented ist ein Rückgabewert, der dem anderen Operanden die Chance gibt.

Prüfstein (offene Frage): Eine Klasse definiert __eq__ und __lt__, aber kein __hash__ und kein __le__. Was passiert bei {obj}, bei obj <= obj2 und bei sorted([obj2, obj])? Wie reparierst du jeden der drei Fälle mit möglichst wenig Code?

Weiter geht es mit Teil 1b: Container, Attributzugriff, Vererbung und Komposition.

Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “PCPP1 Block 1: Fortgeschrittene OOP” mit “1.1 Grundbegriffe und Reflexion” und “1.2 Spezialmethoden (Magic Methods)”. Nicht in der Quelle, aber mit Python 3.13 ausgeführt und geprüft: das Verhalten von += mit und ohne __iadd__, sum() mit __radd__ und dem Startwert 0, functools.total_ordering (nur in der Quelle als Hinweis genannt), die Fehlermeldungen der TypeError und die Unterklassenbeziehung von bool und int (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).