Implementiere Kapitel 15 (Parameter & Container) und Kapitel 16 (Master-Abschlussprojekt)

This commit is contained in:
Alex Mueller 2026-08-29 14:47:24 +02:00
parent a3a6362c77
commit 478b1d95fe
16 changed files with 4271 additions and 6 deletions

View file

@ -341,7 +341,8 @@
<div class="footer-nav"> <div class="footer-nav">
<a href="../13_persistenz_json_und_csv/index.html" class="btn btn-secondary">&larr; Zurück zu Kapitel 13</a> <a href="../13_persistenz_json_und_csv/index.html" class="btn btn-secondary">&larr; Zurück zu Kapitel 13</a>
<a href="../index.html" class="btn">Zur Kursübersicht &rarr;</a> <a href="../index.html" class="btn btn-secondary">🏠 Kursübersicht</a>
<a href="../15_parameter_und_container/index.html" class="btn">Weiter zu Kapitel 15 &rarr;</a>
</div> </div>
</section> </section>

View file

@ -0,0 +1,157 @@
# Kapitel 15: Parameter (*args, **kwargs) & Eigene Container 🎒📦
Willkommen zu **Kapitel 15**! In diesem Modul meisterst du zwei der mächtigsten und flexibelsten Sprachfeatures von Python:
1. **Variable Parameterlisten** (`*args` und `**kwargs`) sowie **Argument-Unpacking** (`*liste`, `**dict`).
2. **Eigene Container-Klassen**, die sich dank magischer **Dunder-Methoden** genau wie Pythons eingebaute Listen und Dictionaries verhalten (`len()`, `[]`, `del`, `in`, `for`).
---
## 🧭 Didaktischer Hintergrund (Schulabgleich 29.0 & 30.0)
In der professionellen Softwareentwicklung und im Lehrplan sind flexible Schnittstellen und eigene Datenstrukturen essenziell:
- **Schulabgleich 29.0:** Variable Parameterlisten (`*args`, `**kwargs`) und Argument-Unpacking.
- **Schulabgleich 30.0:** Eigene Sequenzen und Container-Klassen mit dem Python-Container-Protokoll (`__len__`, `__getitem__`, `__setitem__`, `__delitem__`, `__contains__`, `__iter__`).
---
## 🧰 1. Variable Parameterlisten: `*args` und `**kwargs`
Normalerweise hat eine Python-Funktion eine feste Anzahl von Parametern:
```python
def addiere_zwei(a: float, b: float) -> float:
return a + b
```
Was aber, wenn du eine Funktion schreiben möchtest, die **2, 5 oder 100 Zahlen** zusammenrechnet? Oder eine Funktion, die beliebige Spieler-Attribute entgegennimmt?
---
### A) `*args` (Beliebig viele Positionsargumente als Tupel)
Das Sternchen `*` vor einem Parameternamen signalisiert Python: *„Sammle alle überschüssigen Positionsargumente in einem unveränderlichen **Tupel**!“*
```python
def berechne_summe(*zahlen: float) -> float:
print(type(zahlen)) # <class 'tuple'>
return sum(zahlen)
print(berechne_summe(10, 20)) # -> 30 (zahlen ist (10, 20))
print(berechne_summe(1, 2, 3, 4, 5)) # -> 15 (zahlen ist (1, 2, 3, 4, 5))
print(berechne_summe()) # -> 0 (zahlen ist ())
```
> 💡 **Analogie:** `*args` ist wie ein **unendlicher Werkzeuggürtel**. Egal ob du 1 Schraubendreher oder 10 Hämmer hineinsteckst – alle werden gebündelt in ein Tupel gelegt.
---
### B) `**kwargs` (Beliebig viele Schlüsselwortargumente als Dict)
Zwei Sternchen `**` signalisieren Python: *„Sammle alle benannten Argumente (`schluessel=wert`) in einem **Dictionary**!“*
```python
def erstelle_held(name: str, **attribute) -> dict:
print(type(attribute)) # <class 'dict'>
return {"name": name, **attribute}
held = erstelle_held("Legolas", klasse="Waldlaeufer", level=15, waffe="Elbenbogen")
# -> {'name': 'Legolas', 'klasse': 'Waldlaeufer', 'level': 15, 'waffe': 'Elbenbogen'}
```
> 💡 **Analogie:** `**kwargs` ist wie ein **ausfüllbares Formular**. Der Aufrufer kann beliebig viele zusätzliche Zeilen mit Label und Wert ergänzen.
---
### C) Die goldene Reihenfolge bei Parametern
Wenn du normale Parameter, `*args`, Default-Werte und `**kwargs` kombinierst, gilt stets diese feste Reihenfolge:
```python
def super_funktion(pos1, pos2, *args, standard_wert=10, **kwargs):
pass
```
1. **Positionsargumente** (`pos1`, `pos2`)
2. **`*args`** (Restliche Positionsargumente)
3. **Keyword-Only / Default-Parameter** (`standard_wert=10`)
4. **`**kwargs`** (Restliche Keyword-Argumente)
---
## 🎁 2. Argument-Unpacking: `*liste` und `**dict`
Der Stern-Operator funktioniert nicht nur bei der **Definition** von Funktionen (Packen), sondern auch beim **Aufruf** (Entpacken / Unpacking):
```python
# 1. Listen-Unpacking mit *
preise = [19.99, 49.99, 9.99]
# Statt berechne_summe(preise[0], preise[1], preise[2]):
gesamt = berechne_summe(*preise)
# 2. Dictionary-Unpacking mit **
config = {"klasse": "Magier", "level": 20, "element": "Feuer"}
# Statt erstelle_held("Merlin", klasse="Magier", level=20, element="Feuer"):
magier = erstelle_held("Merlin", **config)
```
---
## 🎒 3. Eigene Container-Klassen mit Dunder-Methoden
Warum fühlt sich Python so elegant an? Weil eingebaute Typen (`list`, `dict`) und eigene Klassen **exakt dieselben Operatoren** nutzen können!
Wenn du eine Klasse `Inventar` erstellst, möchtest du:
- `len(inventar)` statt `inventar.get_anzahl()`
- `inventar[0]` oder `inventar["Schwert"]` statt `inventar.get_element(0)`
- `inventar[0] = trank` statt `inventar.set_element(0, trank)`
- `del inventar["Schwert"]` statt `inventar.remove_element("Schwert")`
- `'Heiltrank' in inventar` statt `inventar.enthaelt("Heiltrank")`
- `for item in inventar:` statt umständlicher Index-Schleifen
### Die magischen Container-Dunder im Überblick:
| Dunder-Methode | Ausgelöst durch | Zweck |
| :--- | :--- | :--- |
| `__len__(self)` | `len(container)` | Gibt die Anzahl der Elemente als `int` zurück. |
| `__getitem__(self, key)` | `container[key]` | Liest Element an Index `key` (z.B. int `0` oder str `"Schwert"`). |
| `__setitem__(self, key, val)` | `container[key] = val` | Setzt/Ersetzt Element an Index `key`. |
| `__delitem__(self, key)` | `del container[key]` | Löscht das Element an Index `key`. |
| `__contains__(self, item)` | `item in container` | Gibt `True` zurück, wenn `item` enthalten ist, sonst `False`. |
| `__iter__(self)` | `for x in container:` | Gibt einen Iterator zurück (`iter(self._items)`). |
---
## 🎮 4. Praxisbeispiel: Das Gaming-Inventar
In `aufgabe.py` baust du das RPG-Inventar eines Helden:
```
┌────────────────────────────────────────────────────────┐
│ 🎒 Inventar (Maximalgewicht: 20.0 kg) │
│ ├── [0] Eisenschwert (3.5 kg, 120 G) │
│ ├── [1] Holzschild (2.0 kg, 45 G) │
│ └── [2] Heiltrank (0.5 kg, 20 G) │
│ ───────────────────────────────────────────────── │
│ Gesamtgewicht: 6.0 / 20.0 kg (Frei: 14.0 kg) │
│ len(inv) -> 3 │
│ "Eisenschwert" in inv -> True │
│ inv["Heiltrank"] -> Gegenstand('Heiltrank', ...) │
└────────────────────────────────────────────────────────┘
```
---
## 📁 Die Dateien in diesem Ordner
- **`index.html`**: Interaktive Web-Lernseite mit visuellen Tabellen, Rucksack-Analogie, Cheat-Sheets und Checkliste.
- **`aufgabe.py`**: Dein Arbeitsblatt mit Type Hints und TODOs für `*args`, `**kwargs` und die Klasse `Inventar`.
- **`test_aufgabe.py`**: Automatische Unittest-Suite zur Überprüfung aller Anforderungen.
- **`musterloesung.py`**: Vollständig ausprogrammierte Referenzlösung.
---
## 🚀 Schnellstart
```bash
# 1. Bearbeite aufgabe.py
# 2. Führe die automatischen Tests aus:
python3 test_aufgabe.py
# 3. Oder starte aufgabe.py direkt:
python3 aufgabe.py
```

View file

@ -0,0 +1,360 @@
"""
Kapitel 15: Parameter (*args, **kwargs) & Eigene Container-Klassen 🎒📦
======================================================================
Aufgabe: Lerne flexible Funktionsparameter kennen und baue eine
vollwertige Gaming-Inventar-Containerklasse mit magischen Dunder-Methoden!
Themen:
1. *args (beliebig viele Positionsargumente als Tupel)
2. **kwargs (beliebig viele Schlüsselwortargumente als Dictionary)
3. Argument-Unpacking (*liste, **dict)
4. Eigene Container mit __len__, __getitem__, __setitem__, __delitem__, __contains__, __iter__
"""
import math
from typing import Union, List, Optional, Dict, Any, Iterator
# ==============================================================================
# TEIL 1: FUNKTIONEN MIT *ARGS UND **KWARGS
# ==============================================================================
def berechne_gesamtsumme(*zahlen: float, rabatt_prozent: float = 0.0) -> float:
"""
Berechnet die Gesamtsumme beliebig vieler Zahlen (*args) und wendet
einen optionalen prozentualen Rabatt an.
Parameter:
*zahlen (float): Beliebig viele numerische Werte (als Tupel empfangen).
rabatt_prozent (float): Rabatt in Prozent (0.0 bis 100.0). Standard ist 0.0.
Rückgabe:
float: Die berechnete Gesamtsumme nach Rabatt, gerundet auf 2 Nachkommastellen.
Exceptions:
ValueError: Falls rabatt_prozent < 0.0 oder > 100.0 ist.
Beispiele:
berechne_gesamtsumme(10.0, 20.0, 30.0) -> 60.0
berechne_gesamtsumme(100.0, 50.0, rabatt_prozent=10.0) -> 135.0
berechne_gesamtsumme() -> 0.0
"""
# ==========================================================================
# TODO 1: Implementiere berechne_gesamtsumme
# 1. Validiere rabatt_prozent: Liegt der Wert nicht zwischen 0.0 und 100.0,
# löse einen ValueError mit passender Nachricht aus.
# 2. Summiere alle Werte in `zahlen` (wenn zahlen leer ist, ist die Summe 0.0).
# 3. Ziehe den Rabatt ab: summe * (1.0 - rabatt_prozent / 100.0)
# 4. Runde das Ergebnis mit round(..., 2) und gib es zurück.
# ==========================================================================
pass
def erstelle_profil(name: str, **details: Any) -> Dict[str, Any]:
"""
Erstellt ein Profil-Dictionary mit dem Namen und beliebigen weiteren
Schlüsselwort-Attributen (**kwargs).
Parameter:
name (str): Der Name der Person oder Spielfigur.
**details (Any): Beliebige Zusatzinformationen (z.B. alter=25, klasse="Krieger").
Rückgabe:
Dict[str, Any]: Ein Dictionary, das {"name": name} enthält, ergänzt um
alle übergebenen Keyword-Argumente.
Beispiele:
erstelle_profil("Aragorn", klasse="Waldlaeufer", level=20)
-> {"name": "Aragorn", "klasse": "Waldlaeufer", "level": 20}
"""
# ==========================================================================
# TODO 2: Implementiere erstelle_profil
# 1. Erstelle ein Dictionary mit {"name": str(name)}.
# 2. Füge alle Schlüssel-Wert-Paare aus `details` hinzu.
# 3. Gib das fertige Dictionary zurück.
# ==========================================================================
pass
# ==============================================================================
# TEIL 2: DIE DATENKLASSE GEGENSTAND (ITEM)
# ==============================================================================
class Gegenstand:
"""
Repräsentiert einen Gegenstand im Spielinventar.
"""
def __init__(self, name: str, gewicht: float, wert: int = 0):
"""
Initialisiert einen Gegenstand.
Parameter:
name (str): Bezeichnung des Gegenstands.
gewicht (float): Gewicht in Kilogramm (muss >= 0 sein).
wert (int): Verkaufswert in Goldstücken (muss >= 0 sein, Standard 0).
Exceptions:
ValueError: Falls gewicht < 0 oder wert < 0 ist.
"""
# ==========================================================================
# TODO 3: Implementiere Gegenstand.__init__, __repr__, __str__ und __eq__
# 1. Validiere gewicht >= 0 und wert >= 0 (sonst ValueError).
# 2. Setze self.name = str(name), self.gewicht = float(gewicht), self.wert = int(wert).
# ==========================================================================
pass
def __repr__(self) -> str:
"""
Entwickler-Darstellung, z.B. "Gegenstand(name='Heiltrank', gewicht=0.5, wert=25)"
"""
pass
def __str__(self) -> str:
"""
Benutzerfreundliche Darstellung, z.B. "Heiltrank (0.5kg, 25G)"
"""
pass
def __eq__(self, other: object) -> bool:
"""
Zwei Gegenstände sind gleich, wenn Name, Gewicht und Wert übereinstimmen.
"""
pass
# ==============================================================================
# TEIL 3: DIE EIGENE CONTAINER-KLASSE INVENTAR
# ==============================================================================
class Inventar:
"""
Eine maßgeschneiderte Container-Klasse für ein Gaming-Rucksack-Inventar.
Unterstützt Maximalgewicht, *args-Hinzufügen und alle Python-Container-Dunder!
"""
def __init__(self, max_gewicht: float = 20.0, gegenstaende: Optional[List[Gegenstand]] = None):
"""
Initialisiert das Inventar mit einem Maximalgewicht und optionalen Start-Gegenständen.
Parameter:
max_gewicht (float): Maximal tragbares Gewicht (muss > 0 sein, Standard 20.0).
gegenstaende (Optional[List[Gegenstand]]): Optionale Liste von Gegenständen.
Exceptions:
ValueError: Falls max_gewicht <= 0 oder die Start-Gegenstände das Maximalgewicht überschreiten.
"""
# ==========================================================================
# TODO 4: Inventar-Initialisierung
# 1. Validiere max_gewicht > 0 (sonst ValueError).
# 2. Speichere self.max_gewicht = float(max_gewicht).
# 3. Initialisiere eine interne Liste self._items = [].
# 4. Falls `gegenstaende` übergeben wurden, füge sie mit self.hinzufuegen(*gegenstaende) hinzu.
# ==========================================================================
pass
@property
def gesamtgewicht(self) -> float:
"""
Berechnet das aktuelle Gesamtgewicht aller enthaltenen Gegenstände,
gerundet auf 2 Nachkommastellen.
"""
# ==========================================================================
# TODO 5a: Berechne Gesamtgewicht (Summe aller item.gewicht)
# ==========================================================================
pass
@property
def freie_kapazitaet(self) -> float:
"""
Gibt das noch verfügbare Restgewicht bis zum Maximalgewicht zurück.
"""
# ==========================================================================
# TODO 5b: Berechne freie Kapazität (max_gewicht - gesamtgewicht, gerundet auf 2 Stellen)
# ==========================================================================
pass
def hinzufuegen(self, *items: Gegenstand) -> None:
"""
Fügt ein oder mehrere Gegenstände (*args) zum Inventar hinzu.
Parameter:
*items (Gegenstand): Beliebig viele Gegenstand-Objekte.
Exceptions:
TypeError: Falls ein Argument kein `Gegenstand`-Objekt ist.
ValueError: Falls das Hinzufügen aller übergebenen Gegenstände
das Maximalgewicht des Inventars überschreiten würde.
"""
# ==========================================================================
# TODO 6: hinzufuegen mit *args
# 1. Prüfe für jedes item in `items`, ob es eine Instanz von Gegenstand ist (sonst TypeError).
# 2. Berechne das Zusatzgewicht aller neuen Gegenstände.
# 3. Wenn self.gesamtgewicht + zusatz_gewicht > self.max_gewicht:
# Löse einen ValueError("Maximalgewicht überschritten!") aus.
# 4. Hänge alle items an self._items an.
# ==========================================================================
pass
def __len__(self) -> int:
"""
Ermöglicht len(inventar) -> Gibt die Anzahl der Gegenstände zurück.
"""
# ==========================================================================
# TODO 7: __len__
# ==========================================================================
pass
def __getitem__(self, key: Union[int, str]) -> Gegenstand:
"""
Ermöglicht Index- und Namenszugriff:
- inventar[0] -> Gibt den Gegenstand an Position 0 zurück (int-Index).
- inventar["Heiltrank"] -> Sucht den ersten Gegenstand mit diesem Namen (case-insensitive).
Exceptions:
IndexError: Bei ungültigem Integer-Index.
KeyError: Falls kein Gegenstand mit dem übergebenen String-Namen gefunden wurde.
TypeError: Falls key weder int noch str ist.
"""
# ==========================================================================
# TODO 8: __getitem__ (Multi-Type Lookup: int und str)
# 1. Wenn isinstance(key, int): gib self._items[key] zurück.
# 2. Wenn isinstance(key, str): suche den ersten Gegenstand mit passendem Namen
# (Vergleich mit g.name.lower() == key.lower()). Wenn gefunden, zurückgeben.
# Wurde kein Item gefunden, löse KeyError aus.
# 3. Andernfalls löse TypeError aus.
# ==========================================================================
pass
def __setitem__(self, index: int, value: Gegenstand) -> None:
"""
Ermöglicht Zuweisung per Index: inventar[0] = neuer_gegenstand.
Exceptions:
TypeError: Falls index kein int oder value kein Gegenstand ist.
IndexError: Falls index außerhalb des gültigen Bereichs liegt.
ValueError: Falls durch den Austausch das Maximalgewicht überschritten würde.
"""
# ==========================================================================
# TODO 9: __setitem__
# 1. Prüfe Typen (index muss int sein, value muss Gegenstand sein, sonst TypeError).
# 2. Prüfe Indexgrenzen: -len(self._items) <= index < len(self._items) (sonst IndexError).
# 3. Berechne neues Gesamtgewicht: gesamtgewicht - altes_item.gewicht + value.gewicht.
# Wenn neues_gesamtgewicht > max_gewicht: löse ValueError aus.
# 4. Ersetze das Element: self._items[index] = value.
# ==========================================================================
pass
def __delitem__(self, key: Union[int, str]) -> None:
"""
Ermöglicht das Löschen per del-Keyword:
- del inventar[0] -> Löscht per Integer-Index.
- del inventar["Heiltrank"] -> Löscht das erste gefundene Item mit diesem Namen.
Exceptions:
IndexError: Bei ungültigem int-Index.
KeyError: Wenn String-Name nicht existiert.
TypeError: Falls key weder int noch str ist.
"""
# ==========================================================================
# TODO 10: __delitem__
# 1. Wenn int: del self._items[key].
# 2. Wenn str: suche erstes Element mit passendem Namen (case-insensitive) und entferne es.
# Falls nicht vorhanden -> KeyError.
# 3. Andernfalls -> TypeError.
# ==========================================================================
pass
def __contains__(self, item: Union[Gegenstand, str]) -> bool:
"""
Ermöglicht den `in`-Operator:
- "Heiltrank" in inventar -> True/False (Name-Suche, case-insensitive)
- gegenstand_objekt in inventar -> True/False (Objekt-Prüfung)
"""
# ==========================================================================
# TODO 11: __contains__
# 1. Wenn str: prüfe, ob ein Gegenstand mit diesem Namen existiert (case-insensitive).
# 2. Wenn Gegenstand: prüfe `item in self._items`.
# 3. Andernfalls gebe False zurück.
# ==========================================================================
pass
def __iter__(self) -> Iterator[Gegenstand]:
"""
Ermöglicht Iteration: for item in inventar: ...
"""
# ==========================================================================
# TODO 12: __iter__
# Gib einen Iterator über self._items zurück (z.B. iter(self._items)).
# ==========================================================================
pass
def __repr__(self) -> str:
"""Entwickler-Darstellung."""
return f"Inventar(max_gewicht={self.max_gewicht}, gegenstaende={self._items!r})"
def __str__(self) -> str:
"""Benutzerfreundliche Zusammenfassung."""
return f"Inventar ({len(self)} Gegenstände, {self.gesamtgewicht:.1f}/{self.max_gewicht:.1f} kg)"
# ==============================================================================
# TEIL 4: UNPACKING-HELPER-FUNKTION
# ==============================================================================
def packe_inventar(*gegenstaende: Gegenstand, max_gewicht: float = 20.0) -> Inventar:
"""
Erstellt ein neues Inventar und packt alle übergebenen Gegenstände (*args) hinein.
Demonstriert Argument-Unpacking und flexible Parameter.
"""
# ==========================================================================
# TODO 13: packe_inventar
# 1. Erstelle ein Inventar mit `max_gewicht=max_gewicht`.
# 2. Füge alle `gegenstaende` mit inv.hinzufuegen(*gegenstaende) hinzu.
# 3. Gib das befüllte Inventar zurück.
# ==========================================================================
pass
# ==============================================================================
# INTERAKTIVER TERMINAL-TEST
# (python3 aufgabe.py)
# ==============================================================================
if __name__ == "__main__":
print("🎒 Kapitel 15: Parameter & Eigene Container-Klassen\n" + "=" * 55)
# 1. Test *args & Rabatt
preise = [19.99, 49.99, 9.99, 29.50]
summe_rabatt = berechne_gesamtsumme(*preise, rabatt_prozent=15.0)
print(f"1. *args Summe mit 15% Rabatt: {summe_rabatt} €")
# 2. Test **kwargs Profil
held_daten = {"klasse": "Magier", "stufe": 12, "mana": 350, "element": "Feuer"}
profil = erstelle_profil("Ignis", **held_daten)
print(f"2. **kwargs Profil: {profil}")
# 3. Test Container Inventar
schwert = Gegenstand("Eisenschwert", 4.5, 120)
schild = Gegenstand("Holzschild", 3.0, 45)
trank = Gegenstand("Heiltrank", 0.5, 20)
inv = Inventar(max_gewicht=15.0)
try:
inv.hinzufuegen(schwert, schild, trank)
print(f"\n3. Inventar initialisiert: {inv}")
print(f" Anzahl Gegenstände (len): {len(inv)}")
print(f" Gesamtgewicht: {inv.gesamtgewicht} / {inv.max_gewicht} kg")
print(f" Erster Gegenstand [0]: {inv[0]}")
print(f" Suche nach Name ['Heiltrank']: {inv['Heiltrank']}")
print(f" Ist 'Holzschild' im Rucksack? {'Holzschild' in inv}")
print("\n Alle Gegenstände im Rucksack:")
for item in inv:
print(f" - {item}")
print("\n Lösche Heiltrank...")
del inv["Heiltrank"]
print(f" Neues Gesamtgewicht: {inv.gesamtgewicht} kg (Anzahl: {len(inv)})")
except Exception as e:
print(f" ⏳ Noch nicht vollständig implementiert: {e}")

