OOP in der Praxis
Lerne objektorientierte Programmierung in der Praxis: Entwurfsmuster, dataclasses, Enums, ein komplettes Bibliothekssystem, SOLID-Prinzipien und Best Practices.
OOP in der Praxis
Du kennst jetzt Klassen, Vererbung und Magic Methods. In diesem Tutorial lernst du, wie du OOP richtig in echten Projekten einsetzt - mit modernen Python-Werkzeugen und bewaehrten Entwurfsmustern.
Wann OOP verwenden?
OOP ist nicht immer die beste Loesung. Hier eine Orientierungshilfe:
OOP ist gut geeignet fuer:
- Systeme mit vielen zusammenhaengenden Entitaeten (Benutzer, Produkte, Bestellungen)
- Zustandsbehaftete Objekte (Spiele, GUIs, Simulationen)
- Wenn du Polymorphismus brauchst (verschiedene Typen, gleiche Schnittstelle)
- Groessere Projekte mit mehreren Entwicklern
- Bibliotheken und Frameworks
Einfache Funktionen reichen oft bei:
- Skripten und Einmal-Aufgaben
- Reine Datenverarbeitung (Pipelines, ETL)
- Kleine Hilfsprogramme
- Mathematische Berechnungen
# OOP ist hier uebertrieben:
class StringHelper:
@staticmethod
def umkehren(text):
return text[::-1]
# Besser - einfach eine Funktion:
def text_umkehren(text):
return text[::-1]
# OOP ist hier sinnvoll:
class Warenkorb:
def __init__(self):
self.artikel = []
self.rabatt_code = None
def hinzufuegen(self, artikel, menge=1):
self.artikel.append({"artikel": artikel, "menge": menge})
def gesamtpreis(self):
summe = sum(a["artikel"].preis * a["menge"] for a in self.artikel)
if self.rabatt_code:
summe *= (1 - self.rabatt_code.prozent / 100)
return summe
Entwurfsmuster: Encapsulation
Encapsulation (Kapselung) bedeutet, interne Details zu verbergen und nur eine kontrollierte Schnittstelle nach aussen anzubieten.
In Python gibt es keine echten privaten Attribute, aber Konventionen:
class Benutzer:
def __init__(self, benutzername, email, passwort):
self.benutzername = benutzername # Oeffentlich
self._email = email # "Geschuetzt" (Konvention: nicht von aussen aendern)
self.__passwort = passwort # "Privat" (Name Mangling)
self._anmeldeversuche = 0
@property
def email(self):
"""Kontrollierter Lesezugriff."""
return self._email
@email.setter
def email(self, neue_email):
"""Kontrollierter Schreibzugriff mit Validierung."""
if "@" not in neue_email or "." not in neue_email:
raise ValueError(f"Ungueltige E-Mail: {neue_email}")
self._email = neue_email
def passwort_aendern(self, altes_passwort, neues_passwort):
"""Passwort nur ueber kontrollierte Methode aendern."""
if not self._passwort_pruefen(altes_passwort):
raise ValueError("Falsches Passwort!")
if len(neues_passwort) < 8:
raise ValueError("Passwort muss mindestens 8 Zeichen haben!")
self.__passwort = neues_passwort
print("Passwort erfolgreich geaendert.")
def _passwort_pruefen(self, passwort):
"""Interne Hilfsmethode (geschuetzt)."""
return passwort == self.__passwort
def anmelden(self, passwort):
if self._anmeldeversuche >= 3:
print("Konto gesperrt! Zu viele fehlgeschlagene Versuche.")
return False
if self._passwort_pruefen(passwort):
self._anmeldeversuche = 0
print(f"Willkommen, {self.benutzername}!")
return True
self._anmeldeversuche += 1
verbleibend = 3 - self._anmeldeversuche
print(f"Falsches Passwort! Noch {verbleibend} Versuche.")
return False
# Verwendung
user = Benutzer("anna42", "anna@email.de", "sicher123")
# Oeffentlich - kein Problem:
print(user.benutzername) # anna42
# Property mit Validierung:
print(user.email) # anna@email.de
user.email = "anna@neu.de" # Funktioniert
# user.email = "ungueltig" # ValueError!
# Passwort ist geschuetzt:
# print(user.__passwort) # AttributeError!
# (Technisch: user._Benutzer__passwort - aber das sollte man NICHT nutzen!)
user.anmelden("falsch") # Falsches Passwort! Noch 2 Versuche.
user.anmelden("sicher123") # Willkommen, anna42!
Zusammenfassung der Konventionen:
| Prafix | Bedeutung | Beispiel |
|---|---|---|
name | Oeffentlich | self.name |
_name | Geschuetzt (Konvention) | self._email |
__name | Privat (Name Mangling) | self.__passwort |
Entwurfsmuster: Polymorphismus
Polymorphismus bedeutet: Verschiedene Objekte, gleiche Schnittstelle. Du kannst verschiedene Typen austauschbar verwenden:
from abc import ABC, abstractmethod
class Benachrichtigung(ABC):
@abstractmethod
def senden(self, empfaenger, nachricht):
pass
@abstractmethod
def status(self):
pass
class EmailBenachrichtigung(Benachrichtigung):
def __init__(self, smtp_server):
self.smtp_server = smtp_server
self._gesendet = 0
def senden(self, empfaenger, nachricht):
print(f"E-Mail an {empfaenger}: {nachricht}")
self._gesendet += 1
return True
def status(self):
return f"E-Mail-Service: {self._gesendet} Nachrichten gesendet"
class SMSBenachrichtigung(Benachrichtigung):
def __init__(self, api_key):
self.api_key = api_key
self._gesendet = 0
def senden(self, empfaenger, nachricht):
if len(nachricht) > 160:
print("SMS zu lang! Wird gekuerzt.")
nachricht = nachricht[:157] + "..."
print(f"SMS an {empfaenger}: {nachricht}")
self._gesendet += 1
return True
def status(self):
return f"SMS-Service: {self._gesendet} Nachrichten gesendet"
class PushBenachrichtigung(Benachrichtigung):
def __init__(self):
self._gesendet = 0
def senden(self, empfaenger, nachricht):
print(f"Push an {empfaenger}: {nachricht}")
self._gesendet += 1
return True
def status(self):
return f"Push-Service: {self._gesendet} Nachrichten gesendet"
# Polymorphismus in Aktion:
def alle_benachrichtigen(dienste, empfaenger, nachricht):
"""Funktioniert mit JEDEM Benachrichtigungstyp!"""
for dienst in dienste:
dienst.senden(empfaenger, nachricht)
dienste = [
EmailBenachrichtigung("smtp.example.com"),
SMSBenachrichtigung("key-123"),
PushBenachrichtigung()
]
alle_benachrichtigen(dienste, "Max", "Deine Bestellung ist da!")
# E-Mail an Max: Deine Bestellung ist da!
# SMS an Max: Deine Bestellung ist da!
# Push an Max: Deine Bestellung ist da!
for dienst in dienste:
print(dienst.status())
dataclasses - Modern und praktisch
Seit Python 3.7 gibt es dataclasses - sie ersparen dir viel Boilerplate-Code:
from dataclasses import dataclass, field
# OHNE dataclass - viel Tipparbeit:
class ProduktAlt:
def __init__(self, name, preis, kategorie, lagerbestand=0):
self.name = name
self.preis = preis
self.kategorie = kategorie
self.lagerbestand = lagerbestand
def __repr__(self):
return (f"ProduktAlt(name='{self.name}', preis={self.preis}, "
f"kategorie='{self.kategorie}', lagerbestand={self.lagerbestand})")
def __eq__(self, other):
if isinstance(other, ProduktAlt):
return (self.name == other.name and self.preis == other.preis
and self.kategorie == other.kategorie)
return NotImplemented
# MIT dataclass - viel kuerzer!
@dataclass
class Produkt:
name: str
preis: float
kategorie: str
lagerbestand: int = 0 # Standardwert
# __init__, __repr__, __eq__ werden AUTOMATISCH generiert!
p1 = Produkt("Laptop", 999.99, "Elektronik", 15)
p2 = Produkt("Laptop", 999.99, "Elektronik", 15)
print(p1) # Produkt(name='Laptop', preis=999.99, kategorie='Elektronik', lagerbestand=15)
print(p1 == p2) # True (automatischer Vergleich)
Der @dataclass Decorator im Detail
from dataclasses import dataclass, field
from typing import List
@dataclass(order=True, frozen=False)
class Student:
"""
order=True -> Erzeugt __lt__, __le__, __gt__, __ge__ (Sortierung!)
frozen=True -> Macht Objekt unveraenderlich (wie ein Tupel)
"""
name: str
matrikelnr: int
semester: int = 1
noten: List[float] = field(default_factory=list) # Mutable Defaults!
_durchschnitt: float = field(init=False, repr=False) # Nicht im __init__
def __post_init__(self):
"""Wird nach __init__ aufgerufen - fuer berechnete Werte."""
self._durchschnitt = 0.0
@property
def durchschnitt(self):
if not self.noten:
return 0.0
return sum(self.noten) / len(self.noten)
def note_hinzufuegen(self, note):
if 1.0 <= note <= 5.0:
self.noten.append(note)
else:
raise ValueError("Note muss zwischen 1.0 und 5.0 liegen!")
# Verwendung
s1 = Student("Anna", 12345, 3)
s2 = Student("Bob", 67890, 1)
s1.note_hinzufuegen(1.3)
s1.note_hinzufuegen(2.0)
s1.note_hinzufuegen(1.7)
print(s1)
# Student(name='Anna', matrikelnr=12345, semester=3, noten=[1.3, 2.0, 1.7])
print(f"Durchschnitt: {s1.durchschnitt:.2f}") # 1.67
# Sortierung funktioniert (nach name, dann matrikelnr, dann semester):
studenten = [s2, s1]
studenten.sort()
for s in studenten:
print(s.name) # Anna, Bob
Wichtig: Fuer mutable Standardwerte (Listen, Dicts) immer field(default_factory=list) verwenden, NICHT noten: list = []! Sonst teilen sich alle Instanzen die gleiche Liste.
Enum-Klassen
Enums (Enumerations) sind ideal fuer feste Wertemengen:
from enum import Enum, auto
class Farbe(Enum):
ROT = "rot"
GRUEN = "gruen"
BLAU = "blau"
GELB = "gelb"
class Prioritaet(Enum):
NIEDRIG = auto() # Automatischer Wert: 1
MITTEL = auto() # 2
HOCH = auto() # 3
KRITISCH = auto() # 4
class Status(Enum):
OFFEN = "offen"
IN_BEARBEITUNG = "in_bearbeitung"
ERLEDIGT = "erledigt"
ABGEBROCHEN = "abgebrochen"
# Verwendung
print(Farbe.ROT) # Farbe.ROT
print(Farbe.ROT.value) # rot
print(Farbe.ROT.name) # ROT
# Vergleiche
print(Farbe.ROT == Farbe.ROT) # True
print(Farbe.ROT == Farbe.BLAU) # False
print(Farbe.ROT == "rot") # False! (Enum != String)
# In Bedingungen
status = Status.IN_BEARBEITUNG
if status == Status.ERLEDIGT:
print("Fertig!")
elif status == Status.IN_BEARBEITUNG:
print("Wird noch dran gearbeitet...")
# Iteration ueber alle Werte
for p in Prioritaet:
print(f"{p.name}: {p.value}")
# Enum aus Wert erstellen
farbe = Farbe("blau")
print(farbe) # Farbe.BLAU
Enums mit Dataclasses kombinieren:
from dataclasses import dataclass, field
from enum import Enum, auto
from datetime import datetime
class TicketStatus(Enum):
OFFEN = "offen"
IN_BEARBEITUNG = "in_bearbeitung"
ERLEDIGT = "erledigt"
class TicketPrioritaet(Enum):
NIEDRIG = 1
MITTEL = 2
HOCH = 3
KRITISCH = 4
@dataclass
class Ticket:
titel: str
beschreibung: str
prioritaet: TicketPrioritaet
status: TicketStatus = TicketStatus.OFFEN
erstellt_am: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M"))
def bearbeiten(self):
self.status = TicketStatus.IN_BEARBEITUNG
def abschliessen(self):
self.status = TicketStatus.ERLEDIGT
# Verwendung
ticket = Ticket(
"Login funktioniert nicht",
"Benutzer kann sich nicht anmelden",
TicketPrioritaet.HOCH
)
print(ticket)
ticket.bearbeiten()
print(f"Status: {ticket.status.value}") # in_bearbeitung
Praxis-Projekt: Bibliothekssystem
Jetzt bauen wir ein vollstaendiges System mit allem, was wir gelernt haben:
from dataclasses import dataclass, field
from enum import Enum, auto
from datetime import datetime, timedelta
from typing import List, Optional
# === Enums ===
class BuchStatus(Enum):
VERFUEGBAR = "verfuegbar"
AUSGELIEHEN = "ausgeliehen"
RESERVIERT = "reserviert"
BESCHAEDIGT = "beschaedigt"
class Genre(Enum):
ROMAN = "Roman"
SACHBUCH = "Sachbuch"
KRIMI = "Krimi"
SCIENCE_FICTION = "Science-Fiction"
FANTASY = "Fantasy"
BIOGRAFIE = "Biografie"
KINDERBUCH = "Kinderbuch"
# === Datenklassen ===
@dataclass
class Buch:
titel: str
autor: str
isbn: str
genre: Genre
erscheinungsjahr: int
status: BuchStatus = BuchStatus.VERFUEGBAR
ausgeliehen_von: Optional[str] = None
faellig_am: Optional[str] = None
def __str__(self):
status_text = f" [{self.status.value}]" if self.status != BuchStatus.VERFUEGBAR else ""
return f'"{self.titel}" von {self.autor} ({self.erscheinungsjahr}){status_text}'
def ist_verfuegbar(self):
return self.status == BuchStatus.VERFUEGBAR
@dataclass
class Benutzer:
name: str
ausweisnr: str
email: str
ausgeliehene_buecher: List[str] = field(default_factory=list) # ISBNs
strafen: float = 0.0
max_buecher: int = 5
def kann_ausleihen(self):
return (len(self.ausgeliehene_buecher) < self.max_buecher
and self.strafen == 0)
def __str__(self):
return f"{self.name} (Nr. {self.ausweisnr}, {len(self.ausgeliehene_buecher)} Buecher)"
@dataclass
class Ausleihe:
isbn: str
ausweisnr: str
ausgeliehen_am: str
faellig_am: str
zurueckgegeben_am: Optional[str] = None
@property
def ist_ueberfaellig(self):
if self.zurueckgegeben_am:
return False
return datetime.now().strftime("%Y-%m-%d") > self.faellig_am
# === Hauptklasse ===
class Bibliothek:
"""Ein vollstaendiges Bibliotheksverwaltungssystem."""
LEIHFRIST_TAGE = 28
STRAFE_PRO_TAG = 0.50
def __init__(self, name):
self.name = name
self._buecher: dict[str, Buch] = {} # ISBN -> Buch
self._benutzer: dict[str, Benutzer] = {} # Ausweisnr -> Benutzer
self._ausleihen: List[Ausleihe] = []
# --- Buecher verwalten ---
def buch_hinzufuegen(self, buch: Buch):
if buch.isbn in self._buecher:
print(f"Buch mit ISBN {buch.isbn} existiert bereits!")
return False
self._buecher[buch.isbn] = buch
print(f"Buch hinzugefuegt: {buch}")
return True
def buch_suchen(self, suchbegriff: str) -> List[Buch]:
"""Sucht in Titel, Autor und ISBN."""
suchbegriff = suchbegriff.lower()
treffer = []
for buch in self._buecher.values():
if (suchbegriff in buch.titel.lower() or
suchbegriff in buch.autor.lower() or
suchbegriff in buch.isbn.lower()):
treffer.append(buch)
return treffer
def buecher_nach_genre(self, genre: Genre) -> List[Buch]:
return [b for b in self._buecher.values() if b.genre == genre]
# --- Benutzer verwalten ---
def benutzer_registrieren(self, benutzer: Benutzer):
if benutzer.ausweisnr in self._benutzer:
print(f"Benutzer mit Nr. {benutzer.ausweisnr} existiert bereits!")
return False
self._benutzer[benutzer.ausweisnr] = benutzer
print(f"Benutzer registriert: {benutzer}")
return True
# --- Ausleihe ---
def ausleihen(self, isbn: str, ausweisnr: str) -> bool:
# Pruefungen
if isbn not in self._buecher:
print("Buch nicht gefunden!")
return False
if ausweisnr not in self._benutzer:
print("Benutzer nicht gefunden!")
return False
buch = self._buecher[isbn]
benutzer = self._benutzer[ausweisnr]
if not buch.ist_verfuegbar():
print(f"'{buch.titel}' ist leider nicht verfuegbar ({buch.status.value}).")
return False
if not benutzer.kann_ausleihen():
if benutzer.strafen > 0:
print(f"{benutzer.name} hat offene Strafen: {benutzer.strafen:.2f} EUR")
else:
print(f"{benutzer.name} hat bereits {len(benutzer.ausgeliehene_buecher)} Buecher (Maximum: {benutzer.max_buecher}).")
return False
# Ausleihe durchfuehren
heute = datetime.now()
faellig = heute + timedelta(days=self.LEIHFRIST_TAGE)
buch.status = BuchStatus.AUSGELIEHEN
buch.ausgeliehen_von = ausweisnr
buch.faellig_am = faellig.strftime("%Y-%m-%d")
benutzer.ausgeliehene_buecher.append(isbn)
ausleihe = Ausleihe(
isbn=isbn,
ausweisnr=ausweisnr,
ausgeliehen_am=heute.strftime("%Y-%m-%d"),
faellig_am=faellig.strftime("%Y-%m-%d")
)
self._ausleihen.append(ausleihe)
print(f"Ausgeliehen: '{buch.titel}' an {benutzer.name}")
print(f"Faellig am: {faellig.strftime('%d.%m.%Y')}")
return True
def zurueckgeben(self, isbn: str) -> bool:
if isbn not in self._buecher:
print("Buch nicht gefunden!")
return False
buch = self._buecher[isbn]
if buch.status != BuchStatus.AUSGELIEHEN:
print(f"'{buch.titel}' ist nicht ausgeliehen.")
return False
# Aktive Ausleihe finden
aktive_ausleihe = None
for ausleihe in self._ausleihen:
if ausleihe.isbn == isbn and ausleihe.zurueckgegeben_am is None:
aktive_ausleihe = ausleihe
break
heute = datetime.now().strftime("%Y-%m-%d")
# Strafe berechnen
if aktive_ausleihe and aktive_ausleihe.ist_ueberfaellig:
tage_ueber = (datetime.now() - datetime.strptime(aktive_ausleihe.faellig_am, "%Y-%m-%d")).days
strafe = tage_ueber * self.STRAFE_PRO_TAG
benutzer = self._benutzer[buch.ausgeliehen_von]
benutzer.strafen += strafe
print(f"Ueberfaellig! Strafe: {strafe:.2f} EUR ({tage_ueber} Tage)")
# Zurueckgabe durchfuehren
benutzer = self._benutzer[buch.ausgeliehen_von]
benutzer.ausgeliehene_buecher.remove(isbn)
if aktive_ausleihe:
aktive_ausleihe.zurueckgegeben_am = heute
buch.status = BuchStatus.VERFUEGBAR
buch.ausgeliehen_von = None
buch.faellig_am = None
print(f"Zurueckgegeben: '{buch.titel}'")
return True
# --- Berichte ---
def bestandsuebersicht(self):
print(f"\n{'='*50}")
print(f"Bibliothek: {self.name}")
print(f"{'='*50}")
print(f"Gesamtbestand: {len(self._buecher)} Buecher")
verfuegbar = sum(1 for b in self._buecher.values() if b.ist_verfuegbar())
ausgeliehen = sum(1 for b in self._buecher.values()
if b.status == BuchStatus.AUSGELIEHEN)
print(f"Verfuegbar: {verfuegbar}")
print(f"Ausgeliehen: {ausgeliehen}")
print(f"Benutzer: {len(self._benutzer)}")
print(f"{'='*50}")
# Nach Genre gruppiert
for genre in Genre:
buecher = self.buecher_nach_genre(genre)
if buecher:
print(f"\n{genre.value} ({len(buecher)}):")
for buch in buecher:
print(f" - {buch}")
def __str__(self):
return f"Bibliothek '{self.name}' ({len(self._buecher)} Buecher, {len(self._benutzer)} Benutzer)"
def __len__(self):
return len(self._buecher)
def __contains__(self, isbn):
return isbn in self._buecher
# === System testen ===
# Bibliothek erstellen
bib = Bibliothek("Stadtbibliothek Berlin")
# Buecher hinzufuegen
buecher = [
Buch("Der Hobbit", "J.R.R. Tolkien", "978-3-423-21412-1",
Genre.FANTASY, 1937),
Buch("1984", "George Orwell", "978-3-548-23410-0",
Genre.ROMAN, 1949),
Buch("Eine kurze Geschichte der Zeit", "Stephen Hawking",
"978-3-499-62600-8", Genre.SACHBUCH, 1988),
Buch("Der Schwarm", "Frank Schaetzing", "978-3-596-16453-2",
Genre.SCIENCE_FICTION, 2004),
Buch("Kommissar Wallander", "Henning Mankell",
"978-3-423-21300-1", Genre.KRIMI, 1991),
]
for buch in buecher:
bib.buch_hinzufuegen(buch)
# Benutzer registrieren
bib.benutzer_registrieren(Benutzer("Anna Mueller", "B001", "anna@email.de"))
bib.benutzer_registrieren(Benutzer("Bob Schmidt", "B002", "bob@email.de"))
# Ausleihen
bib.ausleihen("978-3-423-21412-1", "B001") # Anna leiht "Der Hobbit"
bib.ausleihen("978-3-548-23410-0", "B001") # Anna leiht "1984"
bib.ausleihen("978-3-596-16453-2", "B002") # Bob leiht "Der Schwarm"
# Suche
treffer = bib.buch_suchen("tolkien")
print(f"\nSuchergebnisse fuer 'tolkien':")
for buch in treffer:
print(f" - {buch}")
# Zurueckgabe
bib.zurueckgeben("978-3-548-23410-0") # Anna gibt "1984" zurueck
# Bestandsuebersicht
bib.bestandsuebersicht()
Best Practices fuer OOP in Python
1. Halte Klassen klein und fokussiert
# Schlecht: Eine Klasse macht alles
class GottKlasse:
def daten_laden(self): ...
def daten_validieren(self): ...
def daten_speichern(self): ...
def email_senden(self): ...
def bericht_erstellen(self): ...
def ui_aktualisieren(self): ...
# Besser: Jede Klasse hat eine Aufgabe
class DatenLader:
def laden(self, quelle): ...
class DatenValidierer:
def validieren(self, daten): ...
class EmailService:
def senden(self, empfaenger, nachricht): ...
2. Verwende aussagekraeftige Namen
# Schlecht
class D:
def __init__(self, n, v):
self.n = n
self.v = v
def p(self):
return self.n * self.v
# Gut
class Produkt:
def __init__(self, name, verkaufspreis):
self.name = name
self.verkaufspreis = verkaufspreis
def preis_anzeigen(self):
return f"{self.name}: {self.verkaufspreis:.2f} EUR"
3. Bevorzuge Komposition
# Statt tiefer Vererbung:
class Tier: ...
class Saeugetier(Tier): ...
class Haustier(Saeugetier): ...
class Hund(Haustier): ...
class Labrador(Hund): ... # 5 Ebenen tief - zu viel!
# Besser mit Komposition:
@dataclass
class Eigenschaft:
name: str
wert: str
@dataclass
class Tier:
name: str
art: str
eigenschaften: List[Eigenschaft] = field(default_factory=list)
def hat_eigenschaft(self, name):
return any(e.name == name for e in self.eigenschaften)
4. Nutze Type Hints
from typing import List, Optional, Dict
class Inventar:
def __init__(self):
self._artikel: Dict[str, int] = {}
def hinzufuegen(self, name: str, menge: int = 1) -> None:
self._artikel[name] = self._artikel.get(name, 0) + menge
def entnehmen(self, name: str, menge: int = 1) -> Optional[int]:
if name not in self._artikel or self._artikel[name] < menge:
return None
self._artikel[name] -= menge
return self._artikel[name]
def suchen(self, suchbegriff: str) -> List[str]:
return [n for n in self._artikel if suchbegriff.lower() in n.lower()]
SOLID-Prinzipien (Kurze Einfuehrung)
Die SOLID-Prinzipien sind fuenf Leitlinien fuer gutes OOP-Design:
S - Single Responsibility Principle (Einzelverantwortung)
Jede Klasse sollte nur eine Verantwortung haben.
# Schlecht: Klasse hat mehrere Verantwortungen
class Mitarbeiter:
def gehalt_berechnen(self): ...
def in_datenbank_speichern(self): ...
def bericht_drucken(self): ...
# Besser: Aufgaben trennen
class Mitarbeiter:
def gehalt_berechnen(self): ...
class MitarbeiterRepository:
def speichern(self, mitarbeiter): ...
class MitarbeiterBericht:
def drucken(self, mitarbeiter): ...
O - Open/Closed Principle (Offen/Geschlossen)
Klassen sollten offen fuer Erweiterung, aber geschlossen fuer Aenderung sein.
# Schlecht: Muss bei jedem neuen Typ geaendert werden
class RabattRechner:
def berechnen(self, kunde_typ, betrag):
if kunde_typ == "standard":
return betrag * 0.05
elif kunde_typ == "premium":
return betrag * 0.10
elif kunde_typ == "vip":
return betrag * 0.20
# Neuer Typ? -> Klasse aendern!
# Besser: Erweiterbar ohne Aenderung
from abc import ABC, abstractmethod
class RabattStrategie(ABC):
@abstractmethod
def berechnen(self, betrag):
pass
class StandardRabatt(RabattStrategie):
def berechnen(self, betrag):
return betrag * 0.05
class PremiumRabatt(RabattStrategie):
def berechnen(self, betrag):
return betrag * 0.10
# Neuer Typ? -> Einfach neue Klasse!
class MitarbeiterRabatt(RabattStrategie):
def berechnen(self, betrag):
return betrag * 0.30
L - Liskov Substitution, I - Interface Segregation, D - Dependency Inversion
Diese drei Prinzipien gehen tiefer in die Softwarearchitektur. Kurz zusammengefasst:
- L: Unterklassen muessen ueberall einsetzbar sein, wo die Basisklasse erwartet wird
- I: Lieber viele kleine, spezifische Interfaces als ein grosses allgemeines
- D: Abhaengigkeiten auf Abstraktionen, nicht auf konkrete Klassen
Haeufige Fehler bei OOP
Fehler 1: Uebertriebene Vererbung
# Schlecht: Vererbung fuer alles
class Datenbank: ...
class MySQLDatenbank(Datenbank): ...
class MySQLBenutzerDatenbank(MySQLDatenbank): ...
class MySQLAdminBenutzerDatenbank(MySQLBenutzerDatenbank): ...
# Besser: Flache Hierarchie + Komposition
class DatenbankVerbindung: ...
class BenutzerRepository:
def __init__(self, db: DatenbankVerbindung):
self.db = db
Fehler 2: Mutable Klassenattribute
# GEFAEHRLICH!
class Student:
noten = [] # Wird von ALLEN Instanzen geteilt!
s1 = Student()
s2 = Student()
s1.noten.append(1.0)
print(s2.noten) # [1.0] - Ups! s2 hat auch die Note!
# RICHTIG:
class Student:
def __init__(self):
self.noten = [] # Jede Instanz hat eigene Liste
Fehler 3: Zu viele Getter und Setter
# "Java-Stil" - in Python unnoetig:
class Person:
def __init__(self, name):
self._name = name
def get_name(self):
return self._name
def set_name(self, name):
self._name = name
# Pythonisch - einfach direkt zugreifen:
class Person:
def __init__(self, name):
self.name = name # Direkt oeffentlich!
# Erst @property verwenden, wenn Validierung noetig wird:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, wert):
if not wert.strip():
raise ValueError("Name darf nicht leer sein!")
self._name = wert.strip()
Fehler 4: init macht zu viel
# Schlecht: Seiteneffekte im Konstruktor
class BerichtGenerator:
def __init__(self, daten):
self.daten = daten
self.daten_validieren() # Seiteneffekt
self.bericht = self.erstellen() # Seiteneffekt
self.speichern() # Seiteneffekt
# Besser: __init__ nur fuer Initialisierung
class BerichtGenerator:
def __init__(self, daten):
self.daten = daten
self.bericht = None
def erstellen(self):
self.daten_validieren()
self.bericht = self._generieren()
return self.bericht
def speichern(self, pfad):
if not self.bericht:
raise RuntimeError("Erst erstellen() aufrufen!")
# Speichern...
Uebungen
Uebung 1: Todo-App mit dataclasses
Erstelle ein Todo-Verwaltungssystem mit den Klassen Todo (dataclass mit Titel, Beschreibung, Prioritaet als Enum, Status als Enum, Faelligkeitsdatum) und TodoListe (mit Methoden zum Hinzufuegen, Entfernen, Filtern nach Status/Prioritaet und Sortieren).
Uebung 2: Banksystem erweitern
Erweitere das Bankkonto-Beispiel aus dem ersten Tutorial: Erstelle verschiedene Kontotypen (Girokonto, Sparkonto, Festgeld) mit unterschiedlichen Zinssaetzen und Ueberziehungslimits. Verwende Vererbung, Properties und Enums.
Uebung 3: Kartenspiel
Erstelle ein einfaches Kartenspiel: Karte (dataclass mit Farbe als Enum und Wert), Kartendeck (mit mischen, ziehen, zuruecklegen) und Spieler (mit Hand, Karte ziehen, Karte ablegen). Implementiere ein einfaches “Hoechste Karte gewinnt”-Spiel.
Uebung 4: Dateisystem-Simulation
Simuliere ein einfaches Dateisystem mit Datei (Name, Groesse, Typ als Enum), Ordner (Name, Inhalt - kann Dateien und andere Ordner enthalten) und Dateisystem (mit Navigation, Suche, Groessenberechnung). Verwende Komposition und rekursive Methoden.
Pro-Tipp: Das Zen der Python-OOP
Viele Entwickler, die von Java oder C# kommen, schreiben in Python zu viel OOP-Code. Denke immer an diese Python-Weisheiten:
- “Einfach ist besser als komplex” - Wenn eine Funktion reicht, brauchst du keine Klasse
- “Flach ist besser als verschachtelt” - Halte Vererbungshierarchien kurz (maximal 2-3 Ebenen)
- “Praktikabilitaet schlaegt Reinheit” - Perfektes OOP-Design ist weniger wichtig als lesbarer Code
- “Wir sind hier alle Erwachsene” - Python vertraut Entwicklern, deshalb gibt es kein echtes “private”
Nutze dataclasses fuer einfache Datenobjekte, regulaere Klassen fuer komplexe Logik und einfache Funktionen fuer alles andere. Dieser pragmatische Mix ist typisch fuer guten Python-Code.