Grundlagen

Klassen und OOP in Python: __init__, self, Methoden, Vererbung und dataclass

Aktualisiert: · Skillaria.Top

Klassen in Python sind Baupläne: Sie beschreiben, welche Daten ein Objekt speichert und welche Aktionen es ausführen kann. Ein Objekt (Instanz) entsteht durch den Aufruf der Klasse, __init__ füllt seine Felder, und self verweist auf das aktuelle Objekt. Die Vererbung erlaubt es, das Verhalten eines Elternteils zu übernehmen und Teile davon zu überschreiben, und der Decorator @dataclass erzeugt __init__, __repr__ und __eq__ für Klassen, die vor allem Daten speichern.

Eine Klasse in Python ist ein Bauplan: Sie beschreibt, welche Daten ein Objekt speichert und was es damit tun kann. Ein Objekt (Instanz) ist ein konkretes Exemplar nach diesem Bauplan. Die objektorientierte Programmierung in Python stützt sich auf drei Dinge: Klassen mit Methoden, die Vererbung und die speziellen Methoden wie __init__ und __repr__. Wir gehen alles der Reihe nach an einem durchgehenden Beispiel durch, einer Verwaltung von Ausgaben.

Wozu Klassen dienen, wenn es Dictionaries gibt

Daten lassen sich in einem Dictionary speichern: {"title": "Kaffee", "amount": 250}. Das Problem ist, dass ein Dictionary nicht weiß, welche Schlüssel richtig sind. Ein Tippfehler wie expense.get("amout", 0) erzeugt keinen Fehler, sondern liefert eine Null, und der Bericht zeigt still falsche Zahlen.

Bei einem Objekt einer Klasse sind die Felder im Voraus festgelegt. Der Zugriff auf ein nicht vorhandenes Attribut führt sofort zu einem AttributeError, und aktuelle Python-Versionen schlagen sogar den richtigen Namen vor. Außerdem bündelt die Klasse Daten und die Aktionen darauf an einer Stelle: Eine Ausgabe kann selbst die Summe mit Steuer berechnen, und der Bericht ermittelt seine Gesamtsumme selbst.

Eine Klasse in Python anlegen

Eine Klasse beginnt mit dem Wort class. Der Name wird nach der Konvention PEP 8 in CamelCase geschrieben. Ein einfaches Beispiel:

class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

coffee = Expense("Kaffee", 250)
taxi = Expense("Taxi", 300)
print(coffee.title, coffee.amount)   # Kaffee 250

Der Aufruf Expense("Kaffee", 250) erzeugt eine neue Instanz der Klasse. coffee und taxi sind zwei verschiedene Objekte desselben Typs, jedes mit eigenen Feldwerten. Die Zugehörigkeit prüfen Sie mit isinstance(coffee, Expense), das ergibt True.

Der Konstruktor __init__: Initialisierung der Klasse

__init__ wird oft Konstruktor genannt, genau genommen ist es ein Initialisierer. Das Objekt selbst erzeugt die Methode __new__. __init__ wird direkt danach aufgerufen und verteilt die übergebenen Argumente auf die Felder. Eine Rückgabe ist nicht nötig (genauer: er muss None zurückgeben).

In __init__ prüft man bequem die Eingabewerte, damit im Programm keine offensichtlich falschen Objekte entstehen:

class Expense:
    def __init__(self, title, amount):
        if amount < 0:
            raise ValueError("Der Betrag darf nicht negativ sein")
        self.title = title
        self.amount = amount

Was self in Python bedeutet

self ist ein Verweis auf das Objekt, mit dem die Methode gerade arbeitet. Wenn Sie coffee.with_tax() schreiben, ruft Python im Grunde Expense.with_tax(coffee) auf und übergibt das Objekt als erstes Argument. Deshalb steht self in der Definition der Methode an erster Stelle, beim Aufruf schreibt man es nicht.

self ist kein Schlüsselwort, sondern eine Konvention. Technisch könnte der Parameter beliebig heißen, aber das macht niemand. Ein typischer Anfängerfehler ist, self in der Definition der Methode zu vergessen. Dann bricht der Aufruf mit TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given ab.

Methoden einer Klasse und spezielle Methoden

Eine Methode ist eine Funktion, die innerhalb einer Klasse definiert wird. Über self greift sie auf die Felder des Objekts zu:

