Zum Inhalt springen
Python Fortgeschritten 3 min

OOP in der Praxis

Lerne objektorientierte Programmierung in der Praxis: Entwurfsmuster, dataclasses, Enums, ein komplettes Bibliothekssystem, SOLID-Prinzipien und Best Practices.

Aktualisiert:

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:

PrafixBedeutungBeispiel
nameOeffentlichself.name
_nameGeschuetzt (Konvention)self._email
__namePrivat (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.

Zurück zum Python Kurs