- 1 Section
- 10 Lessons
- unbegrenzt
- Python Fortgeschritten10
- 1.1Module und Pakete: import, pip, venv
- 1.2Objektorientierung in Python: Klassen, __init__, self
- 1.3Vererbung und Mehrfachvererbung in Python
- 1.4Decorators und Generatoren
- 1.5Fehlerbehandlung vertieft: eigene Exceptions
- 1.6Reguläre Ausdrücke mit re
- 1.7Arbeiten mit APIs: requests, JSON
- 1.8Automatisierung mit os, subprocess, pathlib
- 1.9Unit-Tests in Python: pytest
- 1.10Praxisprojekt: Automatisierungsskript mit Tests
Objektorientierung in Python: Klassen, __init__, self
Python ist eine sehr flexible Sprache – du kannst rein prozedurale Skripte schreiben, mit Funktionen arbeiten oder objektorientiert programmieren. Sobald Code aber wächst und gemeinsam genutzte Daten und Verhalten verwaltet werden müssen, ist OOP fast immer die beste Wahl.
Diese Lektion zeigt, wie OOP in Python aussieht: class, der spezielle __init__-Konstruktor, das immer gegenwärtige self, Properties statt klassischer Getter/Setter, Klassen- vs. Instanz-Attribute, und das moderne @dataclass-Idiom. Wenn du OOP in Java: Klassen, Kapselung, Konstruktoren schon kennst, wirst du viele Parallelen sehen – und auch ein paar deutliche Unterschiede.
1) Die einfachste Klasse
Eine Klasse in Python definierst du mit dem Schlüsselwort class. Sie kann Attribute (Felder) und Methoden enthalten:
class Auto: def __init__(self, marke, farbe): self.marke = marke self.farbe = farbe self.tachostand = 0 def beschleunigen(self, km): self.tachostand += km def beschreibung(self): return f"{self.farbe} {self.marke} mit {self.tachostand} km"
Nutzung:
auto = Auto("BMW", "blau") auto.beschleunigen(100) print(auto.beschreibung()) # blau BMW mit 100 km
Was ist hier anders als in Java? Es gibt kein new beim Erzeugen – einfach Auto(...) wie ein Funktionsaufruf. Die Klasse hat einen „Konstruktor" namens __init__, der mit zwei Unterstrichen vor und hinter dem Namen geschrieben wird (deshalb „Dunder" = Double UNDERscore).
2) self – was ist das?
self ist die Python-Entsprechung zu Javas this mit einem kleinen Unterschied: in Python ist self ein expliziter Parameter jeder Methode. Beim Aufruf auto.beschleunigen(100) übergibt Python das Objekt auto automatisch als ersten Parameter – die Methodendefinition zeigt diesen Parameter aber explizit.
__init__(self, ...), beschleunigen(self, km)
farbe="blau"
tachostand=100
farbe="rot"
tachostand=0
self in einer Methode zeigt immer auf die konkrete Instanz, auf der die Methode aufgerufen wurde.Der Name self ist Konvention, aber nicht erzwungen – technisch könntest du auch this oder x schreiben. Mach das nie. Jeder Python-Code nutzt self; alles andere irritiert.
3) Instanz-Attribute setzen
Attribute werden typischerweise in __init__ gesetzt – damit jede Instanz von Anfang an ihre Felder hat:
class Konto: def __init__(self, inhaber, startbetrag=0): self.inhaber = inhaber self.saldo = startbetrag
Du kannst aber auch nach der Erzeugung Attribute hinzufügen – Python ist dynamisch:
k = Konto("Anna") k.alter = 28 # neues Attribut spontan angefügt print(k.alter) # 28
In großen Codebasen ist das eher unschön – setze alle Attribute in __init__, damit klar ist, was eine Instanz hat. Für strikteren Code gibt es __slots__ (siehe Abschnitt 14).
4) Methoden definieren und aufrufen
Eine Methode ist eine Funktion innerhalb einer Klasse, deren erster Parameter self ist. Sie hat Zugriff auf alle Attribute der Instanz:
class Konto: def __init__(self, inhaber, startbetrag=0): self.inhaber = inhaber self.saldo = startbetrag def einzahlen(self, betrag): if betrag <= 0: raise ValueError("Betrag muss positiv sein") self.saldo += betrag def abheben(self, betrag): if betrag > self.saldo: raise ValueError("Nicht genug Saldo") self.saldo -= betrag k = Konto("Anna", 100) k.einzahlen(50) print(k.saldo) # 150
5) Klassen-Attribute vs. Instanz-Attribute
Achtung – ein subtiler, aber wichtiger Unterschied: Werte, die direkt in der Klasse stehen (nicht in __init__), sind Klassen-Attribute. Sie gehören zur Klasse, nicht zur einzelnen Instanz:
class Auto: raeder = 4 # Klassen-Attribut – einmal pro Klasse def __init__(self, marke): self.marke = marke # Instanz-Attribut – pro Objekt a1 = Auto("BMW") a2 = Auto("VW") print(a1.raeder, a2.raeder) # 4 4 – beide sehen das Klassen-Attribut Auto.raeder = 6 # Klassen-Attribut ändern → für ALLE Instanzen print(a1.raeder, a2.raeder) # 6 6
class Kunde: bestellungen = [] # FALSCH – ALLE Kunden teilen sich diese Liste! def __init__(self, name): self.name = nameRichtig: Listen und Dicts in
__init__ initialisieren, dann hat jede Instanz ihre eigene.
6) Public, Private und der Unterstrich
Anders als Java hat Python keine echte Zugriffskontrolle. Alle Attribute sind technisch öffentlich. Es gibt aber Konventionen:
| Namens-Präfix | Bedeutung |
|---|---|
name | Öffentlich – Teil der API, von außen nutzbar |
_name | „Intern" – Konvention, dass externe Nutzer das nicht anfassen sollen |
__name | Name-Mangling: Python benennt das Attribut intern um, schützt vor versehentlichem Überschreiben in Unterklassen |
__name__ | Reserviert für Spezial-Attribute (Dunder) |
class Konto: def __init__(self, inhaber): self.inhaber = inhaber # public self._kontonummer = "DE12" # intern self.__pin = "1234" # Name-Mangling: wird zu _Konto__pin
Die Faustregel: „Wir sind alle erwachsene Menschen" – wenn dort ein Unterstrich steht, fasst du das nicht an. Erzwungen ist es nicht. Manche kommen aus Java und finden das gefährlich; in der Python-Welt funktioniert es trotzdem.
7) Properties – wie Getter/Setter, nur eleganter
In Java schreibst du oft Getter und Setter, um Felder zu validieren oder zu berechnen. Python kennt das Konzept der Property: ein Attribut, das wie ein normales Attribut aussieht, aber hinter den Kulissen Funktionen aufruft:
class Person: def __init__(self, alter): self._alter = alter @property def alter(self): return self._alter @alter.setter def alter(self, wert): if wert < 0: raise ValueError("Alter darf nicht negativ sein") self._alter = wert p = Person(30) print(p.alter) # 30 – sieht aus wie Attribut, ruft aber Funktion auf p.alter = 31 # Setter mit Validierung p.alter = -5 # ValueError
Vorteile: API bleibt einfach (man schreibt p.alter statt p.get_alter()), aber du kannst später Validierung oder Berechnung hinzufügen, ohne dass externer Code sich ändern muss. Das ist der Python-Weg, Kapselung zu erreichen.
8) __str__ und __repr__ – lesbare Ausgabe
Damit Objekte sinnvoll mit print ausgegeben werden, überschreibst du __str__:
class Auto: def __init__(self, marke): self.marke = marke def __str__(self): return f"Auto({self.marke})" def __repr__(self): return f"Auto(marke={self.marke!r})" a = Auto("BMW") print(a) # Auto(BMW) – nutzt __str__ print(repr(a)) # Auto(marke='BMW') – nutzt __repr__
Faustregel: __repr__ ist für Entwickler (zeigt das Objekt eindeutig), __str__ ist für Endnutzer (lesbar). Wenn du dich nicht entscheiden willst, implementiere __repr__ – __str__ fällt sonst darauf zurück.
9) Wichtige Dunder-Methoden
Python-Klassen können durch spezielle „Dunder"-Methoden in das Sprachverhalten integriert werden:
print() und str() genutzt.==. Wenn du das überschreibst, auch __hash__ berücksichtigen.<, >. Für sorted().len(obj) aufgerufen.obj[i].for x in obj.with-Blöcke.Mehr zur Implementierung von Iteratoren und Context Managern in Decorators und Generatoren.
10) Vergleichbare Objekte
Standardmäßig vergleicht == in Python Identität (gleiches Objekt im Speicher), nicht inhaltliche Gleichheit. Mit __eq__ definierst du eigene Logik:
class Punkt: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Punkt): return NotImplemented return self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y)) print(Punkt(1, 2) == Punkt(1, 2)) # True
Wichtig: __hash__ und __eq__ gehören zusammen. Zwei Objekte, die gleich sind, müssen denselben Hash haben – sonst funktionieren set und dict nicht korrekt. Wenn du eines überschreibst, überschreib auch das andere.
11) @dataclass – Schreibarbeit sparen
Viele Klassen sind reine Datencontainer mit Attributen, Konstruktor, __repr__ und __eq__. Seit Python 3.7 gibt es dafür den Decorator @dataclass:
from dataclasses import dataclass @dataclass class Punkt: x: float y: float p1 = Punkt(1.0, 2.0) p2 = Punkt(1.0, 2.0) print(p1) # Punkt(x=1.0, y=2.0) print(p1 == p2) # True
@dataclass erzeugt automatisch __init__, __repr__ und __eq__ aus den Typ-Annotationen. Mit Optionen:
@dataclass(frozen=True) # immutable – Attribute können nicht geändert werden class Punkt: x: float y: float from dataclasses import field @dataclass class Kunde: name: str alter: int = 0 # Default-Wert tags: list = field(default_factory=list) # leere Liste pro Instanz
Für reine Daten-Container ist @dataclass der moderne Standard. Spart enorm Code und reduziert Fehlerquellen.
12) Klassen-Methoden und statische Methoden
Methoden sind nicht alle gleich. Python kennt drei Sorten:
class Konto: bank = "Sparkasse" def __init__(self, inhaber): self.inhaber = inhaber def info(self): # Instanz-Methode return f"{self.inhaber} bei {self.bank}" @classmethod def aus_string(cls, text): # Klassen-Methode return cls(text.strip()) @staticmethod def ist_gueltige_kontonummer(nr): # Statische Methode return len(nr) == 22
Unterschiede:
- Instanz-Methode – erster Parameter ist
self, braucht eine Instanz - Klassen-Methode (
@classmethod) – erster Parameter istcls(die Klasse selbst). Häufig für Factory-Methoden - Statische Methode (
@staticmethod) – kein erster Sonder-Parameter, gehört nur logisch zur Klasse
Aufruf:
k = Konto("Anna") k.info() # Instanz-Methode Konto.aus_string(" Ben ") # Klassen-Methode Konto.ist_gueltige_kontonummer("DE...") # Statisch
13) Java vs. Python – die Kurzversion
–
this implizit verfügbar– Statische Typprüfung
–
private/public erzwungen– Getter/Setter Pflicht für Kapselung
– Eine Datei pro public Klasse
– Vererbung mit
extends
__init__–
self ist expliziter Parameter– Dynamische Typisierung
– Keine echten Zugriffsmodifier – nur Konventionen
– Properties statt Getter/Setter
– Mehrere Klassen pro Datei üblich
– Vererbung mit Klammern:
class B(A):
14) __slots__ – Speicher sparen und Felder festlegen
Standardmäßig speichert jedes Python-Objekt seine Attribute in einem internen __dict__. Das ist flexibel (neue Attribute jederzeit möglich), kostet aber Speicher. Mit __slots__ definierst du die Attribute fest und sparst RAM:
class Punkt: __slots__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y p = Punkt(1, 2) p.z = 3 # AttributeError: 'Punkt' object has no attribute 'z'
Lohnt sich, wenn du Tausende oder Millionen Instanzen einer kleinen Klasse erzeugst. In normalen Anwendungen unnötig.
15) Häufige Fehler
- self vergessen als erster Parameter einer Methode – Python wirft
TypeError: missing 1 required positional argumentbeim Aufruf - self vor Attribut vergessen:
name = "x"in einer Methode ist eine lokale Variable, nichtself.name = "x" - Veränderbare Default-Werte:
def add(self, item, liste=[])teilt die Liste zwischen Aufrufen. Lösung:liste=Nonemitif liste is None: liste = [] - Veränderbare Klassen-Attribute:
tags = []auf Klassenebene wird zur geteilten Liste aller Instanzen - __init__ mit Rückgabewert:
__init__darf nichts zurückgeben – Python wirft sonst Fehler - == ohne __eq__: vergleicht Objekt-Identität, nicht Inhalt.
__eq__definieren oder@dataclassnutzen - __hash__ vergessen bei
__eq__: das Objekt landet als unhashbar und passt nicht inset/dict - Getter/Setter wie in Java: in Python unnötig. Nutze direkte Attribute oder Properties
@dataclass. Für „echte" Klassen mit komplexer Logik die Vollform mit __init__, Methoden und Properties. Vermeide unnötige Getter/Setter – Python-Stil ist „direkter Attribut-Zugriff, mit Property wenn Validierung nötig".
Zusammenfassung
In Python definierst du eine Klasse mit class und einen „Konstruktor" namens __init__. Der Parameter self ist explizit und zeigt auf die aktuelle Instanz. Instanz-Attribute (in __init__ gesetzt) sind pro Objekt eigen, Klassen-Attribute (direkt in der Klasse) sind für alle gleich. Es gibt keine echte Zugriffskontrolle – Konvention: _attribut für intern, __attribut für Name-Mangling. Statt klassischer Getter/Setter nutzt Python Properties mit @property. Wichtige Dunder-Methoden: __str__, __repr__, __eq__, __hash__, __len__, __iter__. Für reine Daten-Container ist @dataclass der moderne Standard – generiert __init__, __repr__, __eq__ automatisch. Drei Methoden-Arten: Instanz-Methode (mit self), Klassen-Methode (@classmethod, mit cls), statische Methode (@staticmethod).