class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    def with_tax(self, rate=0.2):
        return self.amount * (1 + rate)

    def __repr__(self):
        return f"Expense({self.title!r}, {self.amount})"

coffee = Expense("Kaffee", 250)
print(coffee.with_tax())   # 300.0
print(coffee)              # Expense('Kaffee', 250)

Ohne __repr__ würde die Ausgabe wie <__main__.Expense object at 0x7f...> aussehen, was zum Debuggen nichts nützt. Methoden mit doppelten Unterstrichen auf beiden Seiten heißen spezielle Methoden (Dunder-Methoden), Python ruft sie automatisch auf. __str__ ist für die Ausgabe für Benutzer zuständig, __eq__ für den Vergleich mit ==, __len__ für len(obj).

@classmethod und @staticmethod

Eine gewöhnliche Methode arbeitet mit einer Instanz. Eine Klassenmethode mit dem Decorator @classmethod erhält als erstes Argument die Klasse selbst (cls). Sie wird häufig als alternativer Konstruktor verwendet. Eine statische Methode mit @staticmethod erhält weder Objekt noch Klasse: Sie ist eine gewöhnliche Funktion, die thematisch zur Klasse gehört.

class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    @classmethod
    def from_csv(cls, line):
        title, amount = line.split(";")
        return cls(title, float(amount))

    @staticmethod
    def is_valid_amount(value):
        return value >= 0

e = Expense.from_csv("Mittag;520")
print(e.amount)                    # 520.0
print(Expense.is_valid_amount(-1)) # False

Destruktor der Klasse: __del__

Python kennt die Methode __del__, die vor der Zerstörung des Objekts durch die Speicherbereinigung aufgerufen wird. Auf sie sollten Sie sich zum Freigeben von Ressourcen nicht verlassen, denn der Zeitpunkt des Aufrufs ist nicht garantiert. Für Dateien und Verbindungen nehmen Sie den Kontextmanager with oder die ausdrückliche Methode close().

Klassenattribute und Instanzattribute

Felder, die über self.name = ... angelegt werden, gehören zu einem bestimmten Objekt. Variablen, die direkt im Körper der Klasse stehen, sind Klassenattribute und gelten für alle Instanzen gemeinsam. So speichert man üblicherweise Konstanten der Klasse.

class Expense:
    CURRENCY = "EUR"          # Klassenattribut, Konstante

    def __init__(self, title, amount):
        self.title = title    # Instanzattribute
        self.amount = amount

a = Expense("Kaffee", 250)
b = Expense("Tee", 150)
print(a.CURRENCY, Expense.CURRENCY)   # EUR EUR

Eine gefährliche Falle ist ein veränderliches Klassenattribut. Wenn Sie im Körper der Klasse rows = [] schreiben und per self.rows.append(...) hinzufügen, ist die Liste für alle Objekte dieselbe. Veränderliche Felder legen Sie immer in __init__ an.

Die Felder eines Objekts zeigen Sie mit vars(obj) oder obj.__dict__ an, ein Attribut über seinen Namen als String lesen Sie mit getattr(obj, "amount").

Eigenschaften einer Klasse: @property

Eine Eigenschaft ist eine Methode, die von außen wie ein gewöhnliches Feld gelesen wird, ohne Klammern. Das ist praktisch für berechnete Werte: Die Gesamtsumme eines Berichts muss man nicht getrennt speichern und vergisst auch nicht, sie zu aktualisieren.

class Report:
    def __init__(self):
        self.rows = []

    def add(self, expense):
        self.rows.append(expense)

    @property
    def total(self):
        return sum(e.amount for e in self.rows)

r = Report()
r.add(Expense("Kaffee", 250))
r.add(Expense("Taxi", 300))
print(r.total)   # 550

Soll das Schreiben kontrolliert werden, ergänzt man einen Setter mit @total.setter. Ohne Setter führt eine Zuweisung wie r.total = 0 zu einem AttributeError.

Zur Kapselung ein Hinweis: Echte private Felder gibt es in Python nicht. Ein einzelner Unterstrich (_cache) ist eine Vereinbarung nach dem Motto „nicht von außen anfassen“. Zwei Unterstriche (__secret) aktivieren das Verfälschen des Namens (Name Mangling), damit ein Nachfolger das Feld nicht versehentlich überschreibt.

Vererbung von Klassen in Python

