Implementiere Kapitel 15 (Parameter & Container) und Kapitel 16 (Master-Abschlussprojekt)
This commit is contained in:
parent
a3a6362c77
commit
478b1d95fe
16 changed files with 4271 additions and 6 deletions
|
|
@ -341,7 +341,8 @@
|
|||
|
||||
<div class="footer-nav">
|
||||
<a href="../13_persistenz_json_und_csv/index.html" class="btn btn-secondary">← Zurück zu Kapitel 13</a>
|
||||
<a href="../index.html" class="btn">Zur Kursübersicht →</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 →</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
157
15_parameter_und_container/README.md
Normal file
157
15_parameter_und_container/README.md
Normal 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
|
||||
```
|
||||
360
15_parameter_und_container/aufgabe.py
Normal file
360
15_parameter_und_container/aufgabe.py
Normal 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}")
|
||||
316
15_parameter_und_container/index.html
Normal file
316
15_parameter_und_container/index.html
Normal 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>›</span>
|
||||
<a href="../14_gui_mit_tkinter/index.html">Kapitel 14</a>
|
||||
<span>›</span>
|
||||
<span style="color: white; font-weight: 600;">Kapitel 15</span>
|
||||
</nav>
|
||||
<div class="header-badge">Kapitel 15 • Schulabgleich 29.0 & 30.0</div>
|
||||
<h1>Parameter (*args, **kwargs) & 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 & 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- & 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 & 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 & 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 >= 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;">
|
||||
• <code>inv["Heiltrank"]</code> → Findet Trank per Namen!<br>
|
||||
• <code>"Holzschild" in inv</code> → True<br>
|
||||
• <code>inv.hinzufuegen(*items)</code> → 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 & 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 & 8 (__len__ & __getitem__):</strong> <code>len(inv)</code> und Multi-Lookup für Index (int) & 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 & 10 (__setitem__ & __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 & 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 & 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">← 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) →</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
232
15_parameter_und_container/musterloesung.py
Normal file
232
15_parameter_und_container/musterloesung.py
Normal 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}")
|
||||
341
15_parameter_und_container/test_aufgabe.py
Normal file
341
15_parameter_und_container/test_aufgabe.py
Normal 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)
|
||||
178
16_master_abschlussprojekt/README.md
Normal file
178
16_master_abschlussprojekt/README.md
Normal 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.
|
||||
72
16_master_abschlussprojekt/app.py
Normal file
72
16_master_abschlussprojekt/app.py
Normal 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()
|
||||
790
16_master_abschlussprojekt/aufgabe.py
Normal file
790
16_master_abschlussprojekt/aufgabe.py
Normal 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}")
|
||||
327
16_master_abschlussprojekt/index.html
Normal file
327
16_master_abschlussprojekt/index.html
Normal 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>›</span>
|
||||
<a href="../15_parameter_und_container/index.html">Kapitel 15</a>
|
||||
<span>›</span>
|
||||
<span style="color: white; font-weight: 600;">Kapitel 16 (Masterprojekt)</span>
|
||||
</nav>
|
||||
<div class="header-badge">🏆 Kapitel 16 • Das Große Meisterstück</div>
|
||||
<h1>PetCare & Tierheim-Manager Pro 🐾🏥🖥️</h1>
|
||||
<p>Die Krönung aller 3 Lehrpfade: Model-View-Controller (MVC), Vererbung, Polymorphie, JSON- & 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- & 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 & 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 & Logik)</span>
|
||||
<h3 style="margin-top: 10px; color: #065f46;">Klassen <code>Tier</code> & <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 & 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 & Daten).</li>
|
||||
<li><strong>View (Die Speisekarte & 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 & 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 & 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 & 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 & 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 & 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">← Zurück zu Kapitel 15</a>
|
||||
<a href="../index.html" class="btn">🏆 Kurs abgeschlossen – Zur Kursübersicht →</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1035
16_master_abschlussprojekt/musterloesung.py
Normal file
1035
16_master_abschlussprojekt/musterloesung.py
Normal file
File diff suppressed because it is too large
Load diff
427
16_master_abschlussprojekt/test_aufgabe.py
Normal file
427
16_master_abschlussprojekt/test_aufgabe.py
Normal 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)
|
||||
|
|
@ -39,7 +39,9 @@ Es gleicht die gesamte Übungsreihe (Grundlagen & Fortgeschrittene Objektori
|
|||
├── 11: Polymorphie & Interfaces (ABC, Geometrie & Zeichenfläche) (Lehrer 25.1)
|
||||
├── 12: Exceptions & Robuste Fehlerbehandlung (try-except, Bankkonto) (Lehrer 26.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 & Fortgeschrittene Objektori
|
|||
| **12** | **Exceptions** | `try-except-else-finally`, `raise`, eigene Fehler | Bankkonto- & Geldautomat-Simulator |
|
||||
| **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 |
|
||||
| **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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
| **[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 |
|
||||
| **[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!
|
||||
- 💥 **`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:
|
||||
```bash
|
||||
python3 test_all.py
|
||||
|
|
|
|||
26
index.html
26
index.html
|
|
@ -73,8 +73,8 @@
|
|||
</div>
|
||||
<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>
|
||||
<h4 style="margin: 0 0 6px 0; color: #14532d;">🚀 Fortgeschrittenes OOP & 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 & Tkinter (07–14).</p>
|
||||
<h4 style="margin: 0 0 6px 0; color: #14532d;">🚀 Fortgeschrittenes OOP, Container & GUIs</h4>
|
||||
<p style="font-size: 0.88rem; color: var(--text-muted); margin: 0;">Speicher-Referenzen, Dunder-Methoden, Unit Tests (TDD), Vererbung, Polymorphie, Exceptions, JSON, Tkinter & Container (07–15).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -311,7 +311,7 @@
|
|||
<h2>🛣️ Lehrpfad 3: Fortgeschrittenes OOP, Testing & Desktop-GUIs</h2>
|
||||
<div class="section-desc">Professionelle Softwaretechnik: Speicherverwaltung, Vererbung, TDD-Tests, Persistenz und GUIs.</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 class="roadmap-grid">
|
||||
|
|
@ -396,6 +396,26 @@
|
|||
<div class="roadmap-footer">Kapitel öffnen →</div>
|
||||
</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 & 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 →</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 • Masterprojekt</span>
|
||||
<div class="roadmap-title">🐾 PetCare & Tierheim-Manager</div>
|
||||
<div class="roadmap-desc">Das große Meisterstück: MVC-Architektur, Vererbung, Polymorphie, JSON-Savegames, CSV-Export & Tkinter Desktop-App!</div>
|
||||
</div>
|
||||
<div class="roadmap-footer" style="color: #b45309; font-weight: bold;">Meisterstück starten →</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ KAPITEL = [
|
|||
("Kapitel 12: Exceptions & Fehlerbehandlung", "12_exceptions_und_fehlerbehandlung"),
|
||||
("Kapitel 13: Persistenz (JSON & CSV)", "13_persistenz_json_und_csv"),
|
||||
("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():
|
||||
|
|
|
|||
Loading…
Reference in a new issue