View file

@ -0,0 +1,316 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kapitel 15: Parameter (*args, **kwargs) & Eigene Container</title>
<link rel="stylesheet" href="../assets/style.css">
</head>
<body>
<header>
<div class="header-container">
<nav class="nav-links">
<a href="../index.html">🏠 Zur Kursübersicht</a>
<span>&rsaquo;</span>
<a href="../14_gui_mit_tkinter/index.html">Kapitel 14</a>
<span>&rsaquo;</span>
<span style="color: white; font-weight: 600;">Kapitel 15</span>
</nav>
<div class="header-badge">Kapitel 15 &bull; Schulabgleich 29.0 &amp; 30.0</div>
<h1>Parameter (*args, **kwargs) &amp; Eigene Container 🎒📦</h1>
<p>Mache Funktionen maximal flexibel mit variablen Argumenten und baue eigene Datenstrukturen mit den magischen Container-Dunder-Methoden von Python.</p>
</div>
</header>
<main class="container">
<!-- 1. Das Konzept: Die Werkzeugkoffer- & Rucksack-Analogie -->
<section class="card">
<h2>1. Warum flexible Parameter &amp; eigene Container?</h2>
<p>
In einfachen Programmen hat jede Funktion eine starre Anzahl von Parametern (z.B. <code>def addiere(a, b)</code>).
In realen Software-Projekten möchten wir jedoch oft:
</p>
<ul>
<li>Funktionen aufrufen, die <strong>beliebig viele Argumente</strong> annehmen (wie <code>print()</code> oder <code>max()</code>).</li>
<li>Optionale Konfigurationen und Attribute flexibel übergeben (wie bei Profilen oder Einstellungen).</li>
<li>Eigene Klassen bauen, die sich wie <strong>Listen oder Dictionaries</strong> anfühlen (mit <code>len()</code>, <code>[index]</code>, <code>in</code> und <code>for</code>-Schleifen).</li>
</ul>
<div class="box box-analogie">
<div class="box-title">🧰 Die Werkzeugkoffer- &amp; Rucksack-Analogie:</div>
<ul>
<li><strong><code>*args</code> (Der Werkzeuggürtel):</strong> Du packst beliebig viele Werkzeuge hinein. Python schnürt sie automatisch zu einem unveränderlichen <strong>Tupel</strong> zusammen.</li>
<li><strong><code>**kwargs</code> (Das Etiketten-Fach):</strong> Du übergibst Dinge mit Beschriftung (z.B. <code>schluessel=wert</code>). Python bündelt sie zu einem <strong>Dictionary</strong>.</li>
<li><strong>Container-Klasse (Der smarte Rucksack):</strong> Dein Rucksack weiß, wie schwer er maximal sein darf, wie viele Gegenstände hineinpassen (<code>len()</code>), lässt dich per Griff hineinfassen (<code>[0]</code> oder <code>["Schwert"]</code>) und prüft, ob Proviant eingepackt ist (<code>in</code>).</li>
</ul>
</div>
</section>
<!-- 2. Parameter-Cheat-Sheet -->
<section class="card">
<h2>2. Cheat-Sheet: *args, **kwargs &amp; Unpacking</h2>
<p>
Python unterscheidet zwischen dem <strong>Packen</strong> (in der Funktionsdefinition) und dem <strong>Entpacken</strong> (beim Funktionsaufruf):
</p>
<table style="width: 100%; border-collapse: collapse; margin: 15px 0;">
<thead>
<tr style="background: var(--primary-light); text-align: left;">
<th style="padding: 10px; border: 1px solid var(--border-color);">Syntax</th>
<th style="padding: 10px; border: 1px solid var(--border-color);">Typ in Python</th>
<th style="padding: 10px; border: 1px solid var(--border-color);">Bedeutung &amp; Verwendung</th>
<th style="padding: 10px; border: 1px solid var(--border-color);">Code-Beispiel</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>*args</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tuple</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Nimmt beliebig viele Positionsargumente entgegen.</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>def f(*zahlen): return sum(zahlen)</code></td>
</tr>
<tr style="background: #fafafa;">
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>**kwargs</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>dict</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Nimmt beliebige Schlüsselwortargumente (<code>k=v</code>) entgegen.</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>def f(**opt): return opt.get("lang", "de")</code></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>*liste</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Unpacking</td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Entpackt Elemente einer Liste/Sequenz als einzelne Argumente.</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>werte = [10, 20]; f(*werte)</code></td>
</tr>
<tr style="background: #fafafa;">
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>**dict</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Unpacking</td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Entpackt Dictionary-Paare als benannte Schlüsselwortargumente.</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>daten = {"alter": 25}; f(**daten)</code></td>
</tr>
</tbody>
</table>
<div class="box box-tipp">
<div class="box-title">⚠️ Die feste Parameter-Reihenfolge in Funktionsköpfen:</div>
<code>def meine_funktion(pos1, pos2, *args, standard_option=True, **kwargs):</code>
<ol style="margin-left: 20px; margin-top: 6px;">
<li>Feste Positionsargumente (<code>pos1</code>, <code>pos2</code>)</li>
<li>Variable Positionsargumente (<code>*args</code>)</li>
<li>Optionale Argumente mit Default-Werten / Keyword-Only (<code>standard_option=True</code>)</li>
<li>Variable Keyword-Argumente (<code>**kwargs</code>)</li>
</ol>
</div>
</section>
<!-- 3. Container-Dunder Protokoll -->
<section class="card">
<h2>3. Das Python Container-Protokoll: Magische Dunder-Methoden</h2>
<p>
Damit sich eine eigene Klasse wie ein nativer Python-Container verhält, überschreiben wir die speziellen <strong>Double-Underscore (Dunder)</strong> Methoden:
</p>
<table style="width: 100%; border-collapse: collapse; margin: 15px 0;">
<thead>
<tr style="background: var(--primary-light); text-align: left;">
<th style="padding: 10px; border: 1px solid var(--border-color);">Dunder-Methode</th>
<th style="padding: 10px; border: 1px solid var(--border-color);">Auslöser (Python-Syntax)</th>
<th style="padding: 10px; border: 1px solid var(--border-color);">Rückgabe / Verhalten</th>
<th style="padding: 10px; border: 1px solid var(--border-color);">Fehler bei Ungültigkeit</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__len__(self)</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>len(container)</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Gibt die Anzahl der Elemente als <code>int &gt;= 0</code> zurück.</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>TypeError</code> bei nicht-int</td>
</tr>
<tr style="background: #fafafa;">
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__getitem__(self, key)</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>container[key]</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Liest Element an Index <code>key</code> (z.B. int oder str-Name).</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>IndexError</code> / <code>KeyError</code></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__setitem__(self, key, val)</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>container[key] = val</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Ersetzt oder setzt das Element an Position <code>key</code>.</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>IndexError</code> / <code>ValueError</code></td>
</tr>
<tr style="background: #fafafa;">
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__delitem__(self, key)</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>del container[key]</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Entfernt das Element an Position oder mit Name <code>key</code>.</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>IndexError</code> / <code>KeyError</code></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__contains__(self, item)</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>item in container</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Gibt <code>True</code> zurück, wenn Element existiert, sonst <code>False</code>.</td>
<td style="padding: 10px; border: 1px solid var(--border-color);">-</td>
</tr>
<tr style="background: #fafafa;">
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__iter__(self)</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>for x in container:</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Gibt einen Iterator zurück (z.B. <code>iter(self._items)</code>).</td>
<td style="padding: 10px; border: 1px solid var(--border-color);">-</td>
</tr>
</tbody>
</table>
</section>
<!-- 4. Das RPG Gaming-Inventar -->
<section class="card">
<h2>4. Das Gaming-Inventar im Detail</h2>
<p>
In dieser Übung implementierst du ein vollwertiges Rollenspiel-Inventar:
</p>
<div style="background: #1e293b; color: #f8fafc; border-radius: var(--radius-md); padding: 20px; font-family: var(--font-mono); font-size: 0.9rem; line-height: 1.6; margin: 15px 0;">
<div style="color: #60a5fa; font-weight: bold; margin-bottom: 8px;">🎒 Klasse: Inventar (max_gewicht=15.0 kg)</div>
<div style="padding-left: 15px; border-left: 2px solid #3b82f6;">
<div>├── [0] <span style="color: #a7f3d0;">Eisenschwert</span> (4.5 kg, 120 G)</div>
<div>├── [1] <span style="color: #a7f3d0;">Holzschild</span> (3.0 kg, 45 G)</div>
<div>└── [2] <span style="color: #a7f3d0;">Heiltrank</span> (0.5 kg, 20 G)</div>
</div>
<div style="margin-top: 12px; color: #fbbf24;">
📊 Status: 3 Gegenstände | Gewicht: 8.0 / 15.0 kg (Frei: 7.0 kg)
</div>
<div style="margin-top: 8px; color: #94a3b8; font-size: 0.85rem;">
&bull; <code>inv["Heiltrank"]</code> &rarr; Findet Trank per Namen!<br>
&bull; <code>"Holzschild" in inv</code> &rarr; True<br>
&bull; <code>inv.hinzufuegen(*items)</code> &rarr; Prüft Maximalgewicht automatisch!
</div>
</div>
</section>
<!-- 5. Interaktive Checkliste -->
<section class="card">
<h2>5. 📋 Deine Aufgaben-Checkliste</h2>
<p>Öffne die Datei <code>15_parameter_und_container/aufgabe.py</code> und hake die Schritte nach und nach ab:</p>
<ul class="task-list">
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t15_1">
<label for="t15_1" class="task-label">
<strong>TODO 1 (*args Rabatt-Rechner):</strong> <code>berechne_gesamtsumme(*zahlen, rabatt_prozent=0.0)</code> mit Rabattprüfung (0 bis 100) und Rundung auf 2 Stellen.
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t15_2">
<label for="t15_2" class="task-label">
<strong>TODO 2 (**kwargs Profil):</strong> <code>erstelle_profil(name, **details)</code> zur flexiblen Dictionary-Erzeugung.
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t15_3">
<label for="t15_3" class="task-label">
<strong>TODO 3 (Klasse Gegenstand):</strong> <code>__init__</code> mit Validierung (Gewicht/Wert >= 0), <code>__repr__</code>, <code>__str__</code> und <code>__eq__</code>.
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t15_4">
<label for="t15_4" class="task-label">
<strong>TODO 4 &amp; 5 (Inventar Basis):</strong> <code>__init__</code> (Maximalgewicht > 0), Properties <code>gesamtgewicht</code> und <code>freie_kapazitaet</code>.
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t15_5">
<label for="t15_5" class="task-label">
<strong>TODO 6 (hinzufuegen mit *args):</strong> Mehrere Gegenstände auf einmal hinzufügen, Typen validieren und Maximalgewicht überwachen.
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t15_6">
<label for="t15_6" class="task-label">
<strong>TODO 7 &amp; 8 (__len__ &amp; __getitem__):</strong> <code>len(inv)</code> und Multi-Lookup für Index (int) &amp; Name (str).
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t15_7">
<label for="t15_7" class="task-label">
<strong>TODO 9 &amp; 10 (__setitem__ &amp; __delitem__):</strong> Element-Austausch mit Gewichtsprüfung und Löschen per Index/Name.
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t15_8">
<label for="t15_8" class="task-label">
<strong>TODO 11, 12 &amp; 13 (__contains__, __iter__, Helper):</strong> <code>in</code>-Operator, <code>for</code>-Iteration und <code>packe_inventar(*items)</code>.
</label>
</li>
</ul>
<h3>💡 Lösungshinweise (Aufklappen bei Bedarf)</h3>
<details>
<summary>🔍 Tipp zu TODO 8: __getitem__ mit int und str unterstützen</summary>
<div class="details-content">
<p>Prüfe mit <code>isinstance()</code>, welcher Suchtyp übergeben wurde:</p>
<div class="code-container">
<pre><span class="kw">def</span> <span class="fn">__getitem__</span>(<span class="var">self</span>, <span class="var">key</span>):
<span class="kw">if</span> <span class="fn">isinstance</span>(<span class="var">key</span>, <span class="fn">int</span>):
<span class="kw">return</span> <span class="var">self</span>.<span class="attr">_items</span>[<span class="var">key</span>] <span class="cm"># Wirft automatisch IndexError bei ungültigem Index</span>
<span class="kw">elif</span> <span class="fn">isinstance</span>(<span class="var">key</span>, <span class="fn">str</span>):
<span class="kw">for</span> <span class="var">g</span> <span class="kw">in</span> <span class="var">self</span>.<span class="attr">_items</span>:
<span class="kw">if</span> <span class="var">g</span>.<span class="attr">name</span>.<span class="fn">lower</span>() == <span class="var">key</span>.<span class="fn">lower</span>():
<span class="kw">return</span> <span class="var">g</span>
<span class="kw">raise</span> <span class="fn">KeyError</span>(<span class="str">f"Gegenstand '{key}' nicht im Inventar gefunden!"</span>)
<span class="kw">else</span>:
<span class="kw">raise</span> <span class="fn">TypeError</span>(<span class="str">"Index muss int oder str sein!"</span>)</pre>
</div>
</div>
</details>
<details>
<summary>🔍 Tipp zu TODO 9: Gewichtsberechnung bei __setitem__</summary>
<div class="details-content">
<p>Beim Ersetzen eines Elements musst du das Gewicht des alten Elements abziehen und das neue hinzurechnen:</p>
<div class="code-container">
<pre><span class="var">altes_gewicht</span> = <span class="var">self</span>.<span class="attr">_items</span>[<span class="var">index</span>].<span class="attr">gewicht</span>
<span class="var">neues_gesamtgewicht</span> = <span class="var">self</span>.<span class="attr">gesamtgewicht</span> - <span class="var">altes_gewicht</span> + <span class="var">value</span>.<span class="attr">gewicht</span>
<span class="kw">if</span> <span class="var">neues_gesamtgewicht</span> > <span class="var">self</span>.<span class="attr">max_gewicht</span>:
<span class="kw">raise</span> <span class="fn">ValueError</span>(<span class="str">"Austausch überschreitet Maximalgewicht!"</span>)
<span class="var">self</span>.<span class="attr">_items</span>[<span class="var">index</span>] = <span class="var">value</span></pre>
</div>
</div>
</details>
</section>
<!-- 6. Testen & Navigation -->
<section class="card">
<h2>6. 🧪 Lösung überprüfen &amp; Testen</h2>
<h3>1. Automatische Tests ausführen:</h3>
<div class="code-container">
<div class="code-header">
<span>Terminal (im Ordner 15_parameter_und_container)</span>
</div>
<pre><code>python3 test_aufgabe.py</code></pre>
</div>
<h3>2. Interaktives Terminal-Skript starten:</h3>
<div class="code-container">
<div class="code-header">
<span>Terminal</span>
</div>
<pre><code>python3 aufgabe.py</code></pre>
</div>
<div class="footer-nav">
<a href="../14_gui_mit_tkinter/index.html" class="btn btn-secondary">&larr; Zurück zu Kapitel 14 (Tkinter GUI)</a>
<a href="../index.html" class="btn btn-secondary">🏠 Kursübersicht</a>
<a href="../16_master_abschlussprojekt/index.html" class="btn">Weiter zu Kapitel 16 (Master-Abschlussprojekt) &rarr;</a>
</div>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,232 @@
"""
Kapitel 15: Parameter (*args, **kwargs) & Eigene Container-Klassen 🎒📦
======================================================================
Musterlösung für flexible Parameterlisten, Unpacking und magische
Container-Dunder-Methoden.
"""
import math
from typing import Union, List, Optional, Dict, Any, Iterator
# ==============================================================================
# TEIL 1: FUNKTIONEN MIT *ARGS UND **KWARGS
# ==============================================================================
def berechne_gesamtsumme(*zahlen: float, rabatt_prozent: float = 0.0) -> float:
"""
Berechnet die Gesamtsumme beliebig vieler Zahlen (*args) und wendet
einen optionalen prozentualen Rabatt an.
"""
if not (0.0 <= rabatt_prozent <= 100.0):
raise ValueError("Rabatt muss zwischen 0.0 und 100.0 Prozent liegen!")
summe = sum(float(z) for z in zahlen)
rabatt_faktor = 1.0 - (rabatt_prozent / 100.0)
return round(summe * rabatt_faktor, 2)
def erstelle_profil(name: str, **details: Any) -> Dict[str, Any]:
"""
Erstellt ein Profil-Dictionary mit dem Namen und beliebigen weiteren
Schlüsselwort-Attributen (**kwargs).
"""
profil: Dict[str, Any] = {"name": str(name)}
profil.update(details)
return profil
# ==============================================================================
# TEIL 2: DIE DATENKLASSE GEGENSTAND (ITEM)
# ==============================================================================
class Gegenstand:
"""
Repräsentiert einen Gegenstand im Spielinventar.
"""
def __init__(self, name: str, gewicht: float, wert: int = 0):
if float(gewicht) < 0:
raise ValueError("Gewicht darf nicht negativ sein!")
if int(wert) < 0:
raise ValueError("Wert darf nicht negativ sein!")
self.name: str = str(name)
self.gewicht: float = float(gewicht)
self.wert: int = int(wert)
def __repr__(self) -> str:
return f"Gegenstand(name='{self.name}', gewicht={self.gewicht}, wert={self.wert})"
def __str__(self) -> str:
return f"{self.name} ({self.gewicht}kg, {self.wert}G)"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Gegenstand):
return False
return (
self.name == other.name
and math.isclose(self.gewicht, other.gewicht)
and self.wert == other.wert
)
# ==============================================================================
# TEIL 3: DIE EIGENE CONTAINER-KLASSE INVENTAR
# ==============================================================================
class Inventar:
"""
Eine maßgeschneiderte Container-Klasse für ein Gaming-Rucksack-Inventar.
Unterstützt Maximalgewicht, *args-Hinzufügen und alle Python-Container-Dunder!
"""
def __init__(self, max_gewicht: float = 20.0, gegenstaende: Optional[List[Gegenstand]] = None):
if float(max_gewicht) <= 0:
raise ValueError("Maximalgewicht muss positiv sein!")
self.max_gewicht: float = float(max_gewicht)
self._items: List[Gegenstand] = []
if gegenstaende:
self.hinzufuegen(*gegenstaende)
@property
def gesamtgewicht(self) -> float:
"""
Berechnet das aktuelle Gesamtgewicht aller enthaltenen Gegenstände,
gerundet auf 2 Nachkommastellen.
"""
return round(sum(g.gewicht for g in self._items), 2)
@property
def freie_kapazitaet(self) -> float:
"""
Gibt das noch verfügbare Restgewicht bis zum Maximalgewicht zurück.
"""
return round(self.max_gewicht - self.gesamtgewicht, 2)
def hinzufuegen(self, *items: Gegenstand) -> None:
"""
Fügt ein oder mehrere Gegenstände (*args) zum Inventar hinzu.
"""
for item in items:
if not isinstance(item, Gegenstand):
raise TypeError(f"Erwartet 'Gegenstand', erhielt '{type(item).__name__}'")
zusatz_gewicht = sum(item.gewicht for item in items)
if self.gesamtgewicht + zusatz_gewicht > self.max_gewicht:
raise ValueError(
f"Maximalgewicht von {self.max_gewicht}kg überschritten! "
f"(Aktuell: {self.gesamtgewicht}kg + Neu: {zusatz_gewicht:.2f}kg)"
)
self._items.extend(items)
def __len__(self) -> int:
"""Ermöglicht len(inventar)."""
return len(self._items)
def __getitem__(self, key: Union[int, str]) -> Gegenstand:
"""
Ermöglicht Index- und Namenszugriff:
- inventar[0] -> Item an Position 0 (IndexError bei ungültigem Index).
- inventar["Heiltrank"] -> Erstes Item mit diesem Namen (KeyError bei Nicht-Auffinden).
"""
if isinstance(key, int):
return self._items[key]
elif isinstance(key, str):
for g in self._items:
if g.name.lower() == key.lower():
return g
raise KeyError(f"Gegenstand '{key}' nicht im Inventar gefunden!")
else:
raise TypeError("Index muss int (Position) oder str (Name) sein!")
def __setitem__(self, index: int, value: Gegenstand) -> None:
"""
Ermöglicht Zuweisung per Index: inventar[0] = neuer_gegenstand.
"""
if not isinstance(index, int):
raise TypeError("Index muss int sein!")
if not isinstance(value, Gegenstand):
raise TypeError("Wert muss ein Gegenstand sein!")
if index < -len(self._items) or index >= len(self._items):
raise IndexError("Listenindex außerhalb des gültigen Bereichs!")
altes_gewicht = self._items[index].gewicht
neues_gesamtgewicht = self.gesamtgewicht - altes_gewicht + value.gewicht
if neues_gesamtgewicht > self.max_gewicht:
raise ValueError(
f"Austausch überschreitet Maximalgewicht von {self.max_gewicht}kg!"
)
self._items[index] = value
def __delitem__(self, key: Union[int, str]) -> None:
"""
Ermöglicht das Löschen per del-Keyword:
- del inventar[0] (per int-Index)
- del inventar["Heiltrank"] (per str-Name)
"""
if isinstance(key, int):
del self._items[key]
elif isinstance(key, str):
for i, g in enumerate(self._items):
if g.name.lower() == key.lower():
del self._items[i]
return
raise KeyError(f"Gegenstand '{key}' nicht im Inventar gefunden!")
else:
raise TypeError("Schlüssel muss int oder str sein!")
def __contains__(self, item: Union[Gegenstand, str]) -> bool:
"""
Ermöglicht den in-Operator (Name oder Objekt).
"""
if isinstance(item, str):
return any(g.name.lower() == item.lower() for g in self._items)
elif isinstance(item, Gegenstand):
return item in self._items
return False
def __iter__(self) -> Iterator[Gegenstand]:
"""Ermöglicht for item in inventar: ..."""
return iter(self._items)
def __repr__(self) -> str:
"""Entwickler-Darstellung."""
return f"Inventar(max_gewicht={self.max_gewicht}, gegenstaende={self._items!r})"
def __str__(self) -> str:
"""Benutzerfreundliche Zusammenfassung."""
return f"Inventar ({len(self)} Gegenstände, {self.gesamtgewicht:.1f}/{self.max_gewicht:.1f} kg)"
# ==============================================================================
# TEIL 4: UNPACKING-HELPER-FUNKTION
# ==============================================================================
def packe_inventar(*gegenstaende: Gegenstand, max_gewicht: float = 20.0) -> Inventar:
"""
Erstellt ein neues Inventar und packt alle übergebenen Gegenstände (*args) hinein.
"""
inv = Inventar(max_gewicht=max_gewicht)
inv.hinzufuegen(*gegenstaende)
return inv
if __name__ == "__main__":
print("🎒 Kapitel 15: Musterlösung erfolgreich ausgeführt!\n" + "=" * 55)
preise = [19.99, 49.99, 9.99, 29.50]
print(f"Summe mit 15% Rabatt: {berechne_gesamtsumme(*preise, rabatt_prozent=15.0)} €")
profil = erstelle_profil("Ignis", klasse="Magier", level=12, element="Feuer")
print(f"Profil: {profil}")
schwert = Gegenstand("Eisenschwert", 4.5, 120)
trank = Gegenstand("Heiltrank", 0.5, 20)
inv = packe_inventar(schwert, trank, max_gewicht=10.0)
print(f"Rucksack: {inv}")
for item in inv:
print(f" - {item}")