Die Vererbung erlaubt es, auf Basis einer Elternklasse eine Kindklasse zu bilden: Das Kind erhält alle Felder und Methoden, und die benötigten Methoden überschreibt es. Die Elternklasse wird in Klammern angegeben.

class MonthlyReport(Report):
    def __init__(self, month):
        super().__init__()        # __init__ des Elternteils aufrufen
        self.month = month

    def add(self, expense):       # Methode überschreiben
        print(f"[{self.month}] + {expense}")
        super().add(expense)      # das Verhalten des Elternteils beibehalten

m = MonthlyReport("Oktober")
m.add(Expense("Kaffee", 250))   # [Oktober] + Expense('Kaffee', 250)
print(m.total)                  # 250 — die Eigenschaft ist geerbt

super() liefert ein Vermittlerobjekt, über das Sie Methoden der Elternklasse aufrufen. Ein vergessenes super().__init__() im __init__ der Kindklasse ist ein klassischer Fehler: Das Feld rows wird nicht angelegt, und der erste Aufruf von add bricht mit AttributeError ab.

Die Verwandtschaft prüfen Sie mit issubclass(MonthlyReport, Report). Alle Klassen in Python 3 erben implizit von object, daher stammen die Grundmethoden __repr__, __eq__ und andere. Python unterstützt auch Mehrfachvererbung (class C(A, B)). Die Reihenfolge der Methodensuche zeigt C.__mro__.

Abstrakte Klasse in Python

Eine abstrakte Klasse legt eine Schnittstelle fest: Welche Methoden müssen die Unterklassen implementieren? Ein Objekt dieser Klasse lässt sich nicht erzeugen. Dafür gibt es das Modul abc:

from abc import ABC, abstractmethod

class Exporter(ABC):
    @abstractmethod
    def export(self, report): ...

class CsvExporter(Exporter):
    def export(self, report):
        return "\n".join(f"{e.title};{e.amount}" for e in report.rows)

Exporter()   # TypeError: Can't instantiate abstract class Exporter ...

Eine Unterklasse, die nicht alle abstrakten Methoden implementiert, bleibt ebenfalls abstrakt und liefert beim Erzeugen denselben Fehler.

Komposition statt tiefer Hierarchien

Nach dem Einstieg in die Vererbung ist man versucht, lange Treppen von Klassen zu bauen: Basisausgabe, Geldausgabe, Ausgabe in Euro, und das über fünf Ebenen. In Python baut man so selten. Die Vererbung beschreibt die Beziehung „ist ein“, die Komposition die Beziehung „enthält ein“: Ein Bericht ist keine Liste von Ausgaben, er enthält sie im Feld rows.

Praktische Regel: Wenn Sie nur eine Fähigkeit eines anderen Objekts brauchen, speichern Sie es in einem Feld, statt zu erben. Für Daten reicht oft ein dataclass, die Logik halten Sie in gewöhnlichen Funktionen.

Dataclasses in Python: Klassen ohne Schreibarbeit

Für Klassen, die überwiegend Daten speichern, ist es mühsam, __init__, __repr__ und __eq__ von Hand zu schreiben. Der Decorator @dataclass aus dem Modul dataclasses erzeugt sie aus den Annotationen der Felder:

from dataclasses import dataclass, field

@dataclass
class Expense:
    title: str
    amount: float
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Der Betrag darf nicht negativ sein")

e = Expense("Kaffee", 250)
print(e)                           # Expense(title='Kaffee', amount=250, tags=[])
print(e == Expense("Kaffee", 250)) # True

Die Prüfung der Werte, die man früher in __init__ geschrieben hat, wandert in __post_init__. Diese Methode läuft direkt nach dem generierten Initialisierer. Veränderliche Standardwerte legt man nur über field(default_factory=list) fest. Die Schreibweise tags: list = [] verbietet der dataclass und löst einen ValueError aus.

frozen und slots

Der Parameter frozen=True macht das Objekt unveränderlich, slots=True (seit Python 3.10) erzeugt __slots__ und spart Speicher. Der Versuch, ein Feld eines eingefrorenen Objekts zu ändern, löst einen FrozenInstanceError aus. Ein Objekt mit neuem Wert erstellt man als Kopie:

from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Expense:
    title: str
    amount: float

e = Expense("Kaffee", 250)
# e.amount = 300   -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2)   # Expense(title='Kaffee', amount=250) Expense(title='Kaffee', amount=300)

