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

URL: https://skillaria.top/de/guides/python/klassy-python

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

| Fehler | Symptom | Lösung |
| --- | --- | --- |

| `self` in der Methode vergessen | `TypeError: ... takes 0 positional arguments but 1 was given` | Der erste Parameter einer Methode ist immer `self` |

| Feld ohne `self.` in `__init__` | Der Wert verschwindet nach `__init__` | `self.amount = amount` schreiben |

| Liste als Klassenattribut | Daten „fließen“ zwischen Objekten | Listen in `__init__` oder über `default_factory` anlegen |

| `super().__init__()` fehlt | `AttributeError` bei Feldern der Elternklasse | Den Initialisierer des Elternteils aufrufen |

| Eigenschaft mit Klammern aufrufen | `TypeError: 'int' object is not callable` | Die 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.

## FAQ

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


---
Source: Skillaria.Top (https://skillaria.top)