View file

@ -0,0 +1,341 @@
import unittest
import sys
from pathlib import Path
# Sicherstellen, dass das aktuelle Verzeichnis im Python-Pfad liegt
sys.path.insert(0, str(Path(__file__).parent))
try:
import aufgabe
except ImportError as e:
raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}")
class TestKapitel15(unittest.TestCase):
"""
Testsuite für Kapitel 15: Parameter (*args, **kwargs) & Eigene Container.
"""
def setUp(self):
self.berechne_gesamtsumme = getattr(aufgabe, "berechne_gesamtsumme", None)
self.erstelle_profil = getattr(aufgabe, "erstelle_profil", None)
self.packe_inventar = getattr(aufgabe, "packe_inventar", None)
self.Gegenstand = getattr(aufgabe, "Gegenstand", None)
self.Inventar = getattr(aufgabe, "Inventar", None)
self.assertIsNotNone(self.berechne_gesamtsumme, "Funktion 'berechne_gesamtsumme' nicht gefunden!")
self.assertIsNotNone(self.erstelle_profil, "Funktion 'erstelle_profil' nicht gefunden!")
self.assertIsNotNone(self.packe_inventar, "Funktion 'packe_inventar' nicht gefunden!")
self.assertIsNotNone(self.Gegenstand, "Klasse 'Gegenstand' nicht gefunden!")
self.assertIsNotNone(self.Inventar, "Klasse 'Inventar' nicht gefunden!")
# ==========================================================================
# 1. *args & Rabatt-Funktion
# ==========================================================================
def test_01_berechne_gesamtsumme_args(self):
"""Prüft *args, leere Parameterliste, Rabattberechnung und Validierung."""
# Keine Argumente
self.assertEqual(self.berechne_gesamtsumme(), 0.0)
# Einzelne und mehrere Zahlen
self.assertEqual(self.berechne_gesamtsumme(42.5), 42.5)
self.assertEqual(self.berechne_gesamtsumme(10.0, 20.0, 30.0), 60.0)
self.assertEqual(self.berechne_gesamtsumme(5, 15, 25, 35, 20), 100.0)
# Unpacking mit *liste
zahlen_liste = [12.50, 7.50, 30.00]
self.assertEqual(self.berechne_gesamtsumme(*zahlen_liste), 50.0)
# Rabatt-Berechnungen
self.assertEqual(self.berechne_gesamtsumme(100.0, 50.0, rabatt_prozent=10.0), 135.0)
self.assertEqual(self.berechne_gesamtsumme(200.0, rabatt_prozent=50.0), 100.0)
self.assertEqual(self.berechne_gesamtsumme(80.0, rabatt_prozent=100.0), 0.0)
self.assertEqual(self.berechne_gesamtsumme(80.0, rabatt_prozent=0.0), 80.0)
self.assertEqual(self.berechne_gesamtsumme(19.99, 29.99, rabatt_prozent=15.0), 42.48)
# Ungültige Rabatte müssen ValueError werfen
with self.assertRaises(ValueError, msg="Negativer Rabatt muss ValueError auslösen!"):
self.berechne_gesamtsumme(100.0, rabatt_prozent=-5.0)
with self.assertRaises(ValueError, msg="Rabatt > 100% muss ValueError auslösen!"):
self.berechne_gesamtsumme(100.0, rabatt_prozent=105.0)
# ==========================================================================
# 2. **kwargs & Profil-Erstellung
# ==========================================================================
def test_02_erstelle_profil_kwargs(self):
"""Prüft **kwargs und Dictionary-Unpacking bei der Profil-Erstellung."""
# Minimales Profil (nur Name)
p1 = self.erstelle_profil("Arthur")
self.assertEqual(p1, {"name": "Arthur"})
# Profil mit verschiedenen Schlüsselwortargumenten
p2 = self.erstelle_profil("Gandalf", klasse="Zauberer", stufe=99, mana=5000, aktiv=True)
self.assertEqual(p2["name"], "Gandalf")
self.assertEqual(p2["klasse"], "Zauberer")
self.assertEqual(p2["stufe"], 99)
self.assertEqual(p2["mana"], 5000)
self.assertTrue(p2["aktiv"])
# Unpacking mit **dict
zusatz = {"leben": 100, "ruestung": 45, "gilde": "Gefaehrten"}
p3 = self.erstelle_profil("Gimli", **zusatz)
self.assertEqual(p3, {"name": "Gimli", "leben": 100, "ruestung": 45, "gilde": "Gefaehrten"})
# ==========================================================================
# 3. Klasse Gegenstand
# ==========================================================================
def test_03_gegenstand_klasse(self):
"""Prüft Gegenstand-Attribute, Validierung, __repr__, __str__ und __eq__."""
schwert = self.Gegenstand("Eisenschwert", 3.5, 150)
self.assertEqual(schwert.name, "Eisenschwert")
self.assertEqual(schwert.gewicht, 3.5)
self.assertEqual(schwert.wert, 150)
# Standardwert
kiesel = self.Gegenstand("Kieselstein", 0.1)
self.assertEqual(kiesel.wert, 0)
# Validierung negative Werte
with self.assertRaises(ValueError, msg="Negatives Gewicht muss ValueError werfen"):
self.Gegenstand("FehlerItem", -1.0, 10)
with self.assertRaises(ValueError, msg="Negativer Wert muss ValueError werfen"):
self.Gegenstand("FehlerItem", 1.0, -10)
# Strings & Repräsentation
self.assertIn("Eisenschwert", repr(schwert))
self.assertIn("3.5", repr(schwert))
self.assertIn("Eisenschwert", str(schwert))
# Gleichheit __eq__
gleiches_schwert = self.Gegenstand("Eisenschwert", 3.5, 150)
anderes_schwert = self.Gegenstand("Eisenschwert", 4.0, 150)
self.assertEqual(schwert, gleiches_schwert)
self.assertNotEqual(schwert, anderes_schwert)
self.assertNotEqual(schwert, "Kein Gegenstand")
# ==========================================================================
# 4. Klasse Inventar - Initialisierung & Gewichtsberechnung
# ==========================================================================
def test_04_inventar_init_und_gewicht(self):
"""Prüft Konstruktor, Maximalgewicht, Start-Items und Kapazitätsberechnung."""
inv = self.Inventar(max_gewicht=15.0)
self.assertEqual(inv.max_gewicht, 15.0)
self.assertEqual(inv.gesamtgewicht, 0.0)
self.assertEqual(inv.freie_kapazitaet, 15.0)
# Ungültiges Maximalgewicht
with self.assertRaises(ValueError):
self.Inventar(max_gewicht=0.0)
with self.assertRaises(ValueError):
self.Inventar(max_gewicht=-10.0)
# Init mit Start-Gegenständen
g1 = self.Gegenstand("Buch", 1.2, 30)
g2 = self.Gegenstand("Fackel", 0.8, 5)
inv2 = self.Inventar(max_gewicht=10.0, gegenstaende=[g1, g2])
self.assertEqual(inv2.gesamtgewicht, 2.0)
self.assertEqual(inv2.freie_kapazitaet, 8.0)
self.assertEqual(len(inv2), 2)
# Start-Gegenstände zu schwer
schwer = self.Gegenstand("Amboss", 50.0, 500)
with self.assertRaises(ValueError, msg="Überladenes Startinventar muss ValueError werfen"):
self.Inventar(max_gewicht=20.0, gegenstaende=[schwer])
# ==========================================================================
# 5. Inventar - hinzufuegen mit *args
# ==========================================================================
def test_05_inventar_hinzufuegen_args(self):
"""Prüft hinzufuegen mit variablen Argumenten (*args), Unpacking und Überladung."""
inv = self.Inventar(max_gewicht=10.0)
g1 = self.Gegenstand("Trank", 0.5, 20)
g2 = self.Gegenstand("Brot", 0.3, 5)
g3 = self.Gegenstand("Dolch", 1.2, 80)
# Einzeln und mehrere mit *args
inv.hinzufuegen(g1)
self.assertEqual(inv.gesamtgewicht, 0.5)
inv.hinzufuegen(g2, g3)
self.assertEqual(inv.gesamtgewicht, 2.0)
self.assertEqual(len(inv), 3)
# Unpacking beim Aufruf von hinzufuegen
nachschub = [self.Gegenstand("Pfeil", 0.1, 1), self.Gegenstand("Bogen", 2.0, 100)]
inv.hinzufuegen(*nachschub)
self.assertEqual(inv.gesamtgewicht, 4.1)
self.assertEqual(len(inv), 5)
# Überladung prüfen
riesenstein = self.Gegenstand("Felsbrocken", 10.0, 0)
with self.assertRaises(ValueError, msg="Überschreiten des Maximalgewichts muss ValueError werfen"):
inv.hinzufuegen(riesenstein)
# Ungültiger Typ
with self.assertRaises(TypeError, msg="Nicht-Gegenstand muss TypeError werfen"):
inv.hinzufuegen("Kein Gegenstand")
# ==========================================================================
# 6. Container-Dunder: __len__ und __iter__
# ==========================================================================
def test_06_container_len_und_iter(self):
"""Prüft len() und Iterierbarkeit (for item in inventar:)."""
inv = self.Inventar(max_gewicht=20.0)
self.assertEqual(len(inv), 0)
items = [
self.Gegenstand("Item 1", 1.0, 10),
self.Gegenstand("Item 2", 2.0, 20),
self.Gegenstand("Item 3", 3.0, 30),
]
inv.hinzufuegen(*items)
self.assertEqual(len(inv), 3)
# Iteration mit for-Schleife & List Comprehension
namen = [item.name for item in inv]
self.assertEqual(namen, ["Item 1", "Item 2", "Item 3"])
# Konvertierung zu list(inv)
self.assertEqual(list(inv), items)
# ==========================================================================
# 7. Container-Dunder: __getitem__ (int Index & str Name-Suche)
# ==========================================================================
def test_07_container_getitem(self):
"""Prüft Index-Zugriff [i] und Namenssuche ['Name']."""
g1 = self.Gegenstand("Heiltrank", 0.5, 25)
g2 = self.Gegenstand("Langschwert", 4.0, 200)
g3 = self.Gegenstand("Manatrank", 0.5, 30)
inv = self.Inventar(max_gewicht=20.0, gegenstaende=[g1, g2, g3])
# Integer-Index
self.assertEqual(inv[0], g1)
self.assertEqual(inv[1], g2)
self.assertEqual(inv[-1], g3)
with self.assertRaises(IndexError):
_ = inv[10]
# String-Namenssuche (case-insensitive)
self.assertEqual(inv["Heiltrank"], g1)
self.assertEqual(inv["heiltrank"], g1)
self.assertEqual(inv["LANGSCHWERT"], g2)
with self.assertRaises(KeyError, msg="Unbekannter Name muss KeyError werfen"):
_ = inv["NichtImInventar"]
# Ungültiger Key-Typ
with self.assertRaises(TypeError):
_ = inv[3.14]
# ==========================================================================
# 8. Container-Dunder: __setitem__
# ==========================================================================
def test_08_container_setitem(self):
"""Prüft Ersetzen per Index [i] = item und Gewichtsvalidierung."""
g1 = self.Gegenstand("Heiltrank", 1.0, 25)
g2 = self.Gegenstand("Holzschild", 3.0, 40)
inv = self.Inventar(max_gewicht=10.0, gegenstaende=[g1, g2])
self.assertEqual(inv.gesamtgewicht, 4.0)
# Gültiger Austausch
superschild = self.Gegenstand("Stahlschild", 5.0, 150)
inv[1] = superschild
self.assertEqual(inv[1], superschild)
self.assertEqual(inv.gesamtgewicht, 6.0)
# Austausch, der Maximalgewicht sprengt (6 - 1 + 8 = 13 > 10)
riesenhammer = self.Gegenstand("Riesenhammer", 8.0, 300)
with self.assertRaises(ValueError, msg="Austausch über Maximalgewicht muss ValueError werfen"):
inv[0] = riesenhammer
# Typ- & Index-Prüfung
with self.assertRaises(TypeError):
inv["kein_int"] = superschild
with self.assertRaises(TypeError):
inv[0] = "Kein Gegenstand"
with self.assertRaises(IndexError):
inv[10] = superschild
# ==========================================================================
# 9. Container-Dunder: __delitem__
# ==========================================================================
def test_09_container_delitem(self):
"""Prüft Löschen per del inv[i] und del inv['Name']."""
g1 = self.Gegenstand("Heiltrank", 0.5, 20)
g2 = self.Gegenstand("Schwert", 3.5, 120)
g3 = self.Gegenstand("Fackel", 0.5, 5)
inv = self.Inventar(max_gewicht=20.0, gegenstaende=[g1, g2, g3])
self.assertEqual(len(inv), 3)
self.assertEqual(inv.gesamtgewicht, 4.5)
# Löschen per Integer-Index
del inv[0]
self.assertEqual(len(inv), 2)
self.assertEqual(inv[0], g2)
self.assertEqual(inv.gesamtgewicht, 4.0)
# Löschen per Name (case-insensitive)
del inv["fackel"]
self.assertEqual(len(inv), 1)
self.assertEqual(inv.gesamtgewicht, 3.5)
# Fehler beim Löschen
with self.assertRaises(KeyError):
del inv["Unbekannt"]
with self.assertRaises(IndexError):
del inv[99]
# ==========================================================================
# 10. Container-Dunder: __contains__ (in Operator)
# ==========================================================================
def test_10_container_contains(self):
"""Prüft Mitgliedschaftsprüfung ('Item' in inv und obj in inv)."""
schwert = self.Gegenstand("Eisenschwert", 3.0, 100)
trank = self.Gegenstand("Heiltrank", 0.5, 25)
fremdes_item = self.Gegenstand("Diamant", 0.1, 1000)
inv = self.Inventar(max_gewicht=20.0, gegenstaende=[schwert, trank])
# Suche nach String-Name
self.assertTrue("Eisenschwert" in inv)
self.assertTrue("eisenschwert" in inv)
self.assertTrue("HEILTRANK" in inv)
self.assertFalse("Diamant" in inv)
self.assertFalse("Drachenei" in inv)
# Suche nach Objekt-Referenz
self.assertTrue(schwert in inv)
self.assertTrue(trank in inv)
self.assertFalse(fremdes_item in inv)
# Suche nach Fremdtyp
self.assertFalse(12345 in inv)
# ==========================================================================
# 11. Helper-Funktion packe_inventar
# ==========================================================================
def test_11_packe_inventar_helper(self):
"""Prüft packe_inventar mit *args und max_gewicht."""
g1 = self.Gegenstand("Seil", 1.5, 15)
g2 = self.Gegenstand("Proviant", 2.0, 10)
rucksack = self.packe_inventar(g1, g2, max_gewicht=25.0)
self.assertIsInstance(rucksack, self.Inventar)
self.assertEqual(rucksack.max_gewicht, 25.0)
self.assertEqual(len(rucksack), 2)
self.assertEqual(rucksack.gesamtgewicht, 3.5)
self.assertTrue("Seil" in rucksack)
self.assertTrue("Proviant" in rucksack)
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -0,0 +1,178 @@
# Kapitel 16: Master-Abschlussprojekt – PetCare & Tierheim-Manager 🐾🏥🖥️
Herzlichen Glückwunsch! Du bist am **großen Finale der gesamten Python OOP-Übungsreihe** angekommen.
Hier führst du alles zusammen, was du in den drei Lehrpfaden gelernt hast – von Datentypen und Schleifen über Vererbung, Polymorphie, Exceptions und JSON-Persistenz bis hin zur interaktiven **Tkinter Desktop-Applikation** mit professioneller **Model-View-Controller (MVC)** Architektur.
---
## 🧭 Die 3 Lehrpfade im Masterprojekt vereint
```
[ 🏆 KAPITEL 16: MEISTERSTÜCK ]
│
┌───────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
[ 🛣️ LEHRPFAD 1: GRUNDLAGEN ] [ 🛣️ LEHRPFAD 2: OOP & TOOLS ] [ 🛣️ LEHRPFAD 3: SOFTWARETECHNIK ]
• Datentypen, Strings & Mathe • Klassen, __init__, self • Vererbung & super()
• Schleifen, Listen & Dicts • Methoden & Interaktionen • Polymorphie & Interfaces
• Typsichere Validierung • __str__ & Repräsentation • Eigene Exception-Klassen
• CSV-Tabellenexport • Modularer Aufbau • JSON Savegame-Persistenz
• Tkinter Desktop-GUI (MVC)
• 100% TDD / Unittest-Suite
```
---
## 🏛️ Die Architektur: Model-View-Controller (MVC)
In professioneller Software wird die **Geschäftslogik (Model)** strikt von der **grafischen Oberfläche (View & Controller)** getrennt.
Dadurch kann die gesamte Fachlogik zu 100% in automatischen Unittests ohne Bildschirm/Fenster getestet werden!
```
┌────────────────────────────────────────────────────────────────────────┐
│ BENUTZER │
└──────────────────┬─────────────────────────────────▲───────────────────┘
│ 1. Klickt Button / gibt Text ein│ 4. Sieht Update
▼ │
┌────────────────────────────────────────────────────┴───────────────────┐
│ VIEW & CONTROLLER (TierheimApp) │
│ • Tkinter-Fenster, Formular, Listbox, Aktionsbuttons, Statusleiste │
│ • Liest Formulareingaben aus & fängt ValidierungsFehler ab │
└──────────────────┬─────────────────────────────────▲───────────────────┘
│ 2. Ruft Methoden auf │ 3. Liefert Daten
▼ │
┌────────────────────────────────────────────────────┴───────────────────┐
│ MODEL (Tierheim) │
│ • Verwaltet Tierbestand (Hund, Katze, Vogel) │
│ • Filter (Art, ungeimpft, hungrig) & Statistiken (Ø Alter, Gewicht) │
│ • Persistenz: JSON Speichern/Laden & CSV-Export │
└────────────────────────────────────────────────────────────────────────┘
```
---
## 📊 UML-Klassendiagramm
```
┌──────────────────────────────────────────────┐
│ Exception: TierheimFehler │
└──────────────────────▲───────────────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
┌────────┴─────────────┐ ┌────────┴────────────┐ ┌─────────┴───────────────┐
│ ValidierungsFehler │ │TierNichtGefunden │ │KapazitaetUeberschritten │
└──────────────────────┘ └─────────────────────┘ └─────────────────────────┘
┌───────────────────────────────┐
│ «class» │
│ Tier │
├───────────────────────────────┤
│ + name: str │
│ + alter: int │
│ + gewicht: float │
│ + geimpft: bool │
│ + hunger: int (0..100) │
├───────────────────────────────┤
│ + fuettern(menge_g: int): str │
│ + impfen(): bool │
│ + mache_laut(): str │
│ + get_details(): str │
│ + to_dict(): dict │
│ + from_dict(data: dict): Tier │
└───────────────▲───────────────┘
│ (Vererbung)
┌─────────────────────────────────┼─────────────────────────────────┐
│ │ │
┌──────────────┴──────────────┐ ┌──────────────┴──────────────┐ ┌──────────────┴──────────────┐
│ Hund │ │ Katze │ │ Vogel │
├─────────────────────────────┤ ├─────────────────────────────┤ ├─────────────────────────────┤
│ + rasse: str │ │ + stubenrein: bool │ │ + spannweite_cm: float │
│ + gassigegangen: bool │ │ + kratzbaum_benutzt: bool │ │ + kann_sprechen: bool │
├─────────────────────────────┤ ├─────────────────────────────┤ ├─────────────────────────────┤
│ + gassi_gehen(minuten): str │ │ + kratzen(): str │ │ + fliegen(runden): str │
│ + mache_laut(): str (Wuff!) │ │ + mache_laut(): str (Miau!) │ │ + mache_laut(): str (Tsch!) │
└─────────────────────────────┘ └─────────────────────────────┘ └─────────────────────────────┘
▲
│ 1:n Komposition
┌────────────────────────────────────────────────┴────────────────────────┐
│ Tierheim │
├─────────────────────────────────────────────────────────────────────────┤
│ + name: str │
│ + max_kapazitaet: int │
│ + tiere: List[Tier] │
├─────────────────────────────────────────────────────────────────────────┤
│ + tier_aufnehmen(tier: Tier): None │
│ + tier_entlassen(name: str): Tier │
│ + finde_tier(name: str): Optional[Tier] │
│ + filtriere_nach_art(art: str): List[Tier] │
│ + ungeimpfte_tiere(): List[Tier] │
│ + hungrige_tiere(schwellenwert: int): List[Tier] │
│ + durchschnittsalter(): float │
│ + gesamtgewicht(): float │
│ + alle_fuettern(menge: int): dict │
│ + alle_impfen(): int │
│ + speichern_json(pfad: str): None │
│ + laden_json(pfad: str): None │
│ + exportiere_csv(pfad: str): None │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 📝 Aufgabenstellung (Schritt für Schritt)
Öffne die Datei `aufgabe.py` und bearbeite die nummerierten Abschnitte:
### Schritt 1: Eigene Exceptions definieren
Erstelle die Exception-Klassen:
- `TierheimFehler(Exception)` als Basis
- `ValidierungsFehler(TierheimFehler)`
- `TierNichtGefundenFehler(TierheimFehler)`
- `KapazitaetUeberschrittenFehler(TierheimFehler)`
### Schritt 2: Basisklasse `Tier` implementieren
- Validiere im `__init__`: `name` darf nicht leer sein, `alter >= 0`, `gewicht > 0`.
- Implementiere `fuettern(futtermenge_gramm)`: Senkt Hunger um `futtermenge_gramm * 0.2` (Minimum 0).
- Implementiere `impfen()`: Setzt `geimpft = True` (gibt `True` bei Neugeimpften, `False` bei bereits Geimpften).
- Implementiere `to_dict()` und die Factory-Methode `from_dict(cls, data)` (stellt automatisch die korrekte Kindklasse wieder her!).
### Schritt 3: Spezialisierte Kindklassen `Hund`, `Katze`, `Vogel`
- Nutze `super().__init__(...)` für gemeinsame Attribute.
- Überschreibe `mache_laut()` polymorph (Hund bellt, Katze miaut/schnurrt, Vogel zwitschert/plappert).
- Implementiere artspezifische Methoden (`gassi_gehen()`, `kratzen()`, `fliegen()`).
- Erweitere `to_dict()` um die Kindklassen-Attribute.
### Schritt 4: Das Model `Tierheim`
- Aufnahme von Tieren mit Kapazitätsprüfung (`max_kapazitaet`).
- Entlassen von Tieren nach Name (mit Exception-Handling falls nicht gefunden).
- Filterfunktionen (`filtriere_nach_art`, `ungeimpfte_tiere`, `hungrige_tiere`).
- Statistiken: `durchschnittsalter()`, `gesamtgewicht()`.
- Massenaktionen: `alle_fuettern()`, `alle_impfen()`.
- Persistenz: `speichern_json()`, `laden_json()` und `exportiere_csv()`.
### Schritt 5: Die Desktop-GUI `TierheimApp` (Tkinter)
- Verknüpfe Buttons und Formularfelder mit den Model-Methoden.
- Fange Fehler (`ValidierungsFehler`, etc.) in `try-except`-Blöcken ab und gib Feedback in der dynamischen Statusleiste.
- Halte die Listbox und Kapazitätsanzeige synchron mit dem Datenmodell.
---
## 🧪 Tests ausführen & App starten
### 1. Automatische Unittests ausführen:
```bash
python3 test_aufgabe.py
```
### 2. Desktop-App mit grafischer Oberfläche starten:
```bash
python3 app.py
```
---
## 💡 Best Practices für dein Meisterstück
1. **Never trust user input:** Prüfe Datentypen und Grenzwerte frühzeitig im Konstruktor.
2. **Polymorphie nutzen:** Anstatt mit vielen `if/elif`-Abfragen nach dem Typ zu unterscheiden, rufe einfach `tier.mache_laut()` oder `tier.fuettern()` auf!
3. **Model & View trennen:** Halte Berechnungen und Daten im `Tierheim`-Objekt und nutze Tkinter nur zur Anzeige.

View file

@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
Kapitel 16: Master-Abschlussprojekt – Desktop App Launcher 🐾🖥️
===============================================================
Startet die vollwertige grafische Desktop-Applikation 'Tierheim & PetCare Pro'.
Ausführen mit:
python3 app.py
"""
import sys
from pathlib import Path
# Pfad hinzufügen, um Module des Kapitels sauber zu importieren
sys.path.insert(0, str(Path(__file__).parent))
try:
import tkinter as tk
from tkinter import messagebox
TK_AVAILABLE = True
except ImportError:
TK_AVAILABLE = False
# Importiere Klassen aus aufgabe oder musterloesung
try:
from musterloesung import Tierheim, TierheimApp, Hund, Katze, Vogel
except ImportError:
from aufgabe import Tierheim, TierheimApp, Hund, Katze, Vogel
def starte_musterdaten_tierheim() -> Tierheim:
"""Erstellt ein Tierheim mit liebevoll gestalteten Beispieltieren."""
heim = Tierheim(name="Tierheim Sonnenschein & Pfotenglück", max_kapazitaet=15)
# Beispieldaten hinzufügen
bello = Hund("Bello", alter=3, gewicht=16.5, rasse="Golden Retriever", geimpft=True, hunger=30, gassigegangen=True)
luna = Katze("Luna", alter=2, gewicht=4.1, stubenrein=True, geimpft=True, hunger=40)
rocky = Hund("Rocky", alter=5, gewicht=22.0, rasse="Deutscher Schäferhund", geimpft=False, hunger=70)
mimi = Katze("Mimi", alter=1, gewicht=3.2, stubenrein=True, geimpft=False, hunger=60)
tweety = Vogel("Tweety", alter=1, gewicht=0.25, spannweite_cm=18.5, kann_sprechen=True, geimpft=True, hunger=20)
charly = Vogel("Charly", alter=4, gewicht=0.45, spannweite_cm=28.0, kann_sprechen=False, geimpft=False, hunger=80)
for t in (bello, luna, rocky, mimi, tweety, charly):
heim.tier_aufnehmen(t)
return heim
def main():
print("=" * 65)
print("🐾 Tierheim- & PetCare-Manager Pro (Master-Abschlussprojekt)")
print("=" * 65)
if not TK_AVAILABLE:
print("⚠️ Hinweis: Tkinter ist auf diesem System nicht verfügbar.")
print("💡 Führe 'python3 test_aufgabe.py' aus, um das System headless zu testen!")
return
try:
root = tk.Tk()
heim = starte_musterdaten_tierheim()
app = TierheimApp(root, heim)
print(f"✨ GUI erfolgreich gestartet mit {len(heim)} Beispieltieren.")
print("🖥️ Schließe das Fenster, um die Anwendung zu beenden.")
root.mainloop()
except Exception as e:
print(f"⚠️ Konnte GUI nicht im Display anzeigen: {e}")
print("💡 Auf Servern ohne Monitor (Headless) kannst du die Logik mit 'python3 test_aufgabe.py' prüfen.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,790 @@
"""
Kapitel 16: Master-Abschlussprojekt – Tierheim- & PetCare-Manager 🐾🏥
=====================================================================
Das große Meisterstück: Die Vereinigung aller drei Lehrpfade!
1. Modellierung & Vererbung:
- Basisklasse 'Tier' mit Polymorphie und Validierung
- Spezialisierte Kindklassen: 'Hund', 'Katze', 'Vogel'
2. Robuste Fehlerbehandlung:
- Eigene Exception-Hierarchie ('TierheimFehler', 'ValidierungsFehler', etc.)
3. Persistenz:
- Vollständiges JSON Savegame (Speichern & Laden mit Factory-Muster)
- Tabellarischer CSV-Export
4. Architektur & GUI (MVC):
- 'Tierheim' als reines Daten- & Geschäftslogik-Modell (Model)
- 'TierheimApp' als interaktive Tkinter Desktop-Applikation (View & Controller)
"""
from __future__ import annotations
import csv
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
# ==============================================================================
# Tkinter Import mit sicherem Fallback für Headless- & Server-Umgebungen
# ==============================================================================
try:
import tkinter as tk
from tkinter import ttk, messagebox
except ImportError:
# Headless Mock-Objekte (ermöglicht Ausführung und Tests ohne X11-Display)
class _MockWidget:
def __init__(self, master=None, text="", **kwargs):
self.master = master
self._config = dict(kwargs)
if text:
self._config["text"] = text
self._entry_text = str(kwargs.get("textvariable", text))
self._items: list[Any] = []
self._selection: list[int] = []
def config(self, **kwargs):
self._config.update(kwargs)
def configure(self, **kwargs):
self.config(**kwargs)
def cget(self, key):
return self._config.get(key, "")
def __getitem__(self, key):
return self._config.get(key, "")
def __setitem__(self, key, value):
self._config[key] = value
def grid(self, **kwargs):
pass
def pack(self, **kwargs):
pass
def get(self):
return self._entry_text
def set(self, val):
self._entry_text = str(val)
def delete(self, first=0, last=None):
if hasattr(self, "_items") and self._items:
self._items.clear()
self._entry_text = ""
def insert(self, index, string):
if hasattr(self, "_items"):
self._items.append(str(string))
self._entry_text = str(string)
def curselection(self):
return tuple(self._selection) if self._selection else (0,) if self._items else ()
def size(self):
return len(self._items)
def get_item(self, index):
if 0 <= index < len(self._items):
return self._items[index]
return ""
def invoke(self):
cmd = self._config.get("command")
if callable(cmd):
return cmd()
class _MockTk(_MockWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._title = ""
self._geometry = ""
def title(self, title: str):
self._title = title
def geometry(self, geom: str):
self._geometry = geom
def mainloop(self):
pass
def destroy(self):
pass
class _MockStringVar:
def __init__(self, value=""):
self._val = str(value)
def get(self):
return self._val
def set(self, val):
self._val = str(val)
class _MockBooleanVar:
def __init__(self, value=False):
self._val = bool(value)
def get(self):
return self._val
def set(self, val):
self._val = bool(val)
class _MockMessageBox:
@staticmethod
def showinfo(title, message):
pass
@staticmethod
def showwarning(title, message):
pass
@staticmethod
def showerror(title, message):
pass
class _MockTkModule:
Tk = _MockTk
Label = _MockWidget
Button = _MockWidget
Entry = _MockWidget
Frame = _MockWidget
Listbox = _MockWidget
Scrollbar = _MockWidget
Checkbutton = _MockWidget
StringVar = _MockStringVar
BooleanVar = _MockBooleanVar
OptionMenu = _MockWidget
END = "end"
SINGLE = "single"
BOTH = "both"
Y = "y"
W = "w"
E = "e"
class _MockTtkModule:
Combobox = _MockWidget
LabelFrame = _MockWidget
Button = _MockWidget
Label = _MockWidget
Entry = _MockWidget
Frame = _MockWidget
tk = _MockTkModule()
ttk = _MockTtkModule()
messagebox = _MockMessageBox()
# ==============================================================================
# TEIL 1: EIGENE EXCEPTION-HIERARCHIE
# ==============================================================================
class TierheimFehler(Exception):
"""Basis-Exception für alle fachlichen Fehler im Tierheim."""
pass
class ValidierungsFehler(TierheimFehler):
"""Wird ausgelöst, wenn eingegebene Daten (Name, Alter, Gewicht) unzulässig sind."""
pass
class TierNichtGefundenFehler(TierheimFehler):
"""Wird ausgelöst, wenn ein gesuchtes Tier nicht im Tierheim existiert."""
pass
class KapazitaetUeberschrittenFehler(TierheimFehler):
"""Wird ausgelöst, wenn das Tierheim voll ist und kein Tier mehr aufgenommen werden kann."""
pass
# ==============================================================================
# TEIL 2: DAS DATENMODELL (BASISKLASSE & KINDKLASSEN)
# ==============================================================================
class Tier:
"""
Abstrakte Basisklasse für alle Tiere im Tierheim.
Attribute:
name (str): Name des Tiers (darf nicht leer sein).
alter (int): Alter in Jahren (muss >= 0 sein).
gewicht (float): Gewicht in kg (muss > 0 sein).
geimpft (bool): Impfstatus (Standard: False).
hunger (int): Hunger-Level von 0 (satt) bis 100 (verhungert) (Standard: 50).
"""
def __init__(
self,
name: str,
alter: int,
gewicht: float,
geimpft: bool = False,
hunger: int = 50,
) -> None:
"""
Initialisiert ein Tier und validiert alle Attribute.
Löst ValidierungsFehler aus bei:
- leerem oder nur aus Leerzeichen bestehendem Namen
- alter < 0
- gewicht <= 0
Hunger wird auf den Bereich [0, 100] begrenzt (geclampt).
"""
# TODO: Validierung & Zuweisung implementieren
pass
def fuettern(self, futtermenge_gramm: int) -> str:
"""
Reduziert das Hunger-Level des Tiers.
Jede 100g Futter senken den Hunger um 20 Punkte (Minimum: 0).
Beispiel: Bei 250g Futter sinkt der Hunger um int(250 * 0.2) = 50.
Gibt einen Bestätigungstext zurück, z.B.:
"Bello wurde mit 200g gefüttert. Neuer Hunger-Level: 10/100."
"""
# TODO: Implementieren
pass
def impfen(self) -> bool:
"""
Setzt self.geimpft auf True.
Gibt True zurück, wenn das Tier frisch geimpft wurde,
oder False, falls es bereits geimpft war.
"""
# TODO: Implementieren
pass
def mache_laut(self) -> str:
"""Polymorphe Methode: Gibt den artspezifischen Laut des Tiers zurück."""
# Basis-Standard
return f"{self.name} macht ein unbestimmtes Geräusch."
def get_details(self) -> str:
"""Gibt eine Zusammenfassung der artspezifischen Zusatzmerkmale zurück."""
return "Keine speziellen Merkmale"
def to_dict(self) -> Dict[str, Any]:
"""
Wandelt das Tier in ein Dictionary für den JSON-Export um.
Enthält zwingend den Schlüssel 'art': 'Tier'.
"""
# TODO: Implementieren
pass
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> Tier:
"""
Factory-Methode: Erstellt anhand des 'art'-Schlüssels
die korrekte Instanz (Hund, Katze, Vogel oder Tier).
"""
# TODO: Implementieren
pass
def __str__(self) -> str:
impf_icon = "💉 Geimpft" if self.geimpft else "❌ Ungeimpft"
return f"[{self.__class__.__name__}] {self.name} ({self.alter} Jahre, {self.gewicht:.1f} kg, Hunger: {self.hunger}/100, {impf_icon})"
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name='{self.name}', alter={self.alter}, gewicht={self.gewicht}, geimpft={self.geimpft}, hunger={self.hunger})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Tier):
return False
return (
self.name == other.name
and self.alter == other.alter
and abs(self.gewicht - other.gewicht) < 0.001
and self.geimpft == other.geimpft
and self.hunger == other.hunger
)
class Hund(Tier):
"""
Spezialisierte Kindklasse für Hunde.
Zusatzattribute:
rasse (str): Hunderasse (Standard: "Mischling", darf nicht leer sein).
gassigegangen (bool): Ob der Hund heute schon Gassi war (Standard: False).
"""
def __init__(
self,
name: str,
alter: int,
gewicht: float,
rasse: str = "Mischling",
geimpft: bool = False,
hunger: int = 50,
gassigegangen: bool = False,
) -> None:
"""Initialisiert einen Hund mit super().__init__() und validiert rasse."""
# TODO: Implementieren
pass
def gassi_gehen(self, minuten: int) -> str:
"""
Führt den Hund Gassi.
- Setzt self.gassigegangen auf True.
- Erhöht den Hunger um 1 Punkt pro 5 Minuten (Hunger maximal 100).
- Gibt Rückgabetext zurück: "Bello war 30 Minuten Gassi! 🦮"
"""
# TODO: Implementieren
pass
def mache_laut(self) -> str:
"""Gibt Hundegebell zurück: '{self.name} bellt: Wuff! Wuff! 🐶'"""
# TODO: Implementieren
pass
def get_details(self) -> str:
status = "war Gassi" if self.gassigegangen else "muss noch raus"
return f"Rasse: {self.rasse} ({status})"
def to_dict(self) -> Dict[str, Any]:
"""Erweitert das Basis-Dictionary um 'art': 'Hund', 'rasse' und 'gassigegangen'."""
# TODO: Implementieren
pass
class Katze(Tier):
"""
Spezialisierte Kindklasse für Katzen.
Zusatzattribute:
stubenrein (bool): Ob die Katze ein Katzenklo benutzt (Standard: True).
kratzbaum_benutzt (bool): Ob sie heute gekratzt hat (Standard: False).
"""
def __init__(
self,
name: str,
alter: int,
gewicht: float,
stubenrein: bool = True,
geimpft: bool = False,
hunger: int = 50,
kratzbaum_benutzt: bool = False,
) -> None:
"""Initialisiert eine Katze mit super().__init__()."""
# TODO: Implementieren
pass
def kratzen(self) -> str:
"""
Katze wetzt ihre Krallen.
- Setzt self.kratzbaum_benutzt auf True.
- Gibt Rückgabetext zurück: "{self.name} wetzt die Krallen am Kratzbaum! 🐾"
"""
# TODO: Implementieren
pass
def mache_laut(self) -> str:
"""Gibt Katzenlaut zurück: '{self.name} schnurrt: Miau! Schnurr... 🐱'"""
# TODO: Implementieren
pass
def get_details(self) -> str:
sauber = "stubenrein" if self.stubenrein else "nicht stubenrein"
return f"Stubenrein: {sauber}"
def to_dict(self) -> Dict[str, Any]:
"""Erweitert das Basis-Dictionary um 'art': 'Katze', 'stubenrein' und 'kratzbaum_benutzt'."""
# TODO: Implementieren
pass
class Vogel(Tier):
"""
Spezialisierte Kindklasse für Vögel.
Zusatzattribute:
spannweite_cm (float): Flügelspannweite in cm (muss > 0 sein, Standard: 25.0).
kann_sprechen (bool): Kann der Vogel sprechen/nachplappern (Standard: False).
"""
def __init__(
self,
name: str,
alter: int,
gewicht: float,
spannweite_cm: float = 25.0,
kann_sprechen: bool = False,
geimpft: bool = False,
hunger: int = 50,
) -> None:
"""Initialisiert einen Vogel mit super().__init__() und validiert spannweite_cm."""
# TODO: Implementieren
pass
def fliegen(self, runden: int) -> str:
"""
Lässt den Vogel Runden in der Voliere fliegen.
- Erhöht den Hunger um 2 Punkte pro Runde (maximal 100).
- Gibt Text zurück: "{self.name} dreht {runden} elegante Runden in der Voliere! 🕊️"
"""
# TODO: Implementieren
pass
def mache_laut(self) -> str:
"""
Gibt Laut zurück:
- Falls self.kann_sprechen: "{self.name} plappert: 'Hallo Mensch! Tschilp!' 🦜"
- Sonst: "{self.name} zwitschert: Tschilp! Tschilp! 🐦"
"""
# TODO: Implementieren
pass
def get_details(self) -> str:
spricht = "kann sprechen" if self.kann_sprechen else "singt Melodien"
return f"Spannweite: {self.spannweite_cm:.1f} cm ({spricht})"
def to_dict(self) -> Dict[str, Any]:
"""Erweitert das Basis-Dictionary um 'art': 'Vogel', 'spannweite_cm' und 'kann_sprechen'."""
# TODO: Implementieren
pass
# ==============================================================================
# TEIL 3: DIE GESCHÄFTSLOGIK & DER VERWALTER (MODEL)
# ==============================================================================
class Tierheim:
"""
Das Model des Tierheims: Verwaltet Tierbestände, Kapazitäten,
Filter, Berechnungen und Dateipersistenz (JSON & CSV).
Komplett unabhängig von der Benutzeroberfläche (100% testbar).
"""
def __init__(self, name: str = "Tierheim Sonnenschein", max_kapazitaet: int = 20) -> None:
"""
Initialisiert das Tierheim.
- self.name = name
- self.max_kapazitaet = max_kapazitaet (muss > 0 sein, sonst ValidierungsFehler)
- self.tiere: List[Tier] = []
"""
# TODO: Implementieren
pass
def tier_aufnehmen(self, tier: Tier) -> None:
"""
Nimmt ein Tier auf.
- Löst ValidierungsFehler aus, wenn tier kein Objekt der Klasse Tier (oder Subklasse) ist.
- Löst KapazitaetUeberschrittenFehler aus, wenn len(self.tiere) >= self.max_kapazitaet.
- Fügt das Tier zu self.tiere hinzu.
"""
# TODO: Implementieren
pass
def tier_entlassen(self, name: str) -> Tier:
"""
Entlässt (vermittelt) ein Tier anhand seines Namens (case-insensitive).
- Findet das Tier, entfernt es aus self.tiere und gibt das entfernte Tier zurück.
- Löst TierNichtGefundenFehler aus, falls kein Tier mit diesem Namen existiert.
"""
# TODO: Implementieren
pass
def finde_tier(self, name: str) -> Optional[Tier]:
"""
Sucht ein Tier nach Namen (case-insensitive).
Gibt das Tier-Objekt zurück oder None, falls nicht gefunden.
"""
# TODO: Implementieren
pass
def filtriere_nach_art(self, art_name: str) -> List[Tier]:
"""
Gibt eine Liste aller Tiere zurück, deren Klassenname (z.B. 'Hund', 'Katze', 'Vogel')
mit art_name (case-insensitive) übereinstimmt.
Bei art_name == 'Alle' oder leerer Eingabe wird die gesamte Liste zurückgegeben.
"""
# TODO: Implementieren
pass
def ungeimpfte_tiere(self) -> List[Tier]:
"""Gibt eine Liste aller Tiere zurück, bei denen self.geimpft == False ist."""
# TODO: Implementieren
pass
def hungrige_tiere(self, schwellenwert: int = 50) -> List[Tier]:
"""Gibt alle Tiere zurück, deren Hunger-Level >= schwellenwert ist."""
# TODO: Implementieren
pass
def durchschnittsalter(self) -> float:
"""
Berechnet das durchschnittliche Alter aller Tiere im Heim.
Gibt 0.0 zurück, wenn das Tierheim leer ist.
"""
# TODO: Implementieren
pass
def gesamtgewicht(self) -> float:
"""
Berechnet das Gesamtgewicht aller Tiere im Heim.
Gibt 0.0 zurück, wenn das Tierheim leer ist.
"""
# TODO: Implementieren
pass
def alle_fuettern(self, menge_gramm: int = 100) -> Dict[str, str]:
"""
Füttert jedes Tier im Heim mit der angegebenen Futtermenge.
Gibt ein Dictionary {tier.name: bestaetigungstext} zurück.
"""
# TODO: Implementieren
pass
def alle_impfen(self) -> int:
"""
Impft alle noch ungeimpften Tiere im Heim.
Gibt die Anzahl der frisch geimpften Tiere zurück.
"""
# TODO: Implementieren
pass
def to_dict(self) -> Dict[str, Any]:
"""
Serialisiert das gesamte Tierheim in ein Dictionary.
Format:
{
"name": self.name,
"max_kapazitaet": self.max_kapazitaet,
"tiere": [tier.to_dict() for tier in self.tiere]
}
"""
# TODO: Implementieren
pass
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> Tierheim:
"""
Erstellt ein Tierheim-Objekt aus einem Dictionary
und rekonstruiert alle Tiere via Tier.from_dict().
"""
# TODO: Implementieren
pass
def speichern_json(self, dateipfad: Union[str, Path]) -> None:
"""
Speichert den gesamten Datenbestand als formatierte JSON-Datei (indent=2, utf-8).
"""
# TODO: Implementieren
pass
def laden_json(self, dateipfad: Union[str, Path]) -> None:
"""
Lädt Tierheimdaten aus einer JSON-Datei.
Aktualisiert Name, max_kapazitaet und ersetzt self.tiere mit den geladenen Tieren.
Löst FileNotFoundError aus, wenn die Datei nicht existiert.
"""
# TODO: Implementieren
pass
def exportiere_csv(self, dateipfad: Union[str, Path]) -> None:
"""
Exportiert den Tierbestand in eine CSV-Datei mit folgenden Spalten:
Art,Name,Alter,Gewicht,Geimpft,Hunger,Details
"""
# TODO: Implementieren
pass
def __len__(self) -> int:
return len(self.tiere)
def __str__(self) -> str:
return f"Tierheim '{self.name}' ({len(self.tiere)}/{self.max_kapazitaet} Tiere)"
# ==============================================================================
# TEIL 4: DIE DESKTOP-BENUTZEROBERFLÄCHE (VIEW & CONTROLLER)
# ==============================================================================
class TierheimApp:
"""
Die grafische Benutzeroberfläche (Tkinter) nach dem MVC-Muster.
Verbindet GUI-Ereignisse mit den Methoden der Model-Klasse Tierheim.
"""
def __init__(self, root: Any, tierheim: Optional[Tierheim] = None) -> None:
"""
Initialisiert das GUI-Fenster und alle Widgets.
"""
self.root = root
self.tierheim = tierheim if tierheim is not None else Tierheim()
if hasattr(self.root, "title"):
self.root.title("🐾 PetCare & Tierheim-Manager Pro")
if hasattr(self.root, "geometry"):
self.root.geometry("850x620")
self._aktiver_filter = "Alle"
self._erzeuge_widgets()
self.aktualisiere_ansicht()
def _erzeuge_widgets(self) -> None:
"""Erzeugt alle visuellen Komponenten (Formular, Liste, Buttons, Statusleiste)."""
self.label_titel = tk.Label(
self.root,
text=f"🏥 {self.tierheim.name}",
font=("Arial", 16, "bold"),
)
self.label_titel.pack(pady=5)
self.label_kapazitaet = tk.Label(
self.root,
text="Belegung: 0 / 20 Tiere",
font=("Arial", 11),
)
self.label_kapazitaet.pack(pady=2)
# Formular-Variablen
self.var_art = tk.StringVar(value="Hund")
self.var_name = tk.StringVar()
self.var_alter = tk.StringVar(value="2")
self.var_gewicht = tk.StringVar(value="10.5")
self.var_extra = tk.StringVar(value="Golden Retriever")
self.var_geimpft = tk.BooleanVar(value=False)
# Listbox für Tierbestand
self.listbox_tiere = tk.Listbox(self.root, font=("Courier", 10), height=12)
self.listbox_tiere.pack(fill=tk.BOTH, expand=True, padx=15, pady=5)
# Statusleiste
self.label_status = tk.Label(
self.root,
text="Bereit.",
bd=1,
relief="sunken",
anchor="w",
font=("Arial", 9),
)
self.label_status.pack(side="bottom", fill="x")
def aktualisiere_ansicht(self) -> None:
"""
Aktualisiert die Anzeige in der Listbox und den Kapazitätszähler
basierend auf dem aktuellen Datenmodell und Filter.
"""
# 1. Kapazitätstext aktualisieren
if hasattr(self.label_kapazitaet, "config"):
self.label_kapazitaet.config(
text=f"Belegung: {len(self.tierheim)} / {self.tierheim.max_kapazitaet} Tiere (Ø {self.tierheim.durchschnittsalter():.1f} Jahre, Σ {self.tierheim.gesamtgewicht():.1f} kg)"
)
# 2. Listbox leeren und neu befüllen
self.listbox_tiere.delete(0, tk.END)
if self._aktiver_filter == "Ungeimpft":
tiere_anzeige = self.tierheim.ungeimpfte_tiere()
elif self._aktiver_filter == "Hungrig":
tiere_anzeige = self.tierheim.hungrige_tiere()
elif self._aktiver_filter in ("Hund", "Katze", "Vogel"):
tiere_anzeige = self.tierheim.filtriere_nach_art(self._aktiver_filter)
else:
tiere_anzeige = self.tierheim.tiere
for tier in tiere_anzeige:
impf_symbol = "💉" if tier.geimpft else "❌"
eintrag = f"{tier.__class__.__name__:5s} | {tier.name:12s} | {tier.alter:2d} J. | {tier.gewicht:5.1f} kg | Hunger: {tier.hunger:3d}% | {impf_symbol} | {tier.get_details()}"
self.listbox_tiere.insert(tk.END, eintrag)
def setze_status(self, nachricht: str, typ: str = "info") -> None:
"""Setzt den Text und die Farbe der Statusleiste."""
farben = {
"info": "#1e293b",
"success": "#166534",
"warning": "#854d0e",
"error": "#991b1b",
}
icon = {"info": "ℹ️ ", "success": "✅ ", "warning": "⚠️ ", "error": "❌ "}.get(typ, "")
if hasattr(self.label_status, "config"):
self.label_status.config(text=f"{icon}{nachricht}", fg=farben.get(typ, "#1e293b"))
def get_ausgewaehltes_tier(self) -> Optional[Tier]:
"""Ermittelt das aktuell in der Listbox ausgewählte Tier-Objekt."""
try:
selektion = self.listbox_tiere.curselection()
if not selektion:
return None
index = selektion[0]
if 0 <= index < len(self.tierheim.tiere):
return self.tierheim.tiere[index]
return None
except Exception:
return None
def tier_aufnehmen_klick(self) -> None:
"""
Liest die Formulardaten aus, instanziiert das passende Tier-Objekt (Hund, Katze, Vogel)
und fügt es dem Tierheim hinzu. Fängt ValidierungsFehler und KapazitaetUeberschrittenFehler ab.
"""
# TODO: Implementieren
pass
def tier_fuettern_klick(self) -> None:
"""Füttert das ausgewählte Tier und aktualisiert die Ansicht."""
# TODO: Implementieren
pass
def tier_impfen_klick(self) -> None:
"""Impft das ausgewählte Tier und aktualisiert die Ansicht."""
# TODO: Implementieren
pass
def tier_laut_klick(self) -> None:
"""Lässt das ausgewählte Tier seinen artspezifischen Laut von sich geben."""
# TODO: Implementieren
pass
def tier_vermitteln_klick(self) -> None:
"""Vermittelt (entlässt) das ausgewählte Tier aus dem Heim."""
# TODO: Implementieren
pass
def speichern_klick(self, dateipfad: str = "tierheim_save.json") -> None:
"""Speichert den aktuellen Stand als JSON-Datei."""
try:
self.tierheim.speichern_json(dateipfad)
self.setze_status(f"Erfolgreich gespeichert in '{dateipfad}'.", typ="success")
except Exception as e:
self.setze_status(f"Fehler beim Speichern: {e}", typ="error")
def laden_klick(self, dateipfad: str = "tierheim_save.json") -> None:
"""Lädt den Stand aus einer JSON-Datei."""
try:
self.tierheim.laden_json(dateipfad)
self.aktualisiere_ansicht()
self.setze_status(f"Erfolgreich geladen aus '{dateipfad}'.", typ="success")
except Exception as e:
self.setze_status(f"Fehler beim Laden: {e}", typ="error")
def csv_export_klick(self, dateipfad: str = "tierheim_export.csv") -> None:
"""Exportiert den Tierbestand als CSV-Tabelle."""
try:
self.tierheim.exportiere_csv(dateipfad)
self.setze_status(f"CSV-Tabelle erfolgreich exportiert nach '{dateipfad}'.", typ="success")
except Exception as e:
self.setze_status(f"Fehler beim CSV-Export: {e}", typ="error")
# ==============================================================================
# HAUPTPROGRAMM (FÜR MANUELLES TESTEN)
# ==============================================================================
if __name__ == "__main__":
print("🐾 Initialisiere Tierheim Sonnenschein...")
heim = Tierheim("Tierheim Sonnenschein", max_kapazitaet=10)
try:
bello = Hund("Bello", alter=3, gewicht=14.5, rasse="Beagle")
luna = Katze("Luna", alter=2, gewicht=4.2, stubenrein=True)
tweety = Vogel("Tweety", alter=1, gewicht=0.3, spannweite_cm=18.0, kann_sprechen=True)
heim.tier_aufnehmen(bello)
heim.tier_aufnehmen(luna)
heim.tier_aufnehmen(tweety)
print(f"✅ {heim}")
for t in heim.tiere:
print(f" • {t} -> Laut: {t.mache_laut()}")
except TierheimFehler as e:
print(f"⚠️ Tierheim-Fehler: {e}")

View file

@ -0,0 +1,327 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kapitel 16: Master-Abschlussprojekt – PetCare & Tierheim-Manager</title>
<link rel="stylesheet" href="../assets/style.css">
<style>
.gold-header {
background: linear-gradient(135deg, #4338ca 0%, #6366f1 50%, #8b5cf6 100%);
}
.mvc-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
margin: 20px 0;
}
.mvc-card {
background: #ffffff;
border-radius: var(--radius-md);
padding: 18px;
border: 2px solid var(--border-color);
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
}
.mvc-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.mvc-card.model { border-top: 5px solid #10b981; }
.mvc-card.view { border-top: 5px solid #3b82f6; }
.mvc-card.controller { border-top: 5px solid #8b5cf6; }
.badge-master {
background: linear-gradient(90deg, #f59e0b, #ec4899);
color: white;
font-weight: 700;
padding: 4px 14px;
border-radius: 999px;
display: inline-block;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
</style>
</head>
<body>
<header class="gold-header">
<div class="header-container">
<nav class="nav-links">
<a href="../index.html">🏠 Zur Kursübersicht</a>
<span>&rsaquo;</span>
<a href="../15_parameter_und_container/index.html">Kapitel 15</a>
<span>&rsaquo;</span>
<span style="color: white; font-weight: 600;">Kapitel 16 (Masterprojekt)</span>
</nav>
<div class="header-badge">🏆 Kapitel 16 &bull; Das Große Meisterstück</div>
<h1>PetCare &amp; Tierheim-Manager Pro 🐾🏥🖥️</h1>
<p>Die Krönung aller 3 Lehrpfade: Model-View-Controller (MVC), Vererbung, Polymorphie, JSON- &amp; CSV-Persistenz, robuste Exceptions und eine vollwertige Tkinter Desktop-Applikation!</p>
</div>
</header>
<main class="container">
<!-- 1. Motivierendes Intro & Lehrpfad-Konsolidierung -->
<section class="card">
<h2>1. 🏆 Dein Meisterstück: Alle 3 Lehrpfade vereint!</h2>
<p>
Herzlichen Glückwunsch zu diesem Meilenstein! Du hast gelernt, wie man Variablen deklariert, Schleifen baut, Klassen als Baupläne entwirft, Datenstrukturen serialisiert und echte Desktop-Fenster mit Tkinter zum Leben erweckt.
</p>
<p>
In diesem <strong>Master-Abschlussprojekt</strong> baust du eine vollwertige, professionelle Software für ein Tierheim: Den <strong>PetCare- &amp; Tierheim-Manager</strong>. Hier greifen alle Rädchen nahtlos ineinander:
</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 15px; margin: 20px 0;">
<div style="background: #f0fdf4; border-left: 4px solid #10b981; padding: 14px; border-radius: var(--radius-sm);">
<h4 style="color: #166534; margin-bottom: 6px;">🛣️ Lehrpfad 1: Grundlagen</h4>
<p style="font-size: 0.88rem; color: var(--text-muted); margin: 0;">
Typsichere Datenverarbeitung, String-Formatierung für Berichte, Rechnen von Statistiken (Mittelwert, Summen) und sauberer CSV-Export.
</p>
</div>
<div style="background: #eff6ff; border-left: 4px solid #3b82f6; padding: 14px; border-radius: var(--radius-sm);">
<h4 style="color: #1e40af; margin-bottom: 6px;">🛣️ Lehrpfad 2: OOP &amp; Clean Code</h4>
<p style="font-size: 0.88rem; color: var(--text-muted); margin: 0;">
Objekt-Instanziierung, Kapselung, <code>self</code>, <code>__str__</code>, <code>__repr__</code>, 1:n Beziehungen und modulare Methoden.
</p>
</div>
<div style="background: #f5f3ff; border-left: 4px solid #8b5cf6; padding: 14px; border-radius: var(--radius-sm);">
<h4 style="color: #5b21b6; margin-bottom: 6px;">🛣️ Lehrpfad 3: Professionelle Software</h4>
<p style="font-size: 0.88rem; color: var(--text-muted); margin: 0;">
Vererbung (<code>super()</code>), Polymorphie (Tierlaute), eigene Exceptions, JSON-Savegames, Tkinter Desktop-GUI und 100% Unittest-Abdeckung!
</p>
</div>
</div>
</section>
<!-- 2. MVC Architektur -->
<section class="card">
<h2>2. 🏛️ Die Architektur: Model-View-Controller (MVC)</h2>
<p>
In professionellen Software-Unternehmen schreibt man niemals Geschäftslogik und Benutzeroberfläche in einen einzigen großen Code-Klotz ("Spaghetti-Code"). Stattdessen trennt man das System strikt nach dem <strong>MVC-Muster</strong>:
</p>
<div class="mvc-grid">
<div class="mvc-card model">
<span class="badge-master" style="background: #10b981;">1. Das Model (Daten &amp; Logik)</span>
<h3 style="margin-top: 10px; color: #065f46;">Klassen <code>Tier</code> &amp; <code>Tierheim</code></h3>
<p style="font-size: 0.9rem;">
Verwaltet alle Tiere, berechnet Statistiken (Ø Alter, Gewicht), filtert Bestände und speichert/lädt JSON &amp; CSV.
</p>
<div class="box box-tipp" style="margin-top: 10px; font-size: 0.82rem; padding: 8px;">
💡 <strong>100% Headless testbar:</strong> Weiß nichts von Tkinter und läuft komplett ohne Bildschirm!
</div>
</div>
<div class="mvc-card view">
<span class="badge-master" style="background: #3b82f6;">2. Die View (Anzeige)</span>
<h3 style="margin-top: 10px; color: #1e40af;">Tkinter Widgets</h3>
<p style="font-size: 0.9rem;">
Das visuelle Fenster: Header, Formular-Eingabefelder, Listbox für den Tierbestand, Aktionsbuttons und die dynamische Statusleiste.
</p>
<div class="box box-tipp" style="margin-top: 10px; font-size: 0.82rem; padding: 8px;">
👀 <strong>Dumb View:</strong> Zeigt nur an, was das Model ihr mitteilt.
</div>
</div>
<div class="mvc-card controller">
<span class="badge-master" style="background: #8b5cf6;">3. Der Controller (Steuerung)</span>
<h3 style="margin-top: 10px; color: #5b21b6;">Klasse <code>TierheimApp</code></h3>
<p style="font-size: 0.9rem;">
Reagiert auf Benutzerklicks, validiert Eingaben mit <code>try-except</code>, ruft Methoden des Tierheims auf und aktualisiert die Anzeige.
</p>
<div class="box box-tipp" style="margin-top: 10px; font-size: 0.82rem; padding: 8px;">
🛡️ <strong>Fehler-Abfangjäger:</strong> Verhindert App-Abstürze bei Fehleingaben.
</div>
</div>
</div>
<div class="box box-analogie">
<div class="box-title">🍽️ Die Restaurant-Analogie für MVC:</div>
<ul>
<li><strong>Model (Die Küche):</strong> Kocht das Essen, verwaltet die Zutaten und Rezepte (Logik &amp; Daten).</li>
<li><strong>View (Die Speisekarte &amp; Teller):</strong> Das, was der Gast sieht und liest (Grafik).</li>
<li><strong>Controller (Der Kellner):</strong> Nimmt die Bestellung vom Gast auf, gibt sie an die Küche weiter und bringt das fertige Gericht an den Tisch!</li>
</ul>
</div>
</section>
<!-- 3. Klassendiagramm & Vererbung -->
<section class="card">
<h2>3. 🧬 Das Datenmodell: Vererbung &amp; Polymorphie</h2>
<p>
Ein Tierheim beherbergt verschiedene Tierarten: <strong>Hunde</strong>, <strong>Katzen</strong> und <strong>Vögel</strong>.
Sie alle teilen gemeinsame Eigenschaften (Name, Alter, Gewicht, Impfstatus, Hunger), besitzen jedoch jeweils eigene Spezialmerkmale und Verhaltensweisen:
</p>
<div class="code-container">
<div class="code-header">
<div class="code-dots">
<span class="code-dot dot-red"></span>
<span class="code-dot dot-yellow"></span>
<span class="code-dot dot-green"></span>
</div>
<span>Python (Vererbungshierarchie &amp; super())</span>
</div>
<pre><span class="kw">class</span> <span class="cls">Tier</span>:
<span class="kw">def</span> <span class="fn">__init__</span>(<span class="var">self</span>, <span class="var">name</span>: <span class="cls">str</span>, <span class="var">alter</span>: <span class="cls">int</span>, <span class="var">gewicht</span>: <span class="cls">float</span>, <span class="var">geimpft</span>: <span class="cls">bool</span> = <span class="kw">False</span>, <span class="var">hunger</span>: <span class="cls">int</span> = <span class="num">50</span>):
<span class="kw">if</span> <span class="kw">not</span> <span class="var">name</span>.<span class="fn">strip</span>():
<span class="kw">raise</span> <span class="cls">ValidierungsFehler</span>(<span class="str">"Name darf nicht leer sein!"</span>)
<span class="var">self</span>.<span class="attr">name</span> = <span class="var">name</span>.<span class="fn">strip</span>()
<span class="var">self</span>.<span class="attr">alter</span> = <span class="var">alter</span>
<span class="var">self</span>.<span class="attr">gewicht</span> = <span class="var">gewicht</span>
<span class="var">self</span>.<span class="attr">geimpft</span> = <span class="var">geimpft</span>
<span class="var">self</span>.<span class="attr">hunger</span> = <span class="fn">max</span>(<span class="num">0</span>, <span class="fn">min</span>(<span class="num">100</span>, <span class="var">hunger</span>))
<span class="kw">def</span> <span class="fn">mache_laut</span>(<span class="var">self</span>) -> <span class="cls">str</span>:
<span class="kw">return</span> <span class="str">f"{self.name} macht ein Geräusch."</span>
<span class="kw">class</span> <span class="cls">Hund</span>(<span class="cls">Tier</span>):
<span class="kw">def</span> <span class="fn">__init__</span>(<span class="var">self</span>, <span class="var">name</span>: <span class="cls">str</span>, <span class="var">alter</span>: <span class="cls">int</span>, <span class="var">gewicht</span>: <span class="cls">float</span>, <span class="var">rasse</span>: <span class="cls">str</span> = <span class="str">"Mischling"</span>, ...):
<span class="fn">super</span>().<span class="fn">__init__</span>(<span class="var">name</span>, <span class="var">alter</span>, <span class="var">gewicht</span>, ...)
<span class="var">self</span>.<span class="attr">rasse</span> = <span class="var">rasse</span>
<span class="var">self</span>.<span class="attr">gassigegangen</span> = <span class="kw">False</span>
<span class="kw">def</span> <span class="fn">mache_laut</span>(<span class="var">self</span>) -> <span class="cls">str</span>:
<span class="kw">return</span> <span class="str">f"{self.name} bellt: Wuff! Wuff! 🐶"</span></pre>
</div>
<div class="box box-merke">
<div class="box-title">🎭 Polymorphie in der Praxis:</div>
Wenn wir im Tierheim <code>for tier in self.tiere: print(tier.mache_laut())</code> aufrufen, weiß Python dank Polymorphie zur Laufzeit automatisch, welches Tier welchen Laut von sich gibt – ohne eine einzige <code>if/elif</code>-Typabfrage!
</div>
</section>
<!-- 4. Persistenz & Factory-Muster -->
<section class="card">
<h2>4. 💾 Savegame-Persistenz mit dem Factory-Muster</h2>
<p>
Beim Speichern als JSON-Datei wandeln wir jedes Tier-Objekt in ein Dictionary um und ergänzen den Schlüssel <code>"art": "Hund"</code>.
Beim Laden liest die <strong>Factory-Methode</strong> <code>Tier.from_dict(data)</code> diesen Schlüssel aus und erzeugt automatisch die exakte Unterklasse:
</p>
<div class="code-container">
<div class="code-header">
<div class="code-dots">
<span class="code-dot dot-red"></span>
<span class="code-dot dot-yellow"></span>
<span class="code-dot dot-green"></span>
</div>
<span>Python (Factory-Methode mit Type-Discriminator)</span>
</div>
<pre><span class="kw">@classmethod</span>
<span class="kw">def</span> <span class="fn">from_dict</span>(<span class="var">cls</span>, <span class="var">data</span>: <span class="cls">dict</span>) -> <span class="cls">Tier</span>:
<span class="var">art</span> = <span class="var">data</span>.<span class="fn">get</span>(<span class="str">"art"</span>, <span class="str">"Tier"</span>)
<span class="kw">if</span> <span class="var">art</span> == <span class="str">"Hund"</span>:
<span class="kw">return</span> <span class="cls">Hund</span>(<span class="var">name</span>=<span class="var">data</span>[<span class="str">"name"</span>], <span class="var">alter</span>=<span class="var">data</span>[<span class="str">"alter"</span>], <span class="var">gewicht</span>=<span class="var">data</span>[<span class="str">"gewicht"</span>], <span class="var">rasse</span>=<span class="var">data</span>.<span class="fn">get</span>(<span class="str">"rasse"</span>, <span class="str">"Mischling"</span>), ...)
<span class="kw">elif</span> <span class="var">art</span> == <span class="str">"Katze"</span>:
<span class="kw">return</span> <span class="cls">Katze</span>(...)
<span class="kw">elif</span> <span class="var">art</span> == <span class="str">"Vogel"</span>:
<span class="kw">return</span> <span class="cls">Vogel</span>(...)
<span class="kw">else</span>:
<span class="kw">return</span> <span class="cls">Tier</span>(...)</pre>
</div>
</section>
<!-- 5. Projekt-Checkliste -->
<section class="card">
<h2>5. ✅ Deine Projekt-Checkliste</h2>
<p>Arbeite die folgenden 5 Abschnitte in der Datei <code>aufgabe.py</code> der Reihe nach ab:</p>
<ul class="checklist">
<li class="check-item">
<strong>1. Eigene Exceptions:</strong> <code>TierheimFehler</code>, <code>ValidierungsFehler</code>, <code>TierNichtGefundenFehler</code>, <code>KapazitaetUeberschrittenFehler</code>.
</li>
<li class="check-item">
<strong>2. Basisklasse <code>Tier</code>:</strong> Validierung von Name, Alter, Gewicht im <code>__init__</code>, Methoden <code>fuettern()</code>, <code>impfen()</code>, <code>to_dict()</code> und Factory-Methode <code>from_dict()</code>.
</li>
<li class="check-item">
<strong>3. Kindklassen <code>Hund</code>, <code>Katze</code>, <code>Vogel</code>:</strong> Vererbung mit <code>super()</code>, artspezifische Attribute &amp; Methoden (<code>gassi_gehen()</code>, <code>kratzen()</code>, <code>fliegen()</code>) und polymorphes <code>mache_laut()</code>.
</li>
<li class="check-item">
<strong>4. Geschäftslogik <code>Tierheim</code>:</strong> Tieraufnahme mit Kapazitätsprüfung, Vermittlung/Entlassen, Filter nach Art/Impfung/Hunger, Statistiken (Ø Alter, Gewicht) und JSON-/CSV-Persistenz.
</li>
<li class="check-item">
<strong>5. Benutzeroberfläche <code>TierheimApp</code>:</strong> Tkinter-Events mit dem Model verknüpfen, try-except Fehlerabfang und dynamische Statusleisten-Meldungen.
</li>
</ul>
</section>
<!-- 6. Lösungstipps & Details -->
<section class="card">
<h2>6. 💡 Ausklappbare Hilfestellungen &amp; Tipps</h2>
<details>
<summary>🔍 Tipp zu Schritt 1: Eigene Exceptions definieren</summary>
<div class="details-content">
<p>Eigene Exception-Klassen erben in Python einfach von <code>Exception</code> oder einer übergeordneten Basis-Exception:</p>
<div class="code-container">
<pre><span class="kw">class</span> <span class="cls">TierheimFehler</span>(<span class="cls">Exception</span>):
<span class="kw">pass</span>
<span class="kw">class</span> <span class="cls">ValidierungsFehler</span>(<span class="cls">TierheimFehler</span>):
<span class="kw">pass</span></pre>
</div>
</div>
</details>
<details>
<summary>🔍 Tipp zu Schritt 4: CSV-Export mit dem csv-Modul</summary>
<div class="details-content">
<p>Nutze <code>csv.writer</code> mit <code>newline=""</code> für saubere Tabellenausgabe:</p>
<div class="code-container">
<pre><span class="kw">with</span> <span class="fn">open</span>(<span class="var">dateipfad</span>, <span class="str">"w"</span>, <span class="var">newline</span>=<span class="str">""</span>, <span class="var">encoding</span>=<span class="str">"utf-8"</span>) <span class="kw">as</span> <span class="var">f</span>:
<span class="var">writer</span> = <span class="var">csv</span>.<span class="fn">writer</span>(<span class="var">f</span>)
<span class="var">writer</span>.<span class="fn">writerow</span>([<span class="str">"Art"</span>, <span class="str">"Name"</span>, <span class="str">"Alter"</span>, <span class="str">"Gewicht"</span>, <span class="str">"Geimpft"</span>, <span class="str">"Hunger"</span>, <span class="str">"Details"</span>])
<span class="kw">for</span> <span class="var">t</span> <span class="kw">in</span> <span class="var">self</span>.<span class="attr">tiere</span>:
<span class="var">writer</span>.<span class="fn">writerow</span>([<span class="var">t</span>.<span class="var">__class__</span>.<span class="var">__name__</span>, <span class="var">t</span>.<span class="attr">name</span>, <span class="var">t</span>.<span class="attr">alter</span>, <span class="str">f"{t.gewicht:.1f}"</span>, <span class="str">"Ja"</span> <span class="kw">if</span> <span class="var">t</span>.<span class="attr">geimpft</span> <span class="kw">else</span> <span class="str">"Nein"</span>, <span class="str">f"{t.hunger}%"</span>, <span class="var">t</span>.<span class="fn">get_details</span>()])</pre>
</div>
</div>
</details>
<details>
<summary>🔍 Tipp zu Schritt 5: Fehler im GUI-Controller sanft abfangen</summary>
<div class="details-content">
<p>Damit die Desktop-App bei einer Fehleingabe nicht abstürzt, fange Exceptions im Event-Handler ab:</p>
<div class="code-container">
<pre><span class="kw">try</span>:
<span class="var">self</span>.<span class="attr">tierheim</span>.<span class="fn">tier_aufnehmen</span>(<span class="var">neues_tier</span>)
<span class="var">self</span>.<span class="fn">aktualisiere_ansicht</span>()
<span class="var">self</span>.<span class="fn">setze_status</span>(<span class="str">f"{neues_tier.name} aufgenommen! 🎉"</span>, <span class="var">typ</span>=<span class="str">"success"</span>)
<span class="kw">except</span> <span class="cls">TierheimFehler</span> <span class="kw">as</span> <span class="var">e</span>:
<span class="var">self</span>.<span class="fn">setze_status</span>(<span class="fn">str</span>(<span class="var">e</span>), <span class="var">typ</span>=<span class="str">"warning"</span>)</pre>
</div>
</div>
</details>
</section>
<!-- 7. Testen & Starten -->
<section class="card">
<h2>7. 🧪 Lösung überprüfen &amp; App starten</h2>
<h3>1. Automatische Unittests ausführen:</h3>
<div class="code-container">
<div class="code-header">
<span>Terminal (im Ordner 16_master_abschlussprojekt)</span>
</div>
<pre><code>python3 test_aufgabe.py</code></pre>
</div>
<h3>2. Grafische Desktop-Applikation starten:</h3>
<div class="code-container">
<div class="code-header">
<span>Terminal</span>
</div>
<pre><code>python3 app.py</code></pre>
</div>
<div class="footer-nav">
<a href="../15_parameter_und_container/index.html" class="btn btn-secondary">&larr; Zurück zu Kapitel 15</a>
<a href="../index.html" class="btn">🏆 Kurs abgeschlossen &ndash; Zur Kursübersicht &rarr;</a>
</div>
</section>
</main>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,427 @@
#!/usr/bin/env python3
"""
🧪 Umfassende Unittest-Suite für Kapitel 16 (Master-Abschlussprojekt)
=====================================================================
Prüft:
- Teil 1: Exception-Hierarchie & Vererbung
- Teil 2: Datenmodelle (Basisklasse Tier & Kindklassen Hund, Katze, Vogel)
- Teil 3: Polymorphie & Factory-Muster (Tier.from_dict)
- Teil 4: Geschäftslogik (Tierheim, Kapazität, Filter, Massenoperationen, Statistik)
- Teil 5: Persistenz (JSON Savegame-Roundtrip & CSV-Export)
- Teil 6: GUI-Controller (TierheimApp im Headless-Modus)
"""
import csv
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
# Aktuellen Ordner in sys.path einfügen
sys.path.insert(0, str(Path(__file__).parent))
try:
import aufgabe
except ImportError as e:
raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}")
class Test01Exceptions(unittest.TestCase):
"""Prüft die Exception-Klassen und deren Vererbungshierarchie."""
def test_01_exceptions_existieren_und_erben(self):
self.assertTrue(hasattr(aufgabe, "TierheimFehler"), "TierheimFehler fehlt!")
self.assertTrue(hasattr(aufgabe, "ValidierungsFehler"), "ValidierungsFehler fehlt!")
self.assertTrue(hasattr(aufgabe, "TierNichtGefundenFehler"), "TierNichtGefundenFehler fehlt!")
self.assertTrue(hasattr(aufgabe, "KapazitaetUeberschrittenFehler"), "KapazitaetUeberschrittenFehler fehlt!")
self.assertTrue(issubclass(aufgabe.TierheimFehler, Exception))
self.assertTrue(issubclass(aufgabe.ValidierungsFehler, aufgabe.TierheimFehler))
self.assertTrue(issubclass(aufgabe.TierNichtGefundenFehler, aufgabe.TierheimFehler))
self.assertTrue(issubclass(aufgabe.KapazitaetUeberschrittenFehler, aufgabe.TierheimFehler))
class Test02TierBasisklasse(unittest.TestCase):
"""Prüft die Basisklasse Tier inkl. Validierung und Basis-Methoden."""
def test_02_tier_initialisierung_und_attribute(self):
t = aufgabe.Tier("Sammy", alter=4, gewicht=12.5, geimpft=False, hunger=40)
self.assertEqual(t.name, "Sammy")
self.assertEqual(t.alter, 4)
self.assertAlmostEqual(t.gewicht, 12.5)
self.assertFalse(t.geimpft)
self.assertEqual(t.hunger, 40)
def test_03_tier_validierung_leerer_name(self):
with self.assertRaises(aufgabe.ValidierungsFehler):
aufgabe.Tier("", alter=3, gewicht=5.0)
with self.assertRaises(aufgabe.ValidierungsFehler):
aufgabe.Tier(" ", alter=3, gewicht=5.0)
def test_04_tier_validierung_negatives_alter(self):
with self.assertRaises(aufgabe.ValidierungsFehler):
aufgabe.Tier("Sammy", alter=-1, gewicht=5.0)
def test_05_tier_validierung_ungueltiges_gewicht(self):
with self.assertRaises(aufgabe.ValidierungsFehler):
aufgabe.Tier("Sammy", alter=2, gewicht=0.0)
with self.assertRaises(aufgabe.ValidierungsFehler):
aufgabe.Tier("Sammy", alter=2, gewicht=-4.5)
def test_06_tier_fuettern_und_clamping(self):
t = aufgabe.Tier("Sammy", alter=2, gewicht=5.0, hunger=60)
msg = t.fuettern(100) # -20 Hunger
self.assertEqual(t.hunger, 40)
self.assertIn("Sammy", msg)
# Überfüttern darf nicht unter 0 fallen
t.fuettern(500)
self.assertEqual(t.hunger, 0)
def test_07_tier_impfen(self):
t = aufgabe.Tier("Sammy", alter=2, gewicht=5.0, geimpft=False)
erstes_mal = t.impfen()
self.assertTrue(erstes_mal, "Erstmaliges Impfen sollte True zurückgeben.")
self.assertTrue(t.geimpft)
zweites_mal = t.impfen()
self.assertFalse(zweites_mal, "Erneutes Impfen sollte False zurückgeben.")
def test_08_tier_to_dict_und_str(self):
t = aufgabe.Tier("Sammy", alter=2, gewicht=5.0, geimpft=True, hunger=30)
d = t.to_dict()
self.assertEqual(d.get("name"), "Sammy")
self.assertEqual(d.get("alter"), 2)
self.assertEqual(d.get("gewicht"), 5.0)
self.assertEqual(d.get("geimpft"), True)
self.assertEqual(d.get("hunger"), 30)
self.assertIn("Sammy", str(t))
class Test03Kindklassen(unittest.TestCase):
"""Prüft Vererbung und Spezialisierungen für Hund, Katze und Vogel."""
def test_09_hund_spezifisch(self):
h = aufgabe.Hund("Bello", alter=3, gewicht=15.0, rasse="Labrador", geimpft=False, hunger=40)
self.assertTrue(isinstance(h, aufgabe.Tier), "Hund muss von Tier erben!")
self.assertEqual(h.rasse, "Labrador")
self.assertFalse(h.gassigegangen)
# Gassigehen
msg = h.gassi_gehen(20) # +4 Hunger
self.assertTrue(h.gassigegangen)
self.assertEqual(h.hunger, 44)
self.assertIn("Bello", msg)
# Laut
laut = h.mache_laut()
self.assertIn("Wuff", laut)
# to_dict
d = h.to_dict()
self.assertEqual(d.get("art"), "Hund")
self.assertEqual(d.get("rasse"), "Labrador")
self.assertTrue(d.get("gassigegangen"))
def test_10_katze_spezifisch(self):
k = aufgabe.Katze("Luna", alter=2, gewicht=4.0, stubenrein=True, hunger=30)
self.assertTrue(isinstance(k, aufgabe.Tier), "Katze muss von Tier erben!")
self.assertTrue(k.stubenrein)
self.assertFalse(k.kratzbaum_benutzt)
# Kratzen
k.kratzen()
self.assertTrue(k.kratzbaum_benutzt)
# Laut
laut = k.mache_laut()
self.assertIn("Miau", laut)
# to_dict
d = k.to_dict()
self.assertEqual(d.get("art"), "Katze")
self.assertTrue(d.get("stubenrein"))
self.assertTrue(d.get("kratzbaum_benutzt"))
def test_11_vogel_spezifisch(self):
v = aufgabe.Vogel("Tweety", alter=1, gewicht=0.3, spannweite_cm=18.0, kann_sprechen=True, hunger=20)
self.assertTrue(isinstance(v, aufgabe.Tier), "Vogel muss von Tier erben!")
self.assertEqual(v.spannweite_cm, 18.0)
self.assertTrue(v.kann_sprechen)
# Fliegen
v.fliegen(5) # +10 Hunger
self.assertEqual(v.hunger, 30)
# Laut mit Sprechen
laut = v.mache_laut()
self.assertIn("Hallo Mensch", laut)
# Vogel ohne Sprechen
v2 = aufgabe.Vogel("Charly", alter=2, gewicht=0.4, kann_sprechen=False)
laut2 = v2.mache_laut()
self.assertIn("Tschilp", laut2)
class Test04PolymorphieUndFactory(unittest.TestCase):
"""Prüft das Factory-Pattern Tier.from_dict für alle Subklassen."""
def test_12_factory_hund_wiederherstellen(self):
data = {
"art": "Hund",
"name": "Rex",
"alter": 5,
"gewicht": 20.0,
"rasse": "Schäferhund",
"geimpft": True,
"hunger": 35,
"gassigegangen": True,
}
obj = aufgabe.Tier.from_dict(data)
self.assertIsInstance(obj, aufgabe.Hund)
self.assertEqual(obj.name, "Rex")
self.assertEqual(obj.rasse, "Schäferhund")
self.assertTrue(obj.geimpft)
self.assertTrue(obj.gassigegangen)
def test_13_factory_katze_wiederherstellen(self):
data = {
"art": "Katze",
"name": "Minka",
"alter": 3,
"gewicht": 3.8,
"stubenrein": True,
"geimpft": False,
"hunger": 50,
"kratzbaum_benutzt": False,
}
obj = aufgabe.Tier.from_dict(data)
self.assertIsInstance(obj, aufgabe.Katze)
self.assertEqual(obj.name, "Minka")
self.assertTrue(obj.stubenrein)
def test_14_factory_vogel_wiederherstellen(self):
data = {
"art": "Vogel",
"name": "Kiki",
"alter": 2,
"gewicht": 0.2,
"spannweite_cm": 15.5,
"kann_sprechen": True,
"geimpft": True,
"hunger": 10,
}
obj = aufgabe.Tier.from_dict(data)
self.assertIsInstance(obj, aufgabe.Vogel)
self.assertEqual(obj.name, "Kiki")
self.assertEqual(obj.spannweite_cm, 15.5)
self.assertTrue(obj.kann_sprechen)
class Test05TierheimGeschaeftslogik(unittest.TestCase):
"""Prüft die Model-Klasse Tierheim (Aufnahme, Entlassen, Filter, Statistik)."""
def setUp(self):
self.heim = aufgabe.Tierheim("Tierheim Test", max_kapazitaet=4)
self.bello = aufgabe.Hund("Bello", alter=4, gewicht=15.0, geimpft=True, hunger=20)
self.luna = aufgabe.Katze("Luna", alter=2, gewicht=4.0, geimpft=False, hunger=60)
self.tweety = aufgabe.Vogel("Tweety", alter=1, gewicht=0.3, geimpft=False, hunger=70)
def test_15_aufnahme_und_kapazitaet(self):
self.heim.tier_aufnehmen(self.bello)
self.heim.tier_aufnehmen(self.luna)
self.heim.tier_aufnehmen(self.tweety)
self.assertEqual(len(self.heim), 3)
# 4. Tier aufnehmen (Max erreicht)
h2 = aufgabe.Hund("Rocky", alter=1, gewicht=10.0)
self.heim.tier_aufnehmen(h2)
self.assertEqual(len(self.heim), 4)
# 5. Tier aufnehmen -> KapazitaetUeberschrittenFehler
h3 = aufgabe.Hund("Pluto", alter=2, gewicht=8.0)
with self.assertRaises(aufgabe.KapazitaetUeberschrittenFehler):
self.heim.tier_aufnehmen(h3)
def test_16_tier_entlassen_und_suche(self):
self.heim.tier_aufnehmen(self.bello)
self.heim.tier_aufnehmen(self.luna)
# Suchen
gefunden = self.heim.finde_tier("bello")
self.assertIsNotNone(gefunden)
self.assertEqual(gefunden.name, "Bello")
# Entlassen
entlassen = self.heim.tier_entlassen("Bello")
self.assertEqual(entlassen.name, "Bello")
self.assertEqual(len(self.heim), 1)
# Erneutes Entlassen -> TierNichtGefundenFehler
with self.assertRaises(aufgabe.TierNichtGefundenFehler):
self.heim.tier_entlassen("Bello")
def test_17_filter_nach_art(self):
self.heim.tier_aufnehmen(self.bello)
self.heim.tier_aufnehmen(self.luna)
self.heim.tier_aufnehmen(self.tweety)
hunde = self.heim.filtriere_nach_art("Hund")
self.assertEqual(len(hunde), 1)
self.assertEqual(hunde[0].name, "Bello")
katzen = self.heim.filtriere_nach_art("Katze")
self.assertEqual(len(katzen), 1)
self.assertEqual(katzen[0].name, "Luna")
alle = self.heim.filtriere_nach_art("Alle")
self.assertEqual(len(alle), 3)
def test_18_filter_ungeimpft_und_hungrig(self):
self.heim.tier_aufnehmen(self.bello) # Geimpft, Hunger 20
self.heim.tier_aufnehmen(self.luna) # Ungeimpft, Hunger 60
self.heim.tier_aufnehmen(self.tweety) # Ungeimpft, Hunger 70
ungeimpft = self.heim.ungeimpfte_tiere()
self.assertEqual(len(ungeimpft), 2)
hungrig = self.heim.hungrige_tiere(schwellenwert=50)
self.assertEqual(len(hungrig), 2)
def test_19_statistik_berechnungen(self):
self.heim.tier_aufnehmen(self.bello) # 4 J., 15.0 kg
self.heim.tier_aufnehmen(self.luna) # 2 J., 4.0 kg
self.heim.tier_aufnehmen(self.tweety) # 1 J., 0.3 kg
# Alter: (4 + 2 + 1) / 3 = 2.333
self.assertAlmostEqual(self.heim.durchschnittsalter(), 7.0 / 3.0, places=2)
# Gewicht: 15.0 + 4.0 + 0.3 = 19.3
self.assertAlmostEqual(self.heim.gesamtgewicht(), 19.3, places=2)
def test_20_massenaktionen_fuettern_und_impfen(self):
self.heim.tier_aufnehmen(self.bello) # Geimpft, Hunger 20
self.heim.tier_aufnehmen(self.luna) # Ungeimpft, Hunger 60
self.heim.tier_aufnehmen(self.tweety) # Ungeimpft, Hunger 70
# Alle impfen -> 2 neu geimpft
frisch = self.heim.alle_impfen()
self.assertEqual(frisch, 2)
self.assertEqual(len(self.heim.ungeimpfte_tiere()), 0)
# Alle füttern (100g -> -20 Hunger)
ergebnisse = self.heim.alle_fuettern(100)
self.assertEqual(len(ergebnisse), 3)
self.assertEqual(self.bello.hunger, 0)
self.assertEqual(self.luna.hunger, 40)
self.assertEqual(self.tweety.hunger, 50)
class Test06Persistenz(unittest.TestCase):
"""Prüft JSON Savegame und CSV Export."""
def test_21_json_speichern_und_laden(self):
heim = aufgabe.Tierheim("Savegame Heim", max_kapazitaet=10)
heim.tier_aufnehmen(aufgabe.Hund("Bello", 3, 14.5, rasse="Husky", geimpft=True))
heim.tier_aufnehmen(aufgabe.Katze("Luna", 2, 4.2, stubenrein=True, geimpft=False))
heim.tier_aufnehmen(aufgabe.Vogel("Tweety", 1, 0.3, spannweite_cm=19.0, kann_sprechen=True))
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tf:
pfad = tf.name
try:
heim.speichern_json(pfad)
self.assertTrue(os.path.exists(pfad))
# Neues leeres Heim lädt den Stand
neues_heim = aufgabe.Tierheim("Leer", max_kapazitaet=5)
neues_heim.laden_json(pfad)
self.assertEqual(neues_heim.name, "Savegame Heim")
self.assertEqual(len(neues_heim), 3)
# Prüfen ob Typen erhalten blieben (Polymorphie)
self.assertIsInstance(neues_heim.tiere[0], aufgabe.Hund)
self.assertEqual(neues_heim.tiere[0].rasse, "Husky")
self.assertIsInstance(neues_heim.tiere[1], aufgabe.Katze)
self.assertIsInstance(neues_heim.tiere[2], aufgabe.Vogel)
self.assertTrue(neues_heim.tiere[2].kann_sprechen)
finally:
if os.path.exists(pfad):
os.remove(pfad)
def test_22_csv_export(self):
heim = aufgabe.Tierheim("CSV Heim", max_kapazitaet=10)
heim.tier_aufnehmen(aufgabe.Hund("Bello", 3, 14.5, rasse="Husky", geimpft=True))
heim.tier_aufnehmen(aufgabe.Katze("Luna", 2, 4.2, stubenrein=True, geimpft=False))
with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as tf:
pfad = tf.name
try:
heim.exportiere_csv(pfad)
self.assertTrue(os.path.exists(pfad))
with open(pfad, "r", encoding="utf-8") as f:
reader = list(csv.reader(f))
# Header prüfen
self.assertGreaterEqual(len(reader), 3)
self.assertEqual(reader[0], ["Art", "Name", "Alter", "Gewicht", "Geimpft", "Hunger", "Details"])
# Erste Zeile (Bello)
self.assertEqual(reader[1][0], "Hund")
self.assertEqual(reader[1][1], "Bello")
finally:
if os.path.exists(pfad):
os.remove(pfad)
class Test07GUIHeadless(unittest.TestCase):
"""Prüft die Benutzeroberfläche TierheimApp im Headless-Modus."""
def setUp(self):
tk_mod = getattr(aufgabe, "tk", None)
self.assertIsNotNone(tk_mod, "tk Modul in aufgabe.py fehlt!")
self.root = tk_mod.Tk()
self.heim = aufgabe.Tierheim("GUI Test Heim", max_kapazitaet=10)
self.heim.tier_aufnehmen(aufgabe.Hund("Bello", 3, 14.5, rasse="Beagle", geimpft=True))
self.app = aufgabe.TierheimApp(self.root, self.heim)
def test_23_gui_initialisierung_und_widgets(self):
self.assertTrue(hasattr(self.app, "listbox_tiere"))
self.assertTrue(hasattr(self.app, "label_status"))
self.assertTrue(hasattr(self.app, "label_kapazitaet"))
self.assertEqual(self.app.listbox_tiere.size(), 1)
def test_24_gui_tier_aufnehmen_formular(self):
# Formular mit neuem Hund befüllen
self.app.var_art.set("Katze")
self.app.var_name.set("Mimi")
self.app.var_alter.set("1")
self.app.var_gewicht.set("3.5")
self.app.var_extra.set("ja")
self.app.var_geimpft.set(False)
# Aufnahmeklick auslösen
self.app.tier_aufnehmen_klick()
self.assertEqual(len(self.heim), 2)
mimi = self.heim.finde_tier("Mimi")
self.assertIsNotNone(mimi)
self.assertIsInstance(mimi, aufgabe.Katze)
def test_25_gui_validierungsfehler_abfangen(self):
# Leerer Name -> Darf nicht abstürzen
self.app.var_name.set("")
try:
self.app.tier_aufnehmen_klick()
except Exception as e:
self.fail(f"tier_aufnehmen_klick hat bei leerem Namen Exception nicht abgefangen: {e}")
self.assertEqual(len(self.heim), 1, "Bestand darf sich bei Fehleingabe nicht verändern.")
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -39,7 +39,9 @@ Es gleicht die gesamte Übungsreihe (Grundlagen &amp; Fortgeschrittene Objektori
├── 11: Polymorphie & Interfaces (ABC, Geometrie & Zeichenfläche) (Lehrer 25.1) ├── 11: Polymorphie & Interfaces (ABC, Geometrie & Zeichenfläche) (Lehrer 25.1)
├── 12: Exceptions & Robuste Fehlerbehandlung (try-except, Bankkonto) (Lehrer 26.0) ├── 12: Exceptions & Robuste Fehlerbehandlung (try-except, Bankkonto) (Lehrer 26.0)
├── 13: Datei-Persistenz: JSON & CSV Speichern (Savegames) (Lehrer 14.0) ├── 13: Datei-Persistenz: JSON & CSV Speichern (Savegames) (Lehrer 14.0)
└── 14: Desktop-GUIs mit Tkinter & MVC-Architektur (Zähler & Rechner) (Lehrer 27.0) ├── 14: Desktop-GUIs mit Tkinter & MVC-Architektur (Zähler & Rechner) (Lehrer 27.0)
├── 15: Parameter (*args, **kwargs) & Eigene Container (Gaming-Inventar) (Lehrer 29.0 & 30.0)
└── 16: Master-Abschlussprojekt: Professionelle Desktop-App (PetCare) (Meisterstück & MVC)
``` ```
--- ---
@ -75,3 +77,6 @@ Es gleicht die gesamte Übungsreihe (Grundlagen &amp; Fortgeschrittene Objektori
| **12** | **Exceptions** | `try-except-else-finally`, `raise`, eigene Fehler | Bankkonto- & Geldautomat-Simulator | | **12** | **Exceptions** | `try-except-else-finally`, `raise`, eigene Fehler | Bankkonto- & Geldautomat-Simulator |
| **13** | **Persistenz** | `with open()`, `json.dump`/`load`, CSV-Export | Savegames & Notenbuch | | **13** | **Persistenz** | `with open()`, `json.dump`/`load`, CSV-Export | Savegames & Notenbuch |
| **14** | **Tkinter GUI** | Echte Desktop-Apps, Widgets, `grid()`, MVC-Muster | Interaktive Zähler- & Rechner-App | | **14** | **Tkinter GUI** | Echte Desktop-Apps, Widgets, `grid()`, MVC-Muster | Interaktive Zähler- & Rechner-App |
| **15** | **Parameter & Container** | `*args`, `**kwargs`, Unpacking & Container-Dunder (`__getitem__`, `__setitem__`, `__delitem__`, `__contains__`, `__iter__`) | Gaming-Rucksack & Inventar |
| **16** | **Master-Abschlussprojekt** | Vollständiges Desktop-Meisterstück: MVC-Architektur, Vererbung & Polymorphie, JSON-Savegames, CSV-Export, Exceptions & Tkinter-GUI | PetCare- & Tierheim-Manager Pro |

View file

@ -50,6 +50,8 @@ Dieses Portal ist didaktisch aufbereitet, voll **abgeglichen mit dem Informatik-
| **[12. Exceptions & Fehler](./12_exceptions_und_fehlerbehandlung/)** | **Robustes Coding** | `try-except-else-finally`, `raise`, eigene Exception-Klassen im Banking | | **[12. Exceptions & Fehler](./12_exceptions_und_fehlerbehandlung/)** | **Robustes Coding** | `try-except-else-finally`, `raise`, eigene Exception-Klassen im Banking |
| **[13. Datei-Persistenz](./13_persistenz_json_und_csv/)** | **JSON & CSV** | Dauerhaftes Speichern & Laden: Savegames & Tabellen-Export | | **[13. Datei-Persistenz](./13_persistenz_json_und_csv/)** | **JSON & CSV** | Dauerhaftes Speichern & Laden: Savegames & Tabellen-Export |
| **[14. Desktop-GUIs mit Tkinter](./14_gui_mit_tkinter/)** | **Tkinter Desktop-Apps** | Echte Fenster, Buttons, Labels, Grid-Layout & MVC-Architektur | | **[14. Desktop-GUIs mit Tkinter](./14_gui_mit_tkinter/)** | **Tkinter Desktop-Apps** | Echte Fenster, Buttons, Labels, Grid-Layout & MVC-Architektur |
| **[15. Parameter & Container](./15_parameter_und_container/)** | **Dunder-Container** | Variable Argumente (`*args`, `**kwargs`), Unpacking & Container (`len`, `[]`, `in`, `iter`) |
| **[16. Master-Abschlussprojekt](./16_master_abschlussprojekt/)** | **🏆 Meisterstück: PetCare** | Vollständige Desktop-App: MVC, Vererbung, Polymorphie, JSON-Savegames & Tkinter GUI |
--- ---
@ -70,7 +72,7 @@ python3 test_aufgabe.py
- ❌ **`FAIL` (AssertionError)**: Dein Code läuft, liefert aber einen falschen Wert zurück (z.B. Rechenfehler oder falsche Bedingung). Lies die letzte Zeile der Fehlermeldung! - ❌ **`FAIL` (AssertionError)**: Dein Code läuft, liefert aber einen falschen Wert zurück (z.B. Rechenfehler oder falsche Bedingung). Lies die letzte Zeile der Fehlermeldung!
- 💥 **`ERROR` (Syntax / AttributeError)**: Dein Code hat einen Absturz (z.B. Tippfehler im Funktionsnamen). - 💥 **`ERROR` (Syntax / AttributeError)**: Dein Code hat einen Absturz (z.B. Tippfehler im Funktionsnamen).
### 3. Gesamten Fortschritt aller 25 Kapitel prüfen: ### 3. Gesamten Fortschritt aller 27 Kapitel prüfen:
Führe im Hauptordner folgenden Befehl aus: Führe im Hauptordner folgenden Befehl aus:
```bash ```bash
python3 test_all.py python3 test_all.py

View file

@ -73,8 +73,8 @@
</div> </div>
<div style="background: #f8fafc; border-left: 4px solid #22c55e; padding: 15px; border-radius: var(--radius-sm);"> <div style="background: #f8fafc; border-left: 4px solid #22c55e; padding: 15px; border-radius: var(--radius-sm);">
<span class="track-badge track-3-badge">Lehrpfad 3</span> <span class="track-badge track-3-badge">Lehrpfad 3</span>
<h4 style="margin: 0 0 6px 0; color: #14532d;">🚀 Fortgeschrittenes OOP &amp; GUIs</h4> <h4 style="margin: 0 0 6px 0; color: #14532d;">🚀 Fortgeschrittenes OOP, Container &amp; GUIs</h4>
<p style="font-size: 0.88rem; color: var(--text-muted); margin: 0;">Speicher-Referenzen, Dunder-Methoden, eigene Unit Tests (TDD), Vererbung, Polymorphie, Exceptions, JSON-Persistenz &amp; Tkinter (07–14).</p> <p style="font-size: 0.88rem; color: var(--text-muted); margin: 0;">Speicher-Referenzen, Dunder-Methoden, Unit Tests (TDD), Vererbung, Polymorphie, Exceptions, JSON, Tkinter &amp; Container (07–15).</p>
</div> </div>
</div> </div>
@ -311,7 +311,7 @@
<h2>🛣️ Lehrpfad 3: Fortgeschrittenes OOP, Testing &amp; Desktop-GUIs</h2> <h2>🛣️ Lehrpfad 3: Fortgeschrittenes OOP, Testing &amp; Desktop-GUIs</h2>
<div class="section-desc">Professionelle Softwaretechnik: Speicherverwaltung, Vererbung, TDD-Tests, Persistenz und GUIs.</div> <div class="section-desc">Professionelle Softwaretechnik: Speicherverwaltung, Vererbung, TDD-Tests, Persistenz und GUIs.</div>
</div> </div>
<span class="track-badge track-3-badge">Kapitel 07 bis 14</span> <span class="track-badge track-3-badge">Kapitel 07 bis 16</span>
</div> </div>
<div class="roadmap-grid"> <div class="roadmap-grid">
@ -396,6 +396,26 @@
<div class="roadmap-footer">Kapitel öffnen &rarr;</div> <div class="roadmap-footer">Kapitel öffnen &rarr;</div>
</a> </a>
<!-- Kapitel 15 -->
<a href="15_parameter_und_container/index.html" class="roadmap-card" style="border-left: 4px solid var(--success);">
<div>
<span class="roadmap-number" style="background: var(--success-light); color: var(--success);">Kapitel 15</span>
<div class="roadmap-title">🎒 Parameter &amp; Eigene Container</div>
<div class="roadmap-desc">Variable Argumente (<code>*args</code>, <code>**kwargs</code>), Unpacking und Container-Dunder (<code>len</code>, <code>[]</code>, <code>in</code>, <code>iter</code>) im Gaming-Inventar.</div>
</div>
<div class="roadmap-footer">Kapitel öffnen &rarr;</div>
</a>
<!-- Kapitel 16 -->
<a href="16_master_abschlussprojekt/index.html" class="roadmap-card" style="border-left: 4px solid #f59e0b; background: linear-gradient(180deg, #ffffff 0%, #fffbeb 100%);">
<div>
<span class="roadmap-number" style="background: #fef3c7; color: #b45309; font-weight: bold;">🏆 Kapitel 16 &bull; Masterprojekt</span>
<div class="roadmap-title">🐾 PetCare &amp; Tierheim-Manager</div>
<div class="roadmap-desc">Das große Meisterstück: MVC-Architektur, Vererbung, Polymorphie, JSON-Savegames, CSV-Export &amp; Tkinter Desktop-App!</div>
</div>
<div class="roadmap-footer" style="color: #b45309; font-weight: bold;">Meisterstück starten &rarr;</div>
</a>
</div> </div>
</section> </section>

View file

@ -36,6 +36,8 @@ KAPITEL = [
("Kapitel 12: Exceptions & Fehlerbehandlung", "12_exceptions_und_fehlerbehandlung"), ("Kapitel 12: Exceptions & Fehlerbehandlung", "12_exceptions_und_fehlerbehandlung"),
("Kapitel 13: Persistenz (JSON & CSV)", "13_persistenz_json_und_csv"), ("Kapitel 13: Persistenz (JSON & CSV)", "13_persistenz_json_und_csv"),
("Kapitel 14: Desktop-GUIs mit Tkinter", "14_gui_mit_tkinter"), ("Kapitel 14: Desktop-GUIs mit Tkinter", "14_gui_mit_tkinter"),
("Kapitel 15: Parameter & Eigene Container", "15_parameter_und_container"),
("Kapitel 16: Master-Abschlussprojekt", "16_master_abschlussprojekt"),
] ]
def main(): def main():