OOP-Grundlagen: Klassen, Vererbung, super(), MRO, Dunder-Methoden

Track Python · PCAP-Kern (OOP) · ca. 55 Min.

Worum es geht

OOP (Objektorientierte Programmierung, object-oriented programming) ist laut Quelle der größte Prüfungsblock der PCAP, grob ein Drittel der Punkte. Python-Klassen sehen aus wie TypeScript-Klassen, funktionieren aber anders: kein this, das du nicht hinschreibst, keine Prototypen, kein public/private. Dafür gibt es Mehrfachvererbung mit einer festen Suchreihenfolge (MRO) und Spezialmethoden (Dunder-Methoden, __x__), mit denen eigene Klassen wie eingebaute Typen reagieren. Nach der Lektion kannst du Klassenattribut-Fallen, MRO und super()-Ketten vorhersagen und eigene Klassen mit __repr__, __eq__ und Co. ausstatten.

Ablauf: erst die Übersetzung JS/TS nach Python, dann sieben kleine Schritte, dann fünf Übungen. Schwerpunkt sind die Prüfungsfallen.

Von JS/TS her gedacht

Thema JS/TS Python
Instanz im Methodenrumpf this (implizit) self, erster Parameter, explizit
Konstruktor constructor(...) __init__(self, ...) (initialisiert nur, Rückgabe None)
Basisklasse aufrufen super(...) im Konstruktor ist Pflicht super().__init__(...), nicht automatisch, wenn __init__ überschrieben wird
Statisches Feld static x = 1 Variable im Klassenkörper (Klassenvariable, class variable)
Getter/Setter get x(), set x(v) @property, @x.setter
static-Methode static f() @staticmethod (und @classmethod bekommt cls)
private x nur Compiler-Prüfung (#x echt privat) nur Konvention _x, oder Name Mangling __x
Mehrfachvererbung nicht möglich (nur Interfaces/Mixins) möglich, Suchreihenfolge heißt MRO
toString() toString() __str__ (und __repr__ für Entwickler)
abstract class abstract class A ABC und @abstractmethod aus dem Modul abc

Der Unterschied, der am meisten Fehler macht: In TS ist count = 0 im Klassenkörper ein Instanzfeld (jedes Objekt bekommt eine Kopie). In Python ist dieselbe Zeile eine Klassenvariable, die alle Instanzen teilen.

// TypeScript: jede Instanz hat ihr eigenes Feld
class Korb { artikel: string[] = []; }
const a = new Korb(), b = new Korb();
a.artikel.push("x");
console.log(b.artikel);  // []

In Python wäre artikel = [] im Klassenkörper dagegen für alle Körbe dieselbe Liste. Das siehst du gleich.

Konzept in kleinen Schritten

Schritt 1: Klasse, Instanz, self und die Attributsuche

  • __init__(self, ...) läuft direkt nach der Erzeugung und richtet die Instanzvariablen (instance variables) ein: self.name = ....
  • self ist der Name für die Instanz, die Position (erster Parameter) ist Pflicht, der Name nur Konvention.
  • Bei ada.gruss() übergibt Python ada automatisch als self (gebundene Methode, bound method). Person.gruss(ada) ist dasselbe.
  • Attributsuche (attribute lookup): erst die Instanz, dann die Klasse, dann die Basisklassen. Zuweisung über die Instanz legt immer ein Instanzattribut an, die Klassenvariable bleibt unverändert.

__dict__ zeigt die Instanzattribute: Die Klassenvariable art steht dort erst, nachdem du sie über die Instanz zugewiesen hast.

Schritt 2: Methodenarten und @property

  • @classmethod bekommt die Klasse als cls. Typisch: alternative Konstruktoren.
  • @staticmethod bekommt weder self noch cls. Nur eine Funktion im Namensraum der Klasse.
  • @property macht eine Methode zu einem lesbaren Attribut, mit @name.setter auch schreibbar.

Zwei Begriffe aus der Quelle gehören dazu. Duck Typing: Es zählt, ob ein Objekt die nötigen Methoden hat, nicht seine Klasse. Komposition (composition): Ein Objekt enthält andere Objekte, statt von ihnen zu erben.

Schritt 3: Vererbung und super()

Wenn die Unterklasse __init__ überschreibt (override), wird die Basisklasse nicht automatisch initialisiert. super().__init__(...) musst du selbst aufrufen.

Typprüfung (type checks):

  • isinstance(obj, Klasse) prüft die Klasse inklusive Oberklassen.
  • issubclass(A, B) prüft Klassenbeziehungen.
  • type(obj) liefert die exakte Klasse. Alle Klassen erben von object.

Schritt 4: Mehrfachvererbung und MRO

Python erlaubt mehrere Basisklassen. Die Suchreihenfolge heißt MRO (Method Resolution Order), sichtbar über Klasse.__mro__ oder Klasse.mro(). Die Regel laut Quelle: von links nach rechts durch die Basisklassen, und eine gemeinsame Basisklasse kommt erst nach allen ihren Unterklassen. Das ist nicht die Tiefensuche, die du dir aus JS-Prototypketten vorstellst.

Die Reihenfolge der Basisklassen in der Klammer entscheidet. Und super() bedeutet nicht “die Elternklasse”, sondern “der nächste in der MRO”. Das zeigt das Diamant-Beispiel (diamond problem):

Das super() in L ruft hier R auf, obwohl R gar nicht die Elternklasse von L ist. Basis läuft genau einmal. Wer in L und R stattdessen Basis.__init__(self) schriebe, würde Basis doppelt aufrufen und R umgehen.

Schritt 5: Sichtbarkeit und Name Mangling

  • _x: nur Konvention für “intern”, technisch normal zugreifbar.
  • __x (zwei Unterstriche vorn, keiner hinten): Name Mangling. Python benennt das Attribut intern in _Klasse__x um, damit Unterklassen es nicht versehentlich überschreiben.
  • Es gibt kein public/private wie in TypeScript.

Schritt 6: Dunder-Methoden

Mit Dunder-Methoden (double underscore) bestimmst du, wie dein Objekt auf Operatoren und eingebaute Funktionen reagiert. Die Tabelle steht in der Quelle (__init__, __str__, __repr__, __len__, Vergleiche, Rechenoperatoren, __getitem__, __iter__, __call__, __contains__, __bool__). Die wichtigsten Regeln:

  • __str__ ist für Anwender (print, str()), __repr__ für Entwickler (repr(), Anzeige in der Konsole und in Listen).
  • Definierst du __eq__ nicht, vergleicht == die Identität.
  • NotImplemented ist ein eigener Rückgabewert, weder ein Fehler noch False. Damit sagt __eq__ bei fremden Typen: “dafür bin ich nicht zuständig”. Python probiert dann die andere Seite und fällt am Ende auf False zurück.
  • Für < gibt es __lt__ (less than), für + gibt es __add__. sorted braucht nur __lt__. Die Methoden folgen demselben Muster wie __eq__: Sie bekommen self und other.

Zwei weitere Beispiele für Container-Verhalten:

Schritt 7: Introspektion und abstrakte Klassen

Introspektion (introspection) heißt: ein Objekt zur Laufzeit befragen. __dict__ (bei Instanzen: ihre Attribute), __class__, __name__, __bases__, __module__, dazu hasattr, getattr, setattr, delattr, vars, dir. Neue Attribute lassen sich jederzeit an eine Instanz hängen, außer die Klasse nutzt __slots__.

Abstrakte Klassen (abstract classes) brauchen das Modul abc (ABC, @abstractmethod). Sie lassen sich nicht direkt instanziieren, und Unterklassen müssen alle abstrakten Methoden überschreiben.

Falle: die Prüfungs- und Praxisklassiker

  1. Veränderbare Klassenvariable (z. B. eine Liste im Klassenkörper) wird von allen Instanzen geteilt. Richtig: self.artikel = [] in __init__.
  2. self.zaehler += 1 bei einer Klassenvariable liest die Klassenvariable, schreibt aber ein neues Instanzattribut. Die Klassenvariable bleibt unverändert. (Bei einer Liste mit .append ist es anders: das verändert das geteilte Objekt.)
  3. super().__init__() vergessen: Die Basisklasse bleibt uninitialisiert, es fehlen Attribute.
  4. MRO ist nicht Tiefensuche: Die gemeinsame Basisklasse kommt nach allen ihren Unterklassen. super() meint den nächsten in der MRO, nicht die Elternklasse.
  5. isinstance ist inklusive Oberklassen, type(x) is K ist exakt.
  6. obj.__x von außen ergibt AttributeError, intern heißt es _Klasse__x. In einer Unterklasse wird aus __x ein anderer Name (_Unterklasse__x), beide existieren nebeneinander.
  7. __eq__ ohne NotImplemented bei fremden Typen: AttributeError statt False.

Übungen

Übung 1: Klassenattribut-Falle vorhersagen

Der Code steht fest, du musst nur das Ergebnis vorhersagen:

class Warenkorb:
    artikel = []
    anzahl = 0
    def __init__(self, name):
        self.name = name
    def lege(self, x):
        self.artikel.append(x)
        self.anzahl += 1

k1 = Warenkorb("a")
k2 = Warenkorb("b")
k1.lege("x")
k1.lege("y")
k2.lege("z")

Trage das Tupel (k1.artikel, k1.anzahl, k2.anzahl, Warenkorb.anzahl) ein.

Unterscheide bei self.artikel.append(x) und self.anzahl += 1: Welche Zeile verändert ein vorhandenes Objekt, welche weist einen Namen neu zu? Und wo landet diese Zuweisung (Instanz oder Klasse)?

antwort = (["x", "y", "z"], 2, 1, 0)
antwort

Übung 2: MRO vorhersagen

class Basis:
    def hallo(self): return "Basis"
class L(Basis): pass
class R(Basis):
    def hallo(self): return "R"
class M(L, R): pass
class N(R, L): pass

Trage das Tupel (M().hallo(), [k.__name__ for k in M.__mro__], [k.__name__ for k in N.__mro__]) ein.

Die MRO beginnt mit der Klasse selbst, dann die Basisklassen von links nach rechts. Die gemeinsame Basisklasse darf aber erst kommen, wenn alle Klassen vor ihr (die von ihr erben) schon dran waren. Und ganz am Ende steht immer dieselbe Klasse.

antwort = ("R", ["M", "L", "R", "Basis", "object"], ["N", "R", "L", "Basis", "object"])
antwort

Übung 3: super() in der Raute

Fülle die drei Lücken so, dass jede __init__ die Basisklasse über die MRO aufruft und danach den eigenen Namen an self.spur anhängt. Basis darf bei Unten() genau einmal laufen.

Wenn Links die Klasse Basis beim Namen ruft, kommt Rechts an dieser Stelle nie dran. Schau in Schritt 4, wie das Diamant-Beispiel die Kette weiterreicht, ohne dass Links Rechts kennt.

class Basis:
    def __init__(self):
        self.spur = ["Basis"]

class Links(Basis):
    def __init__(self):
        super().__init__()
        self.spur.append("Links")

class Rechts(Basis):
    def __init__(self):
        super().__init__()
        self.spur.append("Rechts")

class Unten(Links, Rechts):
    def __init__(self):
        super().__init__()
        self.spur.append("Unten")

Unten

Übung 4: __repr__, __str__, __eq__, __lt__, __add__

Schreibe die Klasse Geld für Beträge in ganzen Cent. Anforderungen:

  • Geld(250) speichert 250 Cent.
  • repr(Geld(250)) ist "Geld(250)", auch innerhalb einer Liste.
  • str(Geld(250)) ist "2,50 EUR", str(Geld(5)) ist "0,05 EUR", str(Geld(1005)) ist "10,05 EUR".
  • == vergleicht die Cent. Beim Vergleich mit einem Nicht-Geld (z. B. Geld(1) == 1) kommt False, kein Fehler.
  • < vergleicht die Cent (damit funktioniert sorted).
  • Geld(a) + Geld(b) ergibt ein neues Geld.

print einer Liste benutzt für die Elemente nicht __str__. Überlege, welche der beiden Darstellungen das ist. Für die zweistellige Zahl mit führender Null hilft die Formatangabe aus der Lektion zu Strings. Und was soll __eq__ zurückgeben, wenn other gar kein Geld ist, damit Python selbst auf False kommt?

class Geld:
    def __init__(self, cent):
        self.cent = cent

    def __repr__(self):
        return f"Geld({self.cent})"

    def __str__(self):
        return f"{self.cent // 100},{self.cent % 100:02d} EUR"

    def __eq__(self, other):
        if not isinstance(other, Geld):
            return NotImplemented
        return self.cent == other.cent

    def __lt__(self, other):
        if not isinstance(other, Geld):
            return NotImplemented
        return self.cent < other.cent

    def __add__(self, other):
        if not isinstance(other, Geld):
            return NotImplemented
        return Geld(self.cent + other.cent)

Geld

Übung 5: Name Mangling vorhersagen

class A:
    def __init__(self):
        self.__x = 1
        self._y = 2
    def lese(self):
        return self.__x

class B(A):
    def __init__(self):
        super().__init__()
        self.__x = 10

b = B()

Trage das Tupel (b.lese(), sorted(vars(b)), hasattr(b, "__x")) ein. sorted(vars(b)) ist die sortierte Liste der Attributnamen von b.

Schreibe auf, welchen Namen Python jeweils intern für self.__x in A und in B vergibt. Wie viele verschiedene Attribute gibt es dann? Und unter welchem Namen sucht hasattr(b, "__x")?

antwort = (1, ["_A__x", "_B__x", "_y"], False)
antwort

Merksatz und Prüfstein

Merksatz: Klassenvariablen gehören allen, super() geht den MRO-Weg (nicht “zur Elternklasse”), __x wird zu _Klasse__x, und Dunder-Methoden machen eigene Klassen zu vollwertigen Python-Typen.

Prüfstein (offene Frage): Erkläre in eigenen Worten, warum super().__init__() in Links bei einem Unten(Links, Rechts) die Klasse Rechts aufruft, obwohl Rechts nicht die Elternklasse von Links ist. Was würde sich ändern, wenn Links die Basis direkt beim Namen aufriefe?

Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “Objektorientierte Programmierung (OOP)” mit “Grundbegriffe”, “Vererbung und super()”, “Mehrfachvererbung und MRO”, “Sichtbarkeit”, “Dunder-Methoden”, “Introspektion” und “Abstrakte Klassen”. Hinweis: self.anzahl += 1 als neues Instanzattribut, die Anwendung von NotImplemented speziell in __eq__ (die Quelle nennt NotImplemented nur im PCPP1-Teil, bei __add__) und die Aussage, dass Listen ihre Elemente mit __repr__ zeigen, stehen nicht in der Quelle, wurden aber mit Python 3.13 ausgeführt und geprüft (bitte prüfen, ob sie in der PCAP geprüft werden). Die Gewichtung “grob ein Drittel der Punkte” ist die Angabe der Quelle (bitte prüfen).