Seit Python 3.13 kann dasselbe die allgemeine Funktion copy.replace(). Eingefrorene dataclass-Objekte lassen sich in Mengen speichern und als Schlüssel eines Dictionaries verwenden, denn sie besitzen eine __hash__-Methode.

Neues bei Klassen: Self, @override, __slots__

Für die Typisierung von Klassen gibt es zwei nützliche Werkzeuge. typing.Self (Python 3.11, PEP 673) bedeutet „es wird ein Objekt derselben Klasse zurückgegeben“, auch in geerbten Methoden. Der Decorator @override (Python 3.12, PEP 698) kennzeichnet eine Methode, die eine Methode der Elternklasse überschreiben muss. Haben Sie sich im Namen vertippt, meldet mypy oder pyright, dass es keine gleichnamige Basismethode gibt.

from typing import Self, override

class Report:
    def __init__(self):
        self.rows = []

    def add(self, e) -> Self:
        self.rows.append(e)
        return self              # kann verkettet aufgerufen werden

class MonthlyReport(Report):
    @override
    def add(self, e) -> Self:
        return super().add(e)

In Python 3.15 wurde eine Nischeneinschränkung aufgehoben: Unterklassen von tuple dürfen nicht leere __slots__ haben, und __dict__ sowie __weakref__ können in den __slots__ jeder Klasse angegeben werden. In Version 3.14 führte eine solche Deklaration bei einem Nachfolger eines Tupels zum Fehler TypeError: nonempty __slots__ not supported for subtype of 'tuple'.

Typische Fehler bei Klassen

FehlerSymptomLösung
self in der Methode vergessenTypeError: ... takes 0 positional arguments but 1 was givenDer erste Parameter einer Methode ist immer self
Feld ohne self. in __init__Der Wert verschwindet nach __init__self.amount = amount schreiben
Liste als KlassenattributDaten „fließen“ zwischen ObjektenListen in __init__ oder über default_factory anlegen
super().__init__() fehltAttributeError bei Feldern der ElternklasseDen Initialisierer des Elternteils aufrufen
Eigenschaft mit Klammern aufrufenTypeError: 'int' object is not callableDie Eigenschaft ohne Klammern lesen: r.total

Fazit

Die objektorientierte Programmierung in Python beginnt einfach: Eine Klasse beschreibt Felder und Methoden, __init__ füllt die Felder, und self verweist auf das aktuelle Objekt. Vererbung und super() erweitern das Verhalten, abstrakte Klassen legen eine Schnittstelle fest, und @property macht berechnete Felder möglich. Für Datenklassen nehmen Sie @dataclass, und bei tiefen Hierarchien ziehen Sie Komposition vor. Klassen und Dataclasses werden im Python-Kurs am Beispiel des Ausgabentrackers praktisch behandelt.

Häufige Fragen

Worin unterscheidet sich eine Klasse von einem Objekt in Python?

Die Klasse beschreibt den Typ: welche Felder und Methoden er hat. Ein Objekt oder eine Instanz entsteht durch den Aufruf der Klasse, etwa Expense("Kaffee", 250), und speichert konkrete Feldwerte.

Muss ich __init__ in einer Klasse schreiben?

Nein. Ohne eigenes __init__ wird der Initialisierer von object verwendet, der keine Argumente annimmt. Bei einer dataclass erzeugt ihn der Decorator.

Worin unterscheidet sich __str__ von __repr__?

__repr__ ist die eindeutige Darstellung für Entwickler und den Debugger, __str__ die verständliche Ausgabe für Benutzer bei print und f-Strings. Fehlt __str__, verwendet Python __repr__.

Wann nehme ich eine dataclass und wann eine normale Klasse?

Eine dataclass eignet sich, wenn die Klasse vor allem Daten speichert und fertige __init__, __repr__ und Vergleiche benötigt. Eine normale Klasse ist besser, wenn die Initialisierung komplex ist oder das Verhalten wichtiger ist als die Daten.

Gibt es in Python private Felder?

Eine strenge Privatsphäre gibt es nicht. Ein Unterstrich am Anfang des Namens ist eine Vereinbarung „nicht für die Verwendung von außen“, zwei Unterstriche aktivieren das Verfälschen des Namens, das ein Feld vor versehentlichem Überschreiben im Nachfolger schützt.

Das Thema im Video lernen

Dieses Thema wird im Kurs „Python 3.15: от новичка до профи“ ausführlich mit Praxis behandelt.

Zum Kurs