Implementiere Kapitel 08 bis 14 (Operator Overloading, Unit Testing, Vererbung, Polymorphie, Exceptions, Persistenz, Tkinter GUI)
This commit is contained in:
parent
e7c64995aa
commit
0a32dadcc4
40 changed files with 7152 additions and 22 deletions
|
|
@ -205,7 +205,7 @@
|
||||||
|
|
||||||
<div class="footer-nav">
|
<div class="footer-nav">
|
||||||
<a href="../06_abschlussprojekt_tamagotchi/index.html" class="btn btn-secondary">← Zurück zu Kapitel 06</a>
|
<a href="../06_abschlussprojekt_tamagotchi/index.html" class="btn btn-secondary">← Zurück zu Kapitel 06</a>
|
||||||
<a href="../index.html" class="btn">Zur Kursübersicht →</a>
|
<a href="../08_operator_overloading_dunder/index.html" class="btn">Weiter zu Kapitel 08 →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
43
08_operator_overloading_dunder/README.md
Normal file
43
08_operator_overloading_dunder/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Kapitel 08: Operator Overloading & Dunder-Methoden ➕
|
||||||
|
|
||||||
|
In diesem Kapitel lernst du, wie du deine eigenen Klassen mit Pythons eingebauten Operatoren (`+`, `-`, `*`, `==`, `<`, `len()`, `abs()`) ausstattest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Was du lernst
|
||||||
|
|
||||||
|
1. **Was sind Dunder-Methoden?** Methoden mit führenden und nachgestellten doppelten Unterstrichen (**D**ouble **Under**score, z.B. `__add__`).
|
||||||
|
2. **Magische Übersetzung:** Wenn du `a + b` schreibst, ruft Python im Hintergrund `a.__add__(b)` auf.
|
||||||
|
3. **Mathematische Operatoren überladen:**
|
||||||
|
- `__add__(self, other)` für `+`
|
||||||
|
- `__sub__(self, other)` für `-`
|
||||||
|
- `__mul__(self, other)` für `*` (z.B. Skalarmultiplikation mit Zahlen oder Skalarprodukt)
|
||||||
|
- `__rmul__(self, other)` für `*` von links (z.B. `3 * v`)
|
||||||
|
4. **Vergleiche überladen:**
|
||||||
|
- `__eq__(self, other)` für `==`
|
||||||
|
- `__lt__(self, other)` für `<`
|
||||||
|
- `__le__(self, other)` für `<=`
|
||||||
|
5. **Objekt- und Container-Dunder:**
|
||||||
|
- `__str__` (für Menschen) vs. `__repr__` (für Entwickler / Debugger)
|
||||||
|
- `__abs__` für `abs(objekt)` (z.B. Vektorlänge)
|
||||||
|
- `__len__` für `len(objekt)` (z.B. Anzahl von Wegpunkten)
|
||||||
|
- `__getitem__` für Indexzugriff `objekt[i]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Die Dateien in diesem Ordner
|
||||||
|
|
||||||
|
- **`index.html`**: Interaktive Lernseite mit visuellen Tabellen, Code-Beispielen & Ausklapp-Hilfen.
|
||||||
|
- **`aufgabe.py`**: Dein Arbeitsblatt zum Implementieren von `Vektor2D` und `Wegstrecke`.
|
||||||
|
- **`test_aufgabe.py`**: Automatische Tests (`python3 test_aufgabe.py`).
|
||||||
|
- **`musterloesung.py`**: Vollständig ausprogrammierte Beispiellösung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bearbeite aufgabe.py
|
||||||
|
# 2. Teste deine Lösung:
|
||||||
|
python3 test_aufgabe.py
|
||||||
|
```
|
||||||
166
08_operator_overloading_dunder/aufgabe.py
Normal file
166
08_operator_overloading_dunder/aufgabe.py
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
"""
|
||||||
|
Kapitel 08: Operator Overloading & Dunder-Methoden ➕
|
||||||
|
===================================================
|
||||||
|
Aufgabe: Mache 2D-Vektoren rechen- und vergleichbar mit magischen Dunder-Methoden!
|
||||||
|
|
||||||
|
Implementiere die Klasse 'Vektor2D' und die Container-Klasse 'Wegstrecke'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import Union, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Vektor2D:
|
||||||
|
"""
|
||||||
|
Repräsentiert einen 2D-Vektor (x, y).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 1: Konstruktor __init__(self, x: float, y: float)
|
||||||
|
# Speichere self.x = float(x) und self.y = float(y).
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(self, x: float, y: float):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 2: Darstellungsmethoden __repr__ und __str__
|
||||||
|
# - __repr__(self) -> str: Gibt Entwickler-String "Vektor2D(x, y)" zurück (z.B. "Vektor2D(3.0, 4.0)")
|
||||||
|
# - __str__(self) -> str: Gibt benutzerfreundlichen String "(x, y)" zurück (z.B. "(3.0, 4.0)")
|
||||||
|
# ==========================================================================
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 3: Länge und Absolutbetrag
|
||||||
|
# - laenge(self) -> float: Berechnet sqrt(x² + y²)
|
||||||
|
# - __abs__(self) -> float: Gibt self.laenge() zurück, damit abs(v) funktioniert!
|
||||||
|
# ==========================================================================
|
||||||
|
def laenge(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __abs__(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 4: Addition (__add__) und Subtraktion (__sub__)
|
||||||
|
# - __add__(self, other: 'Vektor2D') -> 'Vektor2D':
|
||||||
|
# Addiert zwei Vektoren komponentenweise: (self.x + other.x, self.y + other.y)
|
||||||
|
# - __sub__(self, other: 'Vektor2D') -> 'Vektor2D':
|
||||||
|
# Subtrahiert zwei Vektoren komponentenweise: (self.x - other.x, self.y - other.y)
|
||||||
|
# Hinweis: Falls other kein Vektor2D ist, gebe 'NotImplemented' zurück.
|
||||||
|
# ==========================================================================
|
||||||
|
def __add__(self, other: "Vektor2D") -> "Vektor2D":
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __sub__(self, other: "Vektor2D") -> "Vektor2D":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 5: Multiplikation (__mul__) und Rechts-Multiplikation (__rmul__)
|
||||||
|
# - __mul__(self, other):
|
||||||
|
# * Wenn other eine Zahl ist (int oder float):
|
||||||
|
# Skalarmultiplikation -> neuer Vektor2D(self.x * other, self.y * other)
|
||||||
|
# * Wenn other ein Vektor2D ist:
|
||||||
|
# Skalarprodukt (Dot Product) -> self.x * other.x + self.y * other.y (eine Zahl!)
|
||||||
|
# * Andernfalls -> NotImplemented
|
||||||
|
# - __rmul__(self, other):
|
||||||
|
# * Ermöglicht Zahl * Vektor (z.B. 3 * v), indem self.__mul__(other) aufgerufen wird.
|
||||||
|
# ==========================================================================
|
||||||
|
def __mul__(self, other: Union[int, float, "Vektor2D"]) -> Union["Vektor2D", float]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __rmul__(self, other: Union[int, float]) -> "Vektor2D":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 6: Vergleiche (__eq__, __lt__, __le__)
|
||||||
|
# - __eq__(self, other: object) -> bool:
|
||||||
|
# True, wenn other ein Vektor2D ist und self.x == other.x und self.y == other.y (Tipp: math.isclose)
|
||||||
|
# - __lt__(self, other: 'Vektor2D') -> bool:
|
||||||
|
# Vergleicht anhand der Vektorlänge: self.laenge() < other.laenge()
|
||||||
|
# - __le__(self, other: 'Vektor2D') -> bool:
|
||||||
|
# Vergleicht anhand der Vektorlänge: self.laenge() <= other.laenge()
|
||||||
|
# ==========================================================================
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __lt__(self, other: "Vektor2D") -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __le__(self, other: "Vektor2D") -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Wegstrecke:
|
||||||
|
"""
|
||||||
|
Eine Sequenz von 2D-Wegpunkten (Vektor2D-Objekte).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 7: Wegstrecke __init__ und punkt_hinzufuegen
|
||||||
|
# - __init__(self, punkte: list[Vektor2D] | None = None):
|
||||||
|
# Speichert self.punkte = list(punkte) if punkte else []
|
||||||
|
# - punkt_hinzufuegen(self, punkt: Vektor2D) -> None:
|
||||||
|
# Fügt den Punkt zur internen Liste hinzu.
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(self, punkte: Optional[List[Vektor2D]] = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def punkt_hinzufuegen(self, punkt: Vektor2D) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 8: Container-Dunder (__len__ und __getitem__)
|
||||||
|
# - __len__(self) -> int:
|
||||||
|
# Gibt die Anzahl der Wegpunkte zurück (len(self.punkte)).
|
||||||
|
# - __getitem__(self, index: int) -> Vektor2D:
|
||||||
|
# Gibt den Punkt an Index 'index' zurück (self.punkte[index]).
|
||||||
|
# ==========================================================================
|
||||||
|
def __len__(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __getitem__(self, index: int) -> Vektor2D:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 9: gesamtlaenge(self) -> float
|
||||||
|
# Berechnet die Summe aller Teilstrecken zwischen aufeinanderfolgenden Punkten:
|
||||||
|
# Distanz zwischen Punkt i und Punkt i+1 ist: abs(punkte[i+1] - punkte[i])
|
||||||
|
# Wenn weniger als 2 Punkte vorhanden sind: gebe 0.0 zurück.
|
||||||
|
# ==========================================================================
|
||||||
|
def gesamtlaenge(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Kleiner Test zum Ausprobieren im Terminal:
|
||||||
|
# (python3 aufgabe.py)
|
||||||
|
# ==============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
v1 = Vektor2D(3, 4)
|
||||||
|
v2 = Vektor2D(1, 2)
|
||||||
|
|
||||||
|
print("--- Vektor Test ---")
|
||||||
|
print(f"v1: {v1}")
|
||||||
|
print(f"repr(v1): {repr(v1)}")
|
||||||
|
if v1.laenge() is not None:
|
||||||
|
print(f"Länge |v1|: {abs(v1)}")
|
||||||
|
if (v1 + v2) is not None:
|
||||||
|
print(f"v1 + v2 = {v1 + v2}")
|
||||||
|
print(f"v1 - v2 = {v1 - v2}")
|
||||||
|
print(f"v1 * 3 = {v1 * 3}")
|
||||||
|
print(f"3 * v1 = {3 * v1}")
|
||||||
|
print(f"Skalarprodukt v1 * v2 = {v1 * v2}")
|
||||||
|
print(f"v1 == Vektor2D(3, 4): {v1 == Vektor2D(3, 4)}")
|
||||||
|
print(f"v2 < v1: {v2 < v1}")
|
||||||
|
|
||||||
|
print("\n--- Wegstrecke Test ---")
|
||||||
|
route = Wegstrecke([Vektor2D(0, 0), Vektor2D(3, 0), Vektor2D(3, 4)])
|
||||||
|
print(f"Anzahl Wegpunkte: {len(route) if len(route) is not None else 'noch nicht implementiert'}")
|
||||||
|
try:
|
||||||
|
print(f"Gesamtlänge Route: {route.gesamtlaenge()}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Gesamtlänge Fehler: {e}")
|
||||||
280
08_operator_overloading_dunder/index.html
Normal file
280
08_operator_overloading_dunder/index.html
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kapitel 08: Operator Overloading & Dunder-Methoden</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="../07_referenzen_und_speicher/index.html">Kapitel 07</a>
|
||||||
|
<span>›</span>
|
||||||
|
<span style="color: white; font-weight: 600;">Kapitel 08</span>
|
||||||
|
</nav>
|
||||||
|
<div class="header-badge">Kapitel 08 • Schulabgleich 28.0 Spezielle Methoden</div>
|
||||||
|
<h1>Operator Overloading & Dunder-Methoden ➕📐</h1>
|
||||||
|
<p>Bringe deinen eigenen Objekten das Rechnen und Vergleichen bei – mit Pythons magischen Methoden!</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
|
||||||
|
<!-- 1. Das Konzept: Was sind Dunder-Methoden? -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>1. Was bedeutet "Dunder" & Operator Overloading?</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In Python steht <strong>"Dunder"</strong> kurz für <em>Double Underscore</em> (doppelter Unterstrich), wie bei <code>__init__</code> oder <code>__str__</code>.
|
||||||
|
Man nennt sie auch <strong>magische Methoden</strong> (Magic Methods) oder <strong>spezielle Methoden</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🧙♂️ Der Universal-Übersetzer von Python:</div>
|
||||||
|
Wenn du in Python schreibst: <code>a + b</code>, dann weiß Python von Natur aus nicht, was deine eigenen Klassen bedeuten.
|
||||||
|
Python ruft stattdessen heimlich im Hintergrund die Methode <code>a.__add__(b)</code> auf!<br>
|
||||||
|
<strong>Operator Overloading</strong> bedeutet einfach: Du schreibst die Methode <code>__add__</code> in deine Klasse, und schon funktioniert das Plus-Zeichen <code>+</code> für deine Objekte!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Was Python im Hintergrund wirklich tut</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="cm"># Was du im Code schreibst:</span>
|
||||||
|
<span class="var">ergebnis</span> = <span class="var">v1</span> + <span class="var">v2</span>
|
||||||
|
<span class="var">ist_gleich</span> = (<span class="var">v1</span> == <span class="var">v2</span>)
|
||||||
|
<span class="var">laenge</span> = <span class="fn">len</span>(<span class="var">meine_liste</span>)
|
||||||
|
|
||||||
|
<span class="cm"># Was Python intern daraus macht:</span>
|
||||||
|
<span class="var">ergebnis</span> = <span class="var">v1</span>.<span class="fn">__add__</span>(<span class="var">v2</span>)
|
||||||
|
<span class="var">ist_gleich</span> = <span class="var">v1</span>.<span class="fn">__eq__</span>(<span class="var">v2</span>)
|
||||||
|
<span class="var">laenge</span> = <span class="var">meine_liste</span>.<span class="fn">__len__</span>()</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. Die wichtigsten Dunder-Methoden im Überblick -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>2. Die wichtigste Dunder-Methoden-Tabelle</h2>
|
||||||
|
|
||||||
|
<p>Hier sind die am häufigsten genutzten magischen Methoden in Python:</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);">Kategorie</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">Operator / Funktion</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">Dunder-Methode</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">Bedeutung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);" rowspan="4"><strong>Mathematik</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>a + b</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__add__(self, other)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Addition</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>a - b</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__sub__(self, other)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Subtraktion</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>a * b</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__mul__(self, other)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Multiplikation (von links)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>b * a</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__rmul__(self, other)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Rechts-Multiplikation (z.B. <code>3 * v</code>)</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);" rowspan="3"><strong>Vergleiche</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>a == b</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__eq__(self, other)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Gleichheit (Equal)</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>a < b</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__lt__(self, other)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Kleiner als (Less Than)</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>a <= b</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__le__(self, other)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Kleiner gleich (Less or Equal)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);" rowspan="4"><strong>Darstellung & Container</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>print(a)</code> / <code>str(a)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__str__(self)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Schöne Ausgabe für Menschen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>repr(a)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__repr__(self)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Eindeutige Debug-Ausgabe</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>abs(a)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>__abs__(self)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Betrag / Länge</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>len(a)</code></td>
|
||||||
|
<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);">Anzahl von Elementen</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="box box-tipp">
|
||||||
|
<div class="box-title">💡 Unterschied zwischen <code>__str__</code> und <code>__repr__</code>:</div>
|
||||||
|
<ul>
|
||||||
|
<li><strong><code>__str__</code></strong> ist für <em>Endbenutzer</em> gedacht. Beispiel: <code>(3.0, 4.0)</code>.</li>
|
||||||
|
<li><strong><code>__repr__</code></strong> ist für <em>Entwickler & Debugger</em> gedacht. Sie soll so präzise sein, dass man das Objekt idealerweise wieder nachbauen könnte: <code>Vektor2D(3.0, 4.0)</code>.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. Praxisbeispiel: 2D-Vektor in Aktion -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>3. Praxisbeispiel: Der 2D-Vektor 🚀</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In Videospielen, Physiksimulationen und in der Geometrie rechnet man ständig mit 2D-Vektoren.
|
||||||
|
Mit Dunder-Methoden liest sich dein Code wie echte Mathematik!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Beispiel: Vektor-Addition & Multiplikation</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">class</span> <span class="fn">Vektor2D</span>:
|
||||||
|
<span class="kw">def</span> <span class="fn">__init__</span>(<span class="var">self</span>, <span class="var">x</span>: <span class="fn">float</span>, <span class="var">y</span>: <span class="fn">float</span>):
|
||||||
|
<span class="var">self</span>.<span class="attr">x</span> = <span class="fn">float</span>(<span class="var">x</span>)
|
||||||
|
<span class="var">self</span>.<span class="attr">y</span> = <span class="fn">float</span>(<span class="var">y</span>)
|
||||||
|
|
||||||
|
<span class="kw">def</span> <span class="fn">__add__</span>(<span class="var">self</span>, <span class="var">other</span>: <span class="str">'Vektor2D'</span>) -> <span class="str">'Vektor2D'</span>:
|
||||||
|
<span class="kw">if</span> <span class="fn">isinstance</span>(<span class="var">other</span>, <span class="fn">Vektor2D</span>):
|
||||||
|
<span class="kw">return</span> <span class="fn">Vektor2D</span>(<span class="var">self</span>.<span class="attr">x</span> + <span class="var">other</span>.<span class="attr">x</span>, <span class="var">self</span>.<span class="attr">y</span> + <span class="var">other</span>.<span class="attr">y</span>)
|
||||||
|
<span class="kw">return</span> <span class="kw">NotImplemented</span>
|
||||||
|
|
||||||
|
<span class="kw">def</span> <span class="fn">__mul__</span>(<span class="var">self</span>, <span class="var">other</span>):
|
||||||
|
<span class="cm"># Skalarmultiplikation mit Zahl: v * 3</span>
|
||||||
|
<span class="kw">if</span> <span class="fn">isinstance</span>(<span class="var">other</span>, (<span class="fn">int</span>, <span class="fn">float</span>)):
|
||||||
|
<span class="kw">return</span> <span class="fn">Vektor2D</span>(<span class="var">self</span>.<span class="attr">x</span> * <span class="var">other</span>, <span class="var">self</span>.<span class="attr">y</span> * <span class="var">other</span>)
|
||||||
|
<span class="cm"># Skalarprodukt mit anderem Vektor: v1 * v2</span>
|
||||||
|
<span class="kw">elif</span> <span class="fn">isinstance</span>(<span class="var">other</span>, <span class="fn">Vektor2D</span>):
|
||||||
|
<span class="kw">return</span> <span class="var">self</span>.<span class="attr">x</span> * <span class="var">other</span>.<span class="attr">x</span> + <span class="var">self</span>.<span class="attr">y</span> * <span class="var">other</span>.<span class="attr">y</span>
|
||||||
|
<span class="kw">return</span> <span class="kw">NotImplemented</span>
|
||||||
|
|
||||||
|
<span class="kw">def</span> <span class="fn">__rmul__</span>(<span class="var">self</span>, <span class="var">other</span>):
|
||||||
|
<span class="cm"># Ermöglicht: 3 * v (Zahl steht links)</span>
|
||||||
|
<span class="kw">return</span> <span class="var">self</span>.<span class="fn">__mul__</span>(<span class="var">other</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Deine Aufgaben -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>4. 🎯 Deine Aufgaben in <code>aufgabe.py</code></h2>
|
||||||
|
|
||||||
|
<p>Öffne die Datei <code>08_operator_overloading_dunder/aufgabe.py</code> und vervollständige die folgenden Klassen:</p>
|
||||||
|
|
||||||
|
<ul class="task-list">
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t8_1">
|
||||||
|
<label for="t8_1" class="task-label">
|
||||||
|
<strong>TODO 1:</strong> <code>Vektor2D.__init__</code> mit <code>x</code> und <code>y</code> (als <code>float</code> konvertiert).
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t8_2">
|
||||||
|
<label for="t8_2" class="task-label">
|
||||||
|
<strong>TODO 2:</strong> <code>__repr__</code> (<code>"Vektor2D(3.0, 4.0)"</code>) und <code>__str__</code> (<code>"(3.0, 4.0)"</code>).
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t8_3">
|
||||||
|
<label for="t8_3" class="task-label">
|
||||||
|
<strong>TODO 3:</strong> <code>laenge(self)</code> ($\sqrt{x^2 + y^2}$) und <code>__abs__(self)</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t8_4">
|
||||||
|
<label for="t8_4" class="task-label">
|
||||||
|
<strong>TODO 4:</strong> <code>__add__</code> und <code>__sub__</code> für komponentenweise Vektor-Arithmetik.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t8_5">
|
||||||
|
<label for="t8_5" class="task-label">
|
||||||
|
<strong>TODO 5:</strong> <code>__mul__</code> (für Zahlen & Skalarprodukt) und <code>__rmul__</code> (für <code>3 * v</code>).
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t8_6">
|
||||||
|
<label for="t8_6" class="task-label">
|
||||||
|
<strong>TODO 6:</strong> <code>__eq__</code> (Komponenten prüfen) sowie <code>__lt__</code> und <code>__le__</code> (nach Vektorlänge vergleichen).
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t8_7">
|
||||||
|
<label for="t8_7" class="task-label">
|
||||||
|
<strong>TODO 7, 8, 9:</strong> Die Klasse <code>Wegstrecke</code> mit <code>__len__</code>, <code>__getitem__</code> und <code>gesamtlaenge()</code> (nutzt Vektordifferenz und <code>abs()</code>).
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>💡 Lösungshinweise (Aufklappen bei Bedarf)</h3>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 3 (Länge berechnen mit math.sqrt)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="kw">import</span> <span class="var">math</span>
|
||||||
|
|
||||||
|
<span class="kw">def</span> <span class="fn">laenge</span>(<span class="var">self</span>) -> <span class="fn">float</span>:
|
||||||
|
<span class="kw">return</span> <span class="var">math</span>.<span class="fn">sqrt</span>(<span class="var">self</span>.<span class="attr">x</span>**<span class="num">2</span> + <span class="var">self</span>.<span class="attr">y</span>**<span class="num">2</span>)
|
||||||
|
|
||||||
|
<span class="kw">def</span> <span class="fn">__abs__</span>(<span class="var">self</span>) -> <span class="fn">float</span>:
|
||||||
|
<span class="kw">return</span> <span class="var">self</span>.<span class="fn">laenge</span>()</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 9 (Gesamtlänge der Wegstrecke berechnen)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<p>Iteriere über alle Punkte von <code>0</code> bis <code>len(self.punkte) - 2</code>. Die Distanz zwischen zwei Punkten ist einfach <code>abs(self.punkte[i+1] - self.punkte[i])</code>!</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. Testen & Navigation -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>5. 🧪 Lösung überprüfen</h2>
|
||||||
|
<p>Führe im Terminal im Ordner <code>08_operator_overloading_dunder</code> folgenden Befehl aus:</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Terminal</span>
|
||||||
|
</div>
|
||||||
|
<pre><code>python3 test_aufgabe.py</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-nav">
|
||||||
|
<a href="../07_referenzen_und_speicher/index.html" class="btn btn-secondary">← Zurück zu Kapitel 07</a>
|
||||||
|
<a href="../09_eigene_unit_tests_schreiben/index.html" class="btn">Weiter zu Kapitel 09 →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
146
08_operator_overloading_dunder/musterloesung.py
Normal file
146
08_operator_overloading_dunder/musterloesung.py
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
"""
|
||||||
|
Kapitel 08: Operator Overloading & Dunder-Methoden ➕
|
||||||
|
===================================================
|
||||||
|
Musterlösung: Vollständige Implementierung von Vektor2D und Wegstrecke.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import Union, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Vektor2D:
|
||||||
|
"""
|
||||||
|
Repräsentiert einen zweidimensionalen Vektor (x, y) in der Ebene.
|
||||||
|
Unterstützt mathematische Operatoren (+, -, *), Vergleiche (==, <, <=),
|
||||||
|
sowie __str__, __repr__ und __abs__.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, x: float, y: float):
|
||||||
|
self.x: float = float(x)
|
||||||
|
self.y: float = float(y)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""Entwickler-Darstellung: Vektor2D(3.0, 4.0)"""
|
||||||
|
return f"Vektor2D({self.x}, {self.y})"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Benutzerfreundliche Darstellung: (3.0, 4.0)"""
|
||||||
|
return f"({self.x}, {self.y})"
|
||||||
|
|
||||||
|
def laenge(self) -> float:
|
||||||
|
"""Berechnet den Betrag (die euklidische Länge) des Vektors: sqrt(x² + y²)"""
|
||||||
|
return math.sqrt(self.x**2 + self.y**2)
|
||||||
|
|
||||||
|
def __abs__(self) -> float:
|
||||||
|
"""Ermöglicht abs(v) zur Berechnung der Vektorlänge."""
|
||||||
|
return self.laenge()
|
||||||
|
|
||||||
|
def __add__(self, other: "Vektor2D") -> "Vektor2D":
|
||||||
|
"""Vektoraddition: (x1 + x2, y1 + y2)"""
|
||||||
|
if isinstance(other, Vektor2D):
|
||||||
|
return Vektor2D(self.x + other.x, self.y + other.y)
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __sub__(self, other: "Vektor2D") -> "Vektor2D":
|
||||||
|
"""Vektorsubtraktion: (x1 - x2, y1 - y2)"""
|
||||||
|
if isinstance(other, Vektor2D):
|
||||||
|
return Vektor2D(self.x - other.x, self.y - other.y)
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __mul__(self, other: Union[int, float, "Vektor2D"]) -> Union["Vektor2D", float]:
|
||||||
|
"""
|
||||||
|
Multiplikation:
|
||||||
|
- Mit einer Zahl (int/float): Skalarmultiplikation -> neuer Vektor2D(x*k, y*k)
|
||||||
|
- Mit einem Vektor2D: Skalarprodukt (Dot Product) -> x1*x2 + y1*y2 (Zahl)
|
||||||
|
"""
|
||||||
|
if isinstance(other, (int, float)):
|
||||||
|
return Vektor2D(self.x * other, self.y * other)
|
||||||
|
elif isinstance(other, Vektor2D):
|
||||||
|
return self.x * other.x + self.y * other.y
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __rmul__(self, other: Union[int, float]) -> "Vektor2D":
|
||||||
|
"""Ermöglicht Zahl * Vektor (z.B. 3 * v), indem __mul__ aufgerufen wird."""
|
||||||
|
if isinstance(other, (int, float)):
|
||||||
|
return self.__mul__(other)
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
"""Prüft zwei Vektoren auf Gleichheit ihrer Komponenten."""
|
||||||
|
if not isinstance(other, Vektor2D):
|
||||||
|
return False
|
||||||
|
return math.isclose(self.x, other.x, abs_tol=1e-9) and math.isclose(self.y, other.y, abs_tol=1e-9)
|
||||||
|
|
||||||
|
def __lt__(self, other: "Vektor2D") -> bool:
|
||||||
|
"""Vergleicht zwei Vektoren anhand ihrer Länge (<)."""
|
||||||
|
if isinstance(other, Vektor2D):
|
||||||
|
return self.laenge() < other.laenge()
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __le__(self, other: "Vektor2D") -> bool:
|
||||||
|
"""Vergleicht zwei Vektoren anhand ihrer Länge (<=)."""
|
||||||
|
if isinstance(other, Vektor2D):
|
||||||
|
return self.laenge() <= other.laenge()
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
class Wegstrecke:
|
||||||
|
"""
|
||||||
|
Eine Sequenz von 2D-Wegpunkten, die Container-Dunder (__len__, __getitem__)
|
||||||
|
nutzt und die Gesamtlänge der Strecke berechnet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, punkte: Optional[List[Vektor2D]] = None):
|
||||||
|
self.punkte: List[Vektor2D] = list(punkte) if punkte else []
|
||||||
|
|
||||||
|
def punkt_hinzufuegen(self, punkt: Vektor2D) -> None:
|
||||||
|
"""Fügt einen neuen Wegpunkt hinzu."""
|
||||||
|
self.punkte.append(punkt)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Gibt die Anzahl der Wegpunkte zurück: len(strecke)"""
|
||||||
|
return len(self.punkte)
|
||||||
|
|
||||||
|
def __getitem__(self, index: int) -> Vektor2D:
|
||||||
|
"""Ermöglicht Indexzugriff per strecke[i]"""
|
||||||
|
return self.punkte[index]
|
||||||
|
|
||||||
|
def gesamtlaenge(self) -> float:
|
||||||
|
"""
|
||||||
|
Berechnet die Gesamtdistanz entlang aller Wegpunkte.
|
||||||
|
Nutzt Vektorsubtraktion und abs(), um Distanzen zwischen
|
||||||
|
aufeinanderfolgenden Punkten zu summieren.
|
||||||
|
"""
|
||||||
|
if len(self.punkte) < 2:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
distanz = 0.0
|
||||||
|
for i in range(len(self.punkte) - 1):
|
||||||
|
segment = self.punkte[i + 1] - self.punkte[i]
|
||||||
|
distanz += abs(segment)
|
||||||
|
return distanz
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Terminal-Demo zum direkten Ausführen
|
||||||
|
# ==============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
v1 = Vektor2D(3, 4)
|
||||||
|
v2 = Vektor2D(1, 2)
|
||||||
|
|
||||||
|
print(f"v1: {v1}") # (3.0, 4.0)
|
||||||
|
print(f"repr(v1): {repr(v1)}") # Vektor2D(3.0, 4.0)
|
||||||
|
print(f"Länge |v1|: {abs(v1)}") # 5.0
|
||||||
|
print(f"v1 + v2 = {v1 + v2}") # (4.0, 6.0)
|
||||||
|
print(f"v1 - v2 = {v1 - v2}") # (2.0, 2.0)
|
||||||
|
print(f"v1 * 3 = {v1 * 3}") # (9.0, 12.0)
|
||||||
|
print(f"3 * v1 = {3 * v1}") # (9.0, 12.0)
|
||||||
|
print(f"Skalarprodukt v1 * v2: {v1 * v2}") # 3*1 + 4*2 = 11.0
|
||||||
|
print(f"v1 == Vektor2D(3, 4): {v1 == Vektor2D(3, 4)}") # True
|
||||||
|
print(f"v2 < v1: {v2 < v1}") # True (Länge sqrt(5) < 5.0)
|
||||||
|
|
||||||
|
# Wegstrecken-Test
|
||||||
|
route = Wegstrecke([Vektor2D(0, 0), Vektor2D(3, 0), Vektor2D(3, 4)])
|
||||||
|
print(f"Anzahl Punkte: {len(route)}") # 3
|
||||||
|
print(f"Erster Punkt: {route[0]}") # (0.0, 0.0)
|
||||||
|
print(f"Gesamtlänge Route: {route.gesamtlaenge()}") # 3 + 4 = 7.0
|
||||||
142
08_operator_overloading_dunder/test_aufgabe.py
Normal file
142
08_operator_overloading_dunder/test_aufgabe.py
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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 TestKapitel08(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
Vektor2D = getattr(aufgabe, "Vektor2D", None)
|
||||||
|
Wegstrecke = getattr(aufgabe, "Wegstrecke", None)
|
||||||
|
self.assertIsNotNone(Vektor2D, "Klasse 'Vektor2D' nicht in aufgabe.py gefunden!")
|
||||||
|
self.assertIsNotNone(Wegstrecke, "Klasse 'Wegstrecke' nicht in aufgabe.py gefunden!")
|
||||||
|
self.Vektor2D = Vektor2D
|
||||||
|
self.Wegstrecke = Wegstrecke
|
||||||
|
|
||||||
|
def test_01_init_und_darstellung(self):
|
||||||
|
"""Prüft __init__, __repr__ und __str__ von Vektor2D."""
|
||||||
|
v = self.Vektor2D(3, 4)
|
||||||
|
self.assertEqual(float(v.x), 3.0)
|
||||||
|
self.assertEqual(float(v.y), 4.0)
|
||||||
|
|
||||||
|
# __repr__ soll Entwickler-Syntax liefern
|
||||||
|
self.assertEqual(repr(v), "Vektor2D(3.0, 4.0)")
|
||||||
|
|
||||||
|
# __str__ soll lesbare Klammer-Syntax liefern
|
||||||
|
self.assertEqual(str(v), "(3.0, 4.0)")
|
||||||
|
|
||||||
|
def test_02_laenge_und_abs(self):
|
||||||
|
"""Prüft laenge() und die Dunder-Methode __abs__."""
|
||||||
|
v = self.Vektor2D(3, 4)
|
||||||
|
self.assertAlmostEqual(v.laenge(), 5.0, places=4)
|
||||||
|
self.assertAlmostEqual(abs(v), 5.0, places=4)
|
||||||
|
|
||||||
|
v_null = self.Vektor2D(0, 0)
|
||||||
|
self.assertAlmostEqual(abs(v_null), 0.0, places=4)
|
||||||
|
|
||||||
|
v_neg = self.Vektor2D(-6, -8)
|
||||||
|
self.assertAlmostEqual(abs(v_neg), 10.0, places=4)
|
||||||
|
|
||||||
|
def test_03_addition_und_subtraktion(self):
|
||||||
|
"""Prüft __add__ und __sub__ zweier Vektoren."""
|
||||||
|
v1 = self.Vektor2D(3, 4)
|
||||||
|
v2 = self.Vektor2D(1, -2)
|
||||||
|
|
||||||
|
v_sum = v1 + v2
|
||||||
|
self.assertIsInstance(v_sum, self.Vektor2D, "Das Ergebnis von v1 + v2 muss ein Vektor2D sein!")
|
||||||
|
self.assertAlmostEqual(v_sum.x, 4.0)
|
||||||
|
self.assertAlmostEqual(v_sum.y, 2.0)
|
||||||
|
|
||||||
|
v_diff = v1 - v2
|
||||||
|
self.assertIsInstance(v_diff, self.Vektor2D, "Das Ergebnis von v1 - v2 muss ein Vektor2D sein!")
|
||||||
|
self.assertAlmostEqual(v_diff.x, 2.0)
|
||||||
|
self.assertAlmostEqual(v_diff.y, 6.0)
|
||||||
|
|
||||||
|
def test_04_skalarmultiplikation_und_skalarprodukt(self):
|
||||||
|
"""Prüft Multiplikation mit Zahlen (__mul__, __rmul__) und Skalarprodukt (__mul__)."""
|
||||||
|
v = self.Vektor2D(2.5, -4.0)
|
||||||
|
|
||||||
|
# Skalarmultiplikation Zahl von rechts
|
||||||
|
v_mal_2 = v * 2
|
||||||
|
self.assertIsInstance(v_mal_2, self.Vektor2D)
|
||||||
|
self.assertAlmostEqual(v_mal_2.x, 5.0)
|
||||||
|
self.assertAlmostEqual(v_mal_2.y, -8.0)
|
||||||
|
|
||||||
|
# Skalarmultiplikation Zahl von links (__rmul__)
|
||||||
|
v_links = 3 * v
|
||||||
|
self.assertIsInstance(v_links, self.Vektor2D)
|
||||||
|
self.assertAlmostEqual(v_links.x, 7.5)
|
||||||
|
self.assertAlmostEqual(v_links.y, -12.0)
|
||||||
|
|
||||||
|
# Skalarprodukt zweier Vektoren: v1 * v2 -> x1*x2 + y1*y2
|
||||||
|
v1 = self.Vektor2D(2, 3)
|
||||||
|
v2 = self.Vektor2D(4, -1)
|
||||||
|
skalarprodukt = v1 * v2
|
||||||
|
self.assertTrue(isinstance(skalarprodukt, (int, float)), "Skalarprodukt muss eine Zahl sein!")
|
||||||
|
self.assertAlmostEqual(skalarprodukt, 2 * 4 + 3 * (-1)) # 8 - 3 = 5
|
||||||
|
|
||||||
|
def test_05_vergleiche(self):
|
||||||
|
"""Prüft __eq__, __lt__ und __le__."""
|
||||||
|
v1 = self.Vektor2D(3, 4) # Länge 5.0
|
||||||
|
v2 = self.Vektor2D(3.0, 4.0) # Länge 5.0 (gleiche Werte)
|
||||||
|
v3 = self.Vektor2D(0, 5) # Länge 5.0 (andere Werte)
|
||||||
|
v4 = self.Vektor2D(1, 1) # Länge sqrt(2) ≈ 1.414
|
||||||
|
v5 = self.Vektor2D(6, 8) # Länge 10.0
|
||||||
|
|
||||||
|
# Gleichheit
|
||||||
|
self.assertEqual(v1, v2)
|
||||||
|
self.assertNotEqual(v1, v3)
|
||||||
|
self.assertNotEqual(v1, "kein vektor")
|
||||||
|
|
||||||
|
# Kleiner (<)
|
||||||
|
self.assertTrue(v4 < v1)
|
||||||
|
self.assertFalse(v1 < v4)
|
||||||
|
self.assertFalse(v1 < v3) # gleiche Länge
|
||||||
|
|
||||||
|
# Kleiner-Gleich (<=)
|
||||||
|
self.assertTrue(v4 <= v1)
|
||||||
|
self.assertTrue(v1 <= v3) # gleiche Länge -> True
|
||||||
|
self.assertTrue(v1 <= v5)
|
||||||
|
self.assertFalse(v5 <= v1)
|
||||||
|
|
||||||
|
def test_06_wegstrecke_container(self):
|
||||||
|
"""Prüft Wegstrecke Initialisierung, __len__ und __getitem__."""
|
||||||
|
p1 = self.Vektor2D(0, 0)
|
||||||
|
p2 = self.Vektor2D(2, 3)
|
||||||
|
p3 = self.Vektor2D(5, 7)
|
||||||
|
|
||||||
|
strecke = self.Wegstrecke([p1, p2])
|
||||||
|
self.assertEqual(len(strecke), 2, "len(strecke) muss 2 zurückgeben!")
|
||||||
|
self.assertEqual(strecke[0], p1, "strecke[0] muss p1 sein!")
|
||||||
|
self.assertEqual(strecke[1], p2, "strecke[1] muss p2 sein!")
|
||||||
|
|
||||||
|
strecke.punkt_hinzufuegen(p3)
|
||||||
|
self.assertEqual(len(strecke), 3)
|
||||||
|
self.assertEqual(strecke[2], p3)
|
||||||
|
|
||||||
|
def test_07_wegstrecke_gesamtlaenge(self):
|
||||||
|
"""Prüft Wegstrecke.gesamtlaenge() mit 0, 1 und 3 Wegpunkten."""
|
||||||
|
strecke_leer = self.Wegstrecke()
|
||||||
|
self.assertAlmostEqual(strecke_leer.gesamtlaenge(), 0.0)
|
||||||
|
|
||||||
|
strecke_einzeln = self.Wegstrecke([self.Vektor2D(10, 20)])
|
||||||
|
self.assertAlmostEqual(strecke_einzeln.gesamtlaenge(), 0.0)
|
||||||
|
|
||||||
|
# Route (0,0) -> (3,0) Distanz = 3; (3,0) -> (3,4) Distanz = 4; Summe = 7
|
||||||
|
route = self.Wegstrecke([
|
||||||
|
self.Vektor2D(0, 0),
|
||||||
|
self.Vektor2D(3, 0),
|
||||||
|
self.Vektor2D(3, 4)
|
||||||
|
])
|
||||||
|
self.assertAlmostEqual(route.gesamtlaenge(), 7.0, places=4)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
48
09_eigene_unit_tests_schreiben/README.md
Normal file
48
09_eigene_unit_tests_schreiben/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Kapitel 09: Eigene Unit Tests schreiben & TDD 🧪
|
||||||
|
|
||||||
|
In diesem Kapitel lernst du, wie professionelle Software-Entwickler ihren Code mit automatischen Tests absichern und wie **Test-Driven Development (TDD)** funktioniert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Was du lernst
|
||||||
|
|
||||||
|
1. **Warum automatische Unit Tests?**
|
||||||
|
- Nie wieder manuelle `print()`-Ausgaben kontrollieren.
|
||||||
|
- Sofortiges Feedback bei Fehlern (Regression Prevention).
|
||||||
|
- Saubere Schnittstellendokumentation.
|
||||||
|
2. **Das `unittest`-Modul in Python:**
|
||||||
|
- Eigene Testklassen erstellen: `class MeinTest(unittest.TestCase)`
|
||||||
|
- Jede Testmethode muss mit `test_` beginnen (z.B. `def test_add(self):`).
|
||||||
|
- Die `setUp(self)`-Methode: Wird automatisch vor **jedem einzelnen Test** ausgeführt, um frische Testdaten bereitzustellen.
|
||||||
|
3. **Die wichtigsten Assert-Methoden:**
|
||||||
|
- `self.assertEqual(a, b)`: Prüft $a == b$.
|
||||||
|
- `self.assertAlmostEqual(a, b, places=2)`: Prüft Float-Rundung bei Kommazahlen.
|
||||||
|
- `self.assertTrue(x)` / `self.assertFalse(x)`: Prüft Wahrheitswerte.
|
||||||
|
- `self.assertIn(item, list)` / `self.assertNotIn(item, list)`: Prüft Enthaltensein.
|
||||||
|
- `with self.assertRaises(FehlerTyp):`: Prüft, ob erwartete Fehler (Exceptions wie `ValueError`, `ZeroDivisionError`) geworfen werden.
|
||||||
|
4. **Test-Driven Development (TDD):**
|
||||||
|
- 🔴 **Red:** Schreibe zuerst den Test (schlägt fehl).
|
||||||
|
- 🟢 **Green:** Schreibe den minimalen Code, der den Test besteht.
|
||||||
|
- 🔵 **Refactor:** Verbessere und säubere den Code ohne Angst vor Regressionen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Die Dateien in diesem Ordner
|
||||||
|
|
||||||
|
- **`index.html`**: Ausführliche interaktive HTML-Erklärung mit Assert-Tabelle, visuellen Boxen & Tipps.
|
||||||
|
- **`aufgabe.py`**: Vorgegebene Klassen `Taschenrechner` und `Bankkonto` sowie die Test-Klassen mit TODOs zum Selbstausfüllen.
|
||||||
|
- **`test_aufgabe.py`**: Der Metatest-Runner, der deine geschriebenen Unit Tests validiert (`python3 test_aufgabe.py`).
|
||||||
|
- **`musterloesung.py`**: Vollständig ausprogrammierte Testsuite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bearbeite die Test-Klassen in aufgabe.py
|
||||||
|
# 2. Führe deine eigenen Tests aus:
|
||||||
|
python3 aufgabe.py
|
||||||
|
|
||||||
|
# 3. Validiere deine Tests mit dem Metatest-Skript:
|
||||||
|
python3 test_aufgabe.py
|
||||||
|
```
|
||||||
183
09_eigene_unit_tests_schreiben/aufgabe.py
Normal file
183
09_eigene_unit_tests_schreiben/aufgabe.py
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
"""
|
||||||
|
Kapitel 09: Eigene Unit Tests schreiben & TDD 🧪
|
||||||
|
================================================
|
||||||
|
Aufgabe: Schreibe deine eigenen Unit Tests mit dem 'unittest'-Modul!
|
||||||
|
|
||||||
|
Die Klassen 'Taschenrechner' und 'Bankkonto' sind bereits fertig programmiert.
|
||||||
|
Deine Aufgabe ist es, die Testklassen 'TestTaschenrechner' und 'TestBankkonto'
|
||||||
|
zu vervollständigen, sodass alle Funktionalitäten und Randfälle gründlich geprüft werden.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
import unittest
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Vorgegebene Klassen (nicht verändern, sondern testen!)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class Taschenrechner:
|
||||||
|
"""Einfacher Taschenrechner für mathematische Grundoperationen."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.speicher: float = 0.0
|
||||||
|
|
||||||
|
def add(self, a: float, b: float) -> float:
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
def sub(self, a: float, b: float) -> float:
|
||||||
|
return a - b
|
||||||
|
|
||||||
|
def mul(self, a: float, b: float) -> float:
|
||||||
|
return a * b
|
||||||
|
|
||||||
|
def div(self, a: float, b: float) -> float:
|
||||||
|
if b == 0:
|
||||||
|
raise ZeroDivisionError("Division durch Null ist nicht erlaubt!")
|
||||||
|
return a / b
|
||||||
|
|
||||||
|
def wurzel(self, a: float) -> float:
|
||||||
|
if a < 0:
|
||||||
|
raise ValueError("Wurzel aus negativen Zahlen ist im Reellen nicht definiert!")
|
||||||
|
return math.sqrt(a)
|
||||||
|
|
||||||
|
def speicher_plus(self, wert: float) -> None:
|
||||||
|
self.speicher += wert
|
||||||
|
|
||||||
|
def speicher_loeschen(self) -> None:
|
||||||
|
self.speicher = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class Bankkonto:
|
||||||
|
"""Modelliert ein Bankkonto mit Ein-, Auszahlungen und Überweisungen."""
|
||||||
|
|
||||||
|
def __init__(self, inhaber: str, kontostand: float = 0.0):
|
||||||
|
self.inhaber: str = inhaber
|
||||||
|
self.kontostand: float = float(kontostand)
|
||||||
|
self.transaktionen: List[float] = [float(kontostand)]
|
||||||
|
|
||||||
|
def einzahlen(self, betrag: float) -> None:
|
||||||
|
if betrag <= 0:
|
||||||
|
raise ValueError("Einzahlungsbetrag muss größer als 0 sein!")
|
||||||
|
self.kontostand += betrag
|
||||||
|
self.transaktionen.append(betrag)
|
||||||
|
|
||||||
|
def abheben(self, betrag: float) -> None:
|
||||||
|
if betrag <= 0:
|
||||||
|
raise ValueError("Auszahlungsbetrag muss größer als 0 sein!")
|
||||||
|
if betrag > self.kontostand:
|
||||||
|
raise ValueError("Nicht genügend Guthaben auf dem Konto!")
|
||||||
|
self.kontostand -= betrag
|
||||||
|
self.transaktionen.append(-betrag)
|
||||||
|
|
||||||
|
def ueberweisen(self, ziel_konto: "Bankkonto", betrag: float) -> None:
|
||||||
|
self.abheben(betrag)
|
||||||
|
ziel_konto.einzahlen(betrag)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# HIER BEGINNT DEINE AUFGABE: Eigene Unit Tests schreiben!
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class TestTaschenrechner(unittest.TestCase):
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 1: setUp(self)
|
||||||
|
# Erstelle vor jedem Test eine frische Rechner-Instanz: self.calc = Taschenrechner()
|
||||||
|
# ==========================================================================
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 2: test_01_grundrechenarten(self)
|
||||||
|
# Teste mit self.assertEqual:
|
||||||
|
# - self.calc.add(2, 3) soll 5 sein
|
||||||
|
# - self.calc.sub(10, 4) soll 6 sein
|
||||||
|
# - self.calc.mul(3, 7) soll 21 sein
|
||||||
|
# ==========================================================================
|
||||||
|
def test_01_grundrechenarten(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 3: test_02_division_und_kommazahlen(self)
|
||||||
|
# - Teste self.calc.div(10, 2) mit self.assertEqual (soll 5.0 sein)
|
||||||
|
# - Teste self.calc.div(10, 3) mit self.assertAlmostEqual(..., places=2) (ca. 3.33)
|
||||||
|
# ==========================================================================
|
||||||
|
def test_02_division_und_kommazahlen(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 4: test_03_division_durch_null_exception(self)
|
||||||
|
# Teste mit 'with self.assertRaises(ZeroDivisionError):'
|
||||||
|
# dass self.calc.div(10, 0) den Fehler auslöst!
|
||||||
|
# ==========================================================================
|
||||||
|
def test_03_division_durch_null_exception(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 5: test_04_wurzel_negativ_exception(self)
|
||||||
|
# - Teste self.calc.wurzel(16) soll 4.0 sein (assertEqual oder assertAlmostEqual)
|
||||||
|
# - Teste mit 'with self.assertRaises(ValueError):' dass self.calc.wurzel(-9)
|
||||||
|
# einen ValueError auslöst!
|
||||||
|
# ==========================================================================
|
||||||
|
def test_04_wurzel_negativ_exception(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 6: test_05_speicher_funktionen(self)
|
||||||
|
# - Prüfe, dass self.calc.speicher zu Beginn 0.0 ist
|
||||||
|
# - Rufe self.calc.speicher_plus(15.5) und self.calc.speicher_plus(4.5) auf
|
||||||
|
# - Prüfe, dass self.calc.speicher jetzt 20.0 ist
|
||||||
|
# - Rufe self.calc.speicher_loeschen() auf und prüfe, dass speicher wieder 0.0 ist
|
||||||
|
# ==========================================================================
|
||||||
|
def test_05_speicher_funktionen(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestBankkonto(unittest.TestCase):
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 7: setUp(self)
|
||||||
|
# Erstelle zwei Testkonten:
|
||||||
|
# - self.k1 = Bankkonto("Alice", 100.0)
|
||||||
|
# - self.k2 = Bankkonto("Bob", 50.0)
|
||||||
|
# ==========================================================================
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 8: test_06_einzahlen_und_abheben(self)
|
||||||
|
# - Zahle 50.0 auf self.k1 ein und prüfe kontostand == 150.0
|
||||||
|
# - Prüfe mit self.assertIn(50.0, self.k1.transaktionen), dass die Einzahlung gelistet ist
|
||||||
|
# - Hebe 30.0 von self.k1 ab und prüfe kontostand == 120.0
|
||||||
|
# - Prüfe mit self.assertIn(-30.0, self.k1.transaktionen), dass die Auszahlung gelistet ist
|
||||||
|
# ==========================================================================
|
||||||
|
def test_06_einzahlen_und_abheben(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 9: test_07_abheben_ueber_limit_exception(self)
|
||||||
|
# Teste mit 'with self.assertRaises(ValueError):'
|
||||||
|
# dass das Abheben von 200.0 (bei 100.0 Guthaben) einen ValueError auslöst!
|
||||||
|
# ==========================================================================
|
||||||
|
def test_07_abheben_ueber_limit_exception(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 10: test_08_ueberweisung(self)
|
||||||
|
# - Überweise 40.0 von self.k1 an self.k2
|
||||||
|
# - Prüfe self.k1.kontostand == 60.0
|
||||||
|
# - Prüfe self.k2.kontostand == 90.0
|
||||||
|
# - Prüfe Transaktionen: -40.0 in self.k1.transaktionen und 40.0 in self.k2.transaktionen
|
||||||
|
# ==========================================================================
|
||||||
|
def test_08_ueberweisung(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Führe deine eigenen Tests direkt im Terminal aus:
|
||||||
|
# python3 aufgabe.py
|
||||||
|
# ==============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
299
09_eigene_unit_tests_schreiben/index.html
Normal file
299
09_eigene_unit_tests_schreiben/index.html
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kapitel 09: Eigene Unit Tests schreiben & TDD</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="../08_operator_overloading_dunder/index.html">Kapitel 08</a>
|
||||||
|
<span>›</span>
|
||||||
|
<span style="color: white; font-weight: 600;">Kapitel 09</span>
|
||||||
|
</nav>
|
||||||
|
<div class="header-badge">Kapitel 09 • Softwarequalität & TDD</div>
|
||||||
|
<h1>Eigene Unit Tests schreiben & TDD 🧪🛡️</h1>
|
||||||
|
<p>Lerne, wie du deinen Code mit Pythons <code>unittest</code> absicherst, Test-Driven Development anwendest und Fehler sofort entlarvst.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
|
||||||
|
<!-- 1. Das Konzept: Warum Unit Tests? -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>1. Warum schreiben Profis eigene Unit Tests?</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Bisher hast du deine Programme vermutlich getestet, indem du <code>print()</code>-Befehle geschrieben hast und per Auge überprüft hast, ob das Ergebnis stimmt.
|
||||||
|
Das Problem: Bei 100 Funktionen vergisst man leicht Randfälle, und wenn man eine Zeile ändert, merkt man oft nicht, dass man an anderer Stelle etwas kaputtgemacht hat (eine sogenannte <em>Regression</em>).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🪂 Das automatische Sicherheitsnetz:</div>
|
||||||
|
Ein <strong>Unit Test</strong> ist ein kleines Testprogramm, das eine einzelne Funktion oder Methode isoliert prüft.
|
||||||
|
Es vergleicht das tatsächliche Ergebnis automatisch mit dem Soll-Wert.
|
||||||
|
Läuft dein Code in 0.01 Sekunden durch 50 Tests, kannst du jederzeit mit 100%iger Sicherheit sagen: <em>"Mein Code funktioniert fehlerfrei!"</em>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TDD Cycle -->
|
||||||
|
<h3>Der TDD-Entwicklungszyklus (Red • Green • Refactor)</h3>
|
||||||
|
<p>In der agilen Entwicklung nutzt man oft <strong>Test-Driven Development (TDD)</strong>:</p>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 15px; margin: 20px 0;">
|
||||||
|
|
||||||
|
<div style="background: var(--danger-light); border: 2px solid var(--danger); border-radius: var(--radius-md); padding: 16px;">
|
||||||
|
<h4 style="color: var(--danger); margin-bottom: 6px;">🔴 1. Red (Test schreiben)</h4>
|
||||||
|
<p style="font-size: 0.9rem; margin-bottom: 0; color: #7f1d1d;">
|
||||||
|
Du schreibst zuerst den Test für ein Feature, das es noch gar nicht gibt. Der Test schlägt rot fehl.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: var(--success-light); border: 2px solid var(--success); border-radius: var(--radius-md); padding: 16px;">
|
||||||
|
<h4 style="color: var(--success); margin-bottom: 6px;">🟢 2. Green (Code schreiben)</h4>
|
||||||
|
<p style="font-size: 0.9rem; margin-bottom: 0; color: #064e3b;">
|
||||||
|
Du implementierst genau so viel Code, dass der Test grün wird.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: var(--primary-light); border: 2px solid var(--primary); border-radius: var(--radius-md); padding: 16px;">
|
||||||
|
<h4 style="color: var(--primary); margin-bottom: 6px;">🔵 3. Refactor (Aufräumen)</h4>
|
||||||
|
<p style="font-size: 0.9rem; margin-bottom: 0; color: #312e81;">
|
||||||
|
Du verbesserst den Code, machst ihn lesbarer und schneller – abgesichert durch deine Tests.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. Anatomie einer unittest-Klasse -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>2. Der Aufbau einer <code>unittest</code>-Klasse</h2>
|
||||||
|
|
||||||
|
<p>Python bringt ein komplettes Testframework bereits mit: <code>import unittest</code>.</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Struktur einer Testklasse in Python</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">import</span> <span class="var">unittest</span>
|
||||||
|
|
||||||
|
<span class="kw">class</span> <span class="fn">TestTaschenrechner</span>(<span class="var">unittest</span>.<span class="fn">TestCase</span>):
|
||||||
|
|
||||||
|
<span class="kw">def</span> <span class="fn">setUp</span>(<span class="var">self</span>):
|
||||||
|
<span class="cm"># Wird AUTOMATISCH vor JEDEM einzelnen Test ausgeführt!</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">calc</span> = <span class="fn">Taschenrechner</span>()
|
||||||
|
|
||||||
|
<span class="kw">def</span> <span class="fn">test_addition</span>(<span class="var">self</span>):
|
||||||
|
<span class="cm"># WICHTIG: Jede Testmethode MUSS mit "test_" beginnen!</span>
|
||||||
|
<span class="var">ergebnis</span> = <span class="var">self</span>.<span class="attr">calc</span>.<span class="fn">add</span>(<span class="num">2</span>, <span class="num">3</span>)
|
||||||
|
<span class="var">self</span>.<span class="fn">assertEqual</span>(<span class="var">ergebnis</span>, <span class="num">5</span>)
|
||||||
|
|
||||||
|
<span class="kw">if</span> <span class="var">__name__</span> == <span class="str">"__main__"</span>:
|
||||||
|
<span class="var">unittest</span>.<span class="fn">main</span>(<span class="attr">verbosity</span>=<span class="num">2</span>)</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-tipp">
|
||||||
|
<div class="box-title">💡 Warum <code>setUp()</code> so genial ist:</div>
|
||||||
|
In <code>setUp()</code> erstellst du frische Objekte für jeden Testlauf. Dadurch beeinflussen sich Tests niemals gegenseitig (Test-Isolation).
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. Die wichtigsten Assert-Befehle -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>3. Die wichtigsten Assert-Methoden im Überblick</h2>
|
||||||
|
|
||||||
|
<p>In deinen Testmethoden verwendest du <code>self.assert...()</code>, um Bedingungen zu prüfen:</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);">Assert-Methode</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">Bedeutung</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>self.assertEqual(a, b)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Prüft $a == b$</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertEqual(calc.add(2, 3), 5)</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertNotEqual(a, b)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Prüft $a \ne b$</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertNotEqual(konto.stand, 0)</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertAlmostEqual(a, b, places=2)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Prüft Kommazahlen mit Rundung</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertAlmostEqual(10/3, 3.333, places=2)</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertTrue(x)</code> / <code>assertFalse(x)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Prüft Booleschen Wert</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertTrue(konto.ist_aktiv)</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertIn(item, liste)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Prüft, ob Element in Liste/String ist</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertIn(50.0, konto.transaktionen)</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertIsNone(x)</code> / <code>assertIsNotNone(x)</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Prüft <code>is None</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>self.assertIsNone(person.mutter)</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>with self.assertRaises(FehlerTyp):</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Prüft, ob eine Exception geworfen wird</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>with self.assertRaises(ValueError): konto.abheben(9999)</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Fehler & Exceptions testen mit <code>assertRaises</code></h3>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Beispiel: Testen auf erwartete Abstürze / Fehler</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">def</span> <span class="fn">test_division_durch_null</span>(<span class="var">self</span>):
|
||||||
|
<span class="cm"># Der Test besteht NUR dann, wenn innerhalb des with-Blocks</span>
|
||||||
|
<span class="cm"># tatsächlich ein ZeroDivisionError ausgelöst wird!</span>
|
||||||
|
<span class="kw">with</span> <span class="var">self</span>.<span class="fn">assertRaises</span>(<span class="fn">ZeroDivisionError</span>):
|
||||||
|
<span class="var">self</span>.<span class="attr">calc</span>.<span class="fn">div</span>(<span class="num">10</span>, <span class="num">0</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Deine Aufgaben -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>4. 🎯 Deine Aufgaben in <code>aufgabe.py</code></h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In <code>09_eigene_unit_tests_schreiben/aufgabe.py</code> sind die Klassen <code>Taschenrechner</code> und <code>Bankkonto</code> bereits fertig vorgegeben.
|
||||||
|
Deine Aufgabe ist es, die Unit-Test-Klassen zu schreiben!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="task-list">
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t9_1">
|
||||||
|
<label for="t9_1" class="task-label">
|
||||||
|
<strong>TODO 1:</strong> <code>TestTaschenrechner.setUp</code>: Instanziiere <code>self.calc = Taschenrechner()</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t9_2">
|
||||||
|
<label for="t9_2" class="task-label">
|
||||||
|
<strong>TODO 2:</strong> <code>test_01_grundrechenarten</code>: Teste <code>add</code>, <code>sub</code>, <code>mul</code> mit <code>assertEqual</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t9_3">
|
||||||
|
<label for="t9_3" class="task-label">
|
||||||
|
<strong>TODO 3:</strong> <code>test_02_division_und_kommazahlen</code>: Teste normale Division und <code>10 / 3</code> mit <code>assertAlmostEqual</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t9_4">
|
||||||
|
<label for="t9_4" class="task-label">
|
||||||
|
<strong>TODO 4:</strong> <code>test_03_division_durch_null_exception</code>: Teste mit <code>assertRaises(ZeroDivisionError)</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t9_5">
|
||||||
|
<label for="t9_5" class="task-label">
|
||||||
|
<strong>TODO 5:</strong> <code>test_04_wurzel_negativ_exception</code>: Teste normale Wurzel und <code>assertRaises(ValueError)</code> bei <code>wurzel(-9)</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t9_6">
|
||||||
|
<label for="t9_6" class="task-label">
|
||||||
|
<strong>TODO 6:</strong> <code>test_05_speicher_funktionen</code>: Teste <code>speicher_plus</code> und <code>speicher_loeschen</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t9_7">
|
||||||
|
<label for="t9_7" class="task-label">
|
||||||
|
<strong>TODO 7:</strong> <code>TestBankkonto.setUp</code>: Erstelle zwei Konten <code>self.k1 = Bankkonto("Alice", 100.0)</code> und <code>self.k2 = Bankkonto("Bob", 50.0)</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t9_8">
|
||||||
|
<label for="t9_8" class="task-label">
|
||||||
|
<strong>TODO 8:</strong> <code>test_06_einzahlen_und_abheben</code>: Teste Ein- und Auszahlung und prüfe <code>transaktionen</code> mit <code>assertIn</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t9_9">
|
||||||
|
<label for="t9_9" class="task-label">
|
||||||
|
<strong>TODO 9:</strong> <code>test_07_abheben_ueber_limit_exception</code>: Teste <code>assertRaises(ValueError)</code> bei Abheben von mehr als Kontostand.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t9_10">
|
||||||
|
<label for="t9_10" class="task-label">
|
||||||
|
<strong>TODO 10:</strong> <code>test_08_ueberweisung</code>: Überweise 40.0 von <code>k1</code> nach <code>k2</code> und prüfe beide Kontostände & Transaktionen.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>💡 Lösungshinweise (Aufklappen bei Bedarf)</h3>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 3 (Periodische Kommazahlen testen)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<p>Weil <code>10 / 3 = 3.3333333333333335</code> im Computer nicht exakt dargestellt werden kann, vergleicht man mit Rundungstoleranz:</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="var">self</span>.<span class="fn">assertAlmostEqual</span>(<span class="var">self</span>.<span class="attr">calc</span>.<span class="fn">div</span>(<span class="num">10</span>, <span class="num">3</span>), <span class="num">3.3333</span>, <span class="attr">places</span>=<span class="num">2</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 8 & 10 (Transaktionen mit assertIn prüfen)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="var">self</span>.<span class="attr">k1</span>.<span class="fn">einzahlen</span>(<span class="num">50.0</span>)
|
||||||
|
<span class="var">self</span>.<span class="fn">assertEqual</span>(<span class="var">self</span>.<span class="attr">k1</span>.<span class="attr">kontostand</span>, <span class="num">150.0</span>)
|
||||||
|
<span class="var">self</span>.<span class="fn">assertIn</span>(<span class="num">50.0</span>, <span class="var">self</span>.<span class="attr">k1</span>.<span class="attr">transaktionen</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. Testen & Navigation -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>5. 🧪 Deine Tests ausführen & validieren</h2>
|
||||||
|
|
||||||
|
<p><strong>Schritt 1:</strong> Führe deine eigenen Tests im Terminal aus:</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Terminal (Eigene Tests starten)</span>
|
||||||
|
</div>
|
||||||
|
<pre><code>python3 aufgabe.py</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>Schritt 2:</strong> Validiere deine Tests mit dem Metatest-Skript (prüft, ob deine Tests auch fehlerhaften Code entlarven!):</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Terminal (Metatests prüfen)</span>
|
||||||
|
</div>
|
||||||
|
<pre><code>python3 test_aufgabe.py</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-nav">
|
||||||
|
<a href="../08_operator_overloading_dunder/index.html" class="btn btn-secondary">← Zurück zu Kapitel 08</a>
|
||||||
|
<a href="../10_vererbung_und_super/index.html" class="btn">Weiter zu Kapitel 10 (Vererbung) →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
149
09_eigene_unit_tests_schreiben/musterloesung.py
Normal file
149
09_eigene_unit_tests_schreiben/musterloesung.py
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
"""
|
||||||
|
Kapitel 09: Eigene Unit Tests schreiben & TDD 🧪
|
||||||
|
================================================
|
||||||
|
Musterlösung: Vollständige Test-Suite für Taschenrechner und Bankkonto.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
import unittest
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Vorgegebene Klassen: Taschenrechner & Bankkonto
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class Taschenrechner:
|
||||||
|
"""Einfacher Taschenrechner für mathematische Grundoperationen."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.speicher: float = 0.0
|
||||||
|
|
||||||
|
def add(self, a: float, b: float) -> float:
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
def sub(self, a: float, b: float) -> float:
|
||||||
|
return a - b
|
||||||
|
|
||||||
|
def mul(self, a: float, b: float) -> float:
|
||||||
|
return a * b
|
||||||
|
|
||||||
|
def div(self, a: float, b: float) -> float:
|
||||||
|
if b == 0:
|
||||||
|
raise ZeroDivisionError("Division durch Null ist nicht erlaubt!")
|
||||||
|
return a / b
|
||||||
|
|
||||||
|
def wurzel(self, a: float) -> float:
|
||||||
|
if a < 0:
|
||||||
|
raise ValueError("Wurzel aus negativen Zahlen ist im Reellen nicht definiert!")
|
||||||
|
return math.sqrt(a)
|
||||||
|
|
||||||
|
def speicher_plus(self, wert: float) -> None:
|
||||||
|
self.speicher += wert
|
||||||
|
|
||||||
|
def speicher_loeschen(self) -> None:
|
||||||
|
self.speicher = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class Bankkonto:
|
||||||
|
"""Modelliert ein Bankkonto mit Ein-, Auszahlungen und Überweisungen."""
|
||||||
|
|
||||||
|
def __init__(self, inhaber: str, kontostand: float = 0.0):
|
||||||
|
self.inhaber: str = inhaber
|
||||||
|
self.kontostand: float = float(kontostand)
|
||||||
|
self.transaktionen: List[float] = [float(kontostand)]
|
||||||
|
|
||||||
|
def einzahlen(self, betrag: float) -> None:
|
||||||
|
if betrag <= 0:
|
||||||
|
raise ValueError("Einzahlungsbetrag muss größer als 0 sein!")
|
||||||
|
self.kontostand += betrag
|
||||||
|
self.transaktionen.append(betrag)
|
||||||
|
|
||||||
|
def abheben(self, betrag: float) -> None:
|
||||||
|
if betrag <= 0:
|
||||||
|
raise ValueError("Auszahlungsbetrag muss größer als 0 sein!")
|
||||||
|
if betrag > self.kontostand:
|
||||||
|
raise ValueError("Nicht genügend Guthaben auf dem Konto!")
|
||||||
|
self.kontostand -= betrag
|
||||||
|
self.transaktionen.append(-betrag)
|
||||||
|
|
||||||
|
def ueberweisen(self, ziel_konto: "Bankkonto", betrag: float) -> None:
|
||||||
|
self.abheben(betrag)
|
||||||
|
ziel_konto.einzahlen(betrag)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Musterlösung: Die Unit-Test-Klassen
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class TestTaschenrechner(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Wird vor JEDEM einzelnen Test ausgeführt."""
|
||||||
|
self.calc = Taschenrechner()
|
||||||
|
|
||||||
|
def test_01_grundrechenarten(self):
|
||||||
|
"""Testet Addition, Subtraktion und Multiplikation."""
|
||||||
|
self.assertEqual(self.calc.add(2, 3), 5)
|
||||||
|
self.assertEqual(self.calc.sub(10, 4), 6)
|
||||||
|
self.assertEqual(self.calc.mul(3, 7), 21)
|
||||||
|
|
||||||
|
def test_02_division_und_kommazahlen(self):
|
||||||
|
"""Testet normale Division und periodische Kommazahlen mit assertAlmostEqual."""
|
||||||
|
self.assertEqual(self.calc.div(10, 2), 5.0)
|
||||||
|
self.assertAlmostEqual(self.calc.div(10, 3), 3.3333, places=2)
|
||||||
|
|
||||||
|
def test_03_division_durch_null_exception(self):
|
||||||
|
"""Testet, dass bei Division durch 0 ein ZeroDivisionError geworfen wird."""
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
self.calc.div(10, 0)
|
||||||
|
|
||||||
|
def test_04_wurzel_negativ_exception(self):
|
||||||
|
"""Testet normale Wurzel und Exception bei negativen Zahlen."""
|
||||||
|
self.assertAlmostEqual(self.calc.wurzel(16), 4.0)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.calc.wurzel(-9)
|
||||||
|
|
||||||
|
def test_05_speicher_funktionen(self):
|
||||||
|
"""Testet speicher_plus und speicher_loeschen."""
|
||||||
|
self.assertEqual(self.calc.speicher, 0.0)
|
||||||
|
self.calc.speicher_plus(15.5)
|
||||||
|
self.calc.speicher_plus(4.5)
|
||||||
|
self.assertEqual(self.calc.speicher, 20.0)
|
||||||
|
self.calc.speicher_loeschen()
|
||||||
|
self.assertEqual(self.calc.speicher, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBankkonto(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Erstellt zwei frische Konten vor jedem Test."""
|
||||||
|
self.k1 = Bankkonto("Alice", 100.0)
|
||||||
|
self.k2 = Bankkonto("Bob", 50.0)
|
||||||
|
|
||||||
|
def test_06_einzahlen_und_abheben(self):
|
||||||
|
"""Testet Ein- und Auszahlung sowie Transaktionshistorie."""
|
||||||
|
self.k1.einzahlen(50.0)
|
||||||
|
self.assertEqual(self.k1.kontostand, 150.0)
|
||||||
|
self.assertIn(50.0, self.k1.transaktionen)
|
||||||
|
|
||||||
|
self.k1.abheben(30.0)
|
||||||
|
self.assertEqual(self.k1.kontostand, 120.0)
|
||||||
|
self.assertIn(-30.0, self.k1.transaktionen)
|
||||||
|
|
||||||
|
def test_07_abheben_ueber_limit_exception(self):
|
||||||
|
"""Testet, dass Abheben von mehr als Kontostand einen ValueError auslöst."""
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.k1.abheben(200.0)
|
||||||
|
|
||||||
|
def test_08_ueberweisung(self):
|
||||||
|
"""Testet erfolgreiche Überweisung zwischen zwei Konten."""
|
||||||
|
self.k1.ueberweisen(self.k2, 40.0)
|
||||||
|
self.assertEqual(self.k1.kontostand, 60.0)
|
||||||
|
self.assertEqual(self.k2.kontostand, 90.0)
|
||||||
|
self.assertIn(-40.0, self.k1.transaktionen)
|
||||||
|
self.assertIn(40.0, self.k2.transaktionen)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
175
09_eigene_unit_tests_schreiben/test_aufgabe.py
Normal file
175
09_eigene_unit_tests_schreiben/test_aufgabe.py
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
import io
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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 TestKapitel09Meta(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Metatest-Suite: Prüft, ob die Schülerin saubere, wirksame Unit Tests geschrieben hat.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.TestTaschenrechner = getattr(aufgabe, "TestTaschenrechner", None)
|
||||||
|
self.TestBankkonto = getattr(aufgabe, "TestBankkonto", None)
|
||||||
|
self.assertIsNotNone(self.TestTaschenrechner, "Klasse 'TestTaschenrechner' nicht in aufgabe.py gefunden!")
|
||||||
|
self.assertIsNotNone(self.TestBankkonto, "Klasse 'TestBankkonto' nicht in aufgabe.py gefunden!")
|
||||||
|
self.assertTrue(issubclass(self.TestTaschenrechner, unittest.TestCase))
|
||||||
|
self.assertTrue(issubclass(self.TestBankkonto, unittest.TestCase))
|
||||||
|
|
||||||
|
def _run_test_case(self, test_class, test_method_name):
|
||||||
|
"""Hilfsfunktion: Führt eine einzelne Testmethode aus und gibt das TestResult zurück."""
|
||||||
|
suite = unittest.TestSuite()
|
||||||
|
suite.addTest(test_class(test_method_name))
|
||||||
|
stream = io.StringIO()
|
||||||
|
runner = unittest.TextTestRunner(stream=stream, verbosity=0)
|
||||||
|
return runner.run(suite)
|
||||||
|
|
||||||
|
def test_01_alle_testmethoden_vorhanden(self):
|
||||||
|
"""Prüft, ob alle 8 geforderten Testmethoden existieren."""
|
||||||
|
erwartete_calc_tests = [
|
||||||
|
"test_01_grundrechenarten",
|
||||||
|
"test_02_division_und_kommazahlen",
|
||||||
|
"test_03_division_durch_null_exception",
|
||||||
|
"test_04_wurzel_negativ_exception",
|
||||||
|
"test_05_speicher_funktionen",
|
||||||
|
]
|
||||||
|
for name in erwartete_calc_tests:
|
||||||
|
self.assertTrue(
|
||||||
|
hasattr(self.TestTaschenrechner, name),
|
||||||
|
f"Methode '{name}' fehlt in TestTaschenrechner!"
|
||||||
|
)
|
||||||
|
|
||||||
|
erwartete_konto_tests = [
|
||||||
|
"test_06_einzahlen_und_abheben",
|
||||||
|
"test_07_abheben_ueber_limit_exception",
|
||||||
|
"test_08_ueberweisung",
|
||||||
|
]
|
||||||
|
for name in erwartete_konto_tests:
|
||||||
|
self.assertTrue(
|
||||||
|
hasattr(self.TestBankkonto, name),
|
||||||
|
f"Methode '{name}' fehlt in TestBankkonto!"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_02_tests_bestehen_auf_korrektem_code(self):
|
||||||
|
"""Prüft, ob alle Tests auf dem korrekten Code erfolgreich durchlaufen."""
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
suite = unittest.TestSuite()
|
||||||
|
suite.addTests(loader.loadTestsFromTestCase(self.TestTaschenrechner))
|
||||||
|
suite.addTests(loader.loadTestsFromTestCase(self.TestBankkonto))
|
||||||
|
|
||||||
|
stream = io.StringIO()
|
||||||
|
runner = unittest.TextTestRunner(stream=stream, verbosity=0)
|
||||||
|
result = runner.run(suite)
|
||||||
|
|
||||||
|
self.assertGreaterEqual(result.testsRun, 8, "Es müssen mindestens 8 Tests ausgeführt werden!")
|
||||||
|
self.assertEqual(
|
||||||
|
len(result.failures), 0,
|
||||||
|
f"Deine Tests sind auf korrektem Code fehlgeschlagen:\n{stream.getvalue()}"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
len(result.errors), 0,
|
||||||
|
f"Deine Tests haben Fehler/Abstürze verursacht:\n{stream.getvalue()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_03_entlarvt_rechenfehler(self):
|
||||||
|
"""Mutationstest: Prüft, ob test_01 fehlerhafte Addition entlarvt."""
|
||||||
|
OriginalCalc = aufgabe.Taschenrechner
|
||||||
|
class BuggyCalc(OriginalCalc):
|
||||||
|
def add(self, a, b):
|
||||||
|
return a + b + 99 # Absichtlicher Bug
|
||||||
|
|
||||||
|
try:
|
||||||
|
aufgabe.Taschenrechner = BuggyCalc
|
||||||
|
result = self._run_test_case(self.TestTaschenrechner, "test_01_grundrechenarten")
|
||||||
|
self.assertFalse(
|
||||||
|
result.wasSuccessful(),
|
||||||
|
"test_01_grundrechenarten muss fehlschlagen, wenn add() ein falsches Ergebnis liefert! Hast du self.assertEqual verwendet?"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
aufgabe.Taschenrechner = OriginalCalc
|
||||||
|
|
||||||
|
def test_04_entlarvt_fehlende_nulldivision_exception(self):
|
||||||
|
"""Mutationstest: Prüft, ob test_03 fehlende ZeroDivisionError Exception entlarvt."""
|
||||||
|
OriginalCalc = aufgabe.Taschenrechner
|
||||||
|
class BuggyCalc(OriginalCalc):
|
||||||
|
def div(self, a, b):
|
||||||
|
if b == 0:
|
||||||
|
return 0.0 # Fehler: Wirft KEINE Exception
|
||||||
|
return a / b
|
||||||
|
|
||||||
|
try:
|
||||||
|
aufgabe.Taschenrechner = BuggyCalc
|
||||||
|
result = self._run_test_case(self.TestTaschenrechner, "test_03_division_durch_null_exception")
|
||||||
|
self.assertFalse(
|
||||||
|
result.wasSuccessful(),
|
||||||
|
"test_03_division_durch_null_exception muss fehlschlagen, wenn div(10, 0) keine Exception wirft! Nutze 'with self.assertRaises(ZeroDivisionError):'."
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
aufgabe.Taschenrechner = OriginalCalc
|
||||||
|
|
||||||
|
def test_05_entlarvt_fehlende_wurzel_exception(self):
|
||||||
|
"""Mutationstest: Prüft, ob test_04 fehlende ValueError Exception entlarvt."""
|
||||||
|
OriginalCalc = aufgabe.Taschenrechner
|
||||||
|
class BuggyCalc(OriginalCalc):
|
||||||
|
def wurzel(self, a):
|
||||||
|
if a < 0:
|
||||||
|
return 0.0 # Fehler: Wirft KEINE Exception
|
||||||
|
return super().wurzel(a)
|
||||||
|
|
||||||
|
try:
|
||||||
|
aufgabe.Taschenrechner = BuggyCalc
|
||||||
|
result = self._run_test_case(self.TestTaschenrechner, "test_04_wurzel_negativ_exception")
|
||||||
|
self.assertFalse(
|
||||||
|
result.wasSuccessful(),
|
||||||
|
"test_04_wurzel_negativ_exception muss fehlschlagen, wenn wurzel(-9) keinen ValueError wirft!"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
aufgabe.Taschenrechner = OriginalCalc
|
||||||
|
|
||||||
|
def test_06_entlarvt_fehlende_ueberziehung_exception(self):
|
||||||
|
"""Mutationstest: Prüft, ob test_07 unbefugte Überziehung entlarvt."""
|
||||||
|
OriginalKonto = aufgabe.Bankkonto
|
||||||
|
class BuggyKonto(OriginalKonto):
|
||||||
|
def abheben(self, betrag):
|
||||||
|
# Fehler: Erlaubt Überziehung ohne ValueError
|
||||||
|
self.kontostand -= betrag
|
||||||
|
|
||||||
|
try:
|
||||||
|
aufgabe.Bankkonto = BuggyKonto
|
||||||
|
result = self._run_test_case(self.TestBankkonto, "test_07_abheben_ueber_limit_exception")
|
||||||
|
self.assertFalse(
|
||||||
|
result.wasSuccessful(),
|
||||||
|
"test_07_abheben_ueber_limit_exception muss fehlschlagen, wenn abheben() ohne Prüfung das Konto überzieht!"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
aufgabe.Bankkonto = OriginalKonto
|
||||||
|
|
||||||
|
def test_07_entlarvt_fehlerhafte_ueberweisung(self):
|
||||||
|
"""Mutationstest: Prüft, ob test_08 fehlerhafte Überweisung entlarvt."""
|
||||||
|
OriginalKonto = aufgabe.Bankkonto
|
||||||
|
class BuggyKonto(OriginalKonto):
|
||||||
|
def ueberweisen(self, ziel_konto, betrag):
|
||||||
|
self.abheben(betrag)
|
||||||
|
# Fehler: Zielkonto erhält kein Geld!
|
||||||
|
|
||||||
|
try:
|
||||||
|
aufgabe.Bankkonto = BuggyKonto
|
||||||
|
result = self._run_test_case(self.TestBankkonto, "test_08_ueberweisung")
|
||||||
|
self.assertFalse(
|
||||||
|
result.wasSuccessful(),
|
||||||
|
"test_08_ueberweisung muss fehlschlagen, wenn bei der Überweisung das Zielkonto nicht gutgeschrieben wird!"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
aufgabe.Bankkonto = OriginalKonto
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
91
10_vererbung_und_super/README.md
Normal file
91
10_vererbung_und_super/README.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Kapitel 10: Vererbung (Inheritance) & super() 🧬🚗
|
||||||
|
|
||||||
|
Willkommen zu Kapitel 10! In diesem Kapitel lernst du eines der mächtigsten Konzepte der Objektorientierten Programmierung kennen: **Vererbung**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Was du lernst
|
||||||
|
|
||||||
|
1. **Was ist Vererbung?**
|
||||||
|
- Basisklasse (Elternklasse / Superklasse) vs. abgeleitete Klasse (Kindklasse / Subklasse).
|
||||||
|
- "Ist-ein"-Beziehung: Ein `Auto` *ist ein* `Fahrzeug`. Ein `ElektroAuto` *ist ein* `Auto`.
|
||||||
|
2. **Das DRY-Prinzip (Don't Repeat Yourself):**
|
||||||
|
- Gemeinsame Attribute (`marke`, `modell`, `baujahr`, `grundpreis`, `kilometerstand`) und Methoden (`fahren()`, `berechne_restwert()`) müssen nur **einmal** in der Basisklasse `Fahrzeug` geschrieben werden.
|
||||||
|
3. **Syntax in Python:**
|
||||||
|
```python
|
||||||
|
class KindKlasse(ElternKlasse):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
4. **Der Konstruktor der Elternklasse: `super().__init__()`:**
|
||||||
|
- Mit `super().__init__(...)` delegierst du die Initialisierung der geerbten Attribute an die Elternklasse.
|
||||||
|
5. **Methoden erweitern & überschreiben (Method Overriding):**
|
||||||
|
- Eine Kindklasse kann Methoden der Elternklasse neu definieren oder mit `super().methode()` erweitern.
|
||||||
|
6. **Typ-Prüfung:**
|
||||||
|
- `isinstance(mein_auto, Fahrzeug)` → `True`
|
||||||
|
- `issubclass(ElektroAuto, Fahrzeug)` → `True`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 UML-Klassendiagramm
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────┐
|
||||||
|
│ Fahrzeug │
|
||||||
|
├───────────────────────────────────────────────┤
|
||||||
|
│ - marke: str │
|
||||||
|
│ - modell: str │
|
||||||
|
│ - baujahr: int │
|
||||||
|
│ - grundpreis: float │
|
||||||
|
│ - kilometerstand: float │
|
||||||
|
├───────────────────────────────────────────────┤
|
||||||
|
│ + __init__(marke, modell, baujahr, grundpreis)│
|
||||||
|
│ + fahren(km: float) -> None │
|
||||||
|
│ + berechne_restwert(aktuelles_jahr) -> float │
|
||||||
|
│ + info() -> str │
|
||||||
|
└───────────────────────▲───────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────┴───────────────┐
|
||||||
|
│ │
|
||||||
|
┌───────────┴───────────────┐ ┌───────────┴───────────────┐
|
||||||
|
│ Auto │ │ Lkw │
|
||||||
|
├───────────────────────────┤ ├───────────────────────────┤
|
||||||
|
│ - sitzplaetze: int │ │ - max_zuladung_kg: float │
|
||||||
|
│ - anzahl_tueren: int │ │ - aktuelle_ladung_kg: floa│
|
||||||
|
├───────────────────────────┤ ├───────────────────────────┤
|
||||||
|
│ + __init__(..., sitze, ...)│ │ + __init__(..., max_ladung)│
|
||||||
|
│ + hupen() -> str │ │ + beladen(kg) -> bool │
|
||||||
|
│ + info() -> str │ │ + entladen(kg) -> float │
|
||||||
|
└───────────▲───────────────┘ │ + info() -> str │
|
||||||
|
│ └───────────────────────────┘
|
||||||
|
┌───────────┴───────────────┐
|
||||||
|
│ ElektroAuto │
|
||||||
|
├───────────────────────────┤
|
||||||
|
│ - batterie_kapazitaet_kwh │
|
||||||
|
│ - batterie_ladestand_kwh │
|
||||||
|
│ - verbrauch_pro_100km │
|
||||||
|
├───────────────────────────┤
|
||||||
|
│ + reichweite() -> float │
|
||||||
|
│ + fahren(km) -> bool │
|
||||||
|
│ + aufladen(kwh) -> float │
|
||||||
|
│ + info() -> str │
|
||||||
|
└───────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Die Dateien in diesem Ordner
|
||||||
|
|
||||||
|
- **`index.html`**: Die interaktive Lernseite mit visuellen Erklärungen, Analogien, Diagrammen und Lösungstipps.
|
||||||
|
- **`aufgabe.py`**: Dein Arbeitsblatt mit den Klassen `Fahrzeug`, `Auto`, `ElektroAuto` und `Lkw`.
|
||||||
|
- **`test_aufgabe.py`**: Automatische Tests (`python3 test_aufgabe.py`).
|
||||||
|
- **`musterloesung.py`**: Vollständig ausprogrammierte Beispiellösung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bearbeite aufgabe.py
|
||||||
|
# 2. Teste deine Lösung:
|
||||||
|
python3 test_aufgabe.py
|
||||||
|
```
|
||||||
284
10_vererbung_und_super/aufgabe.py
Normal file
284
10_vererbung_und_super/aufgabe.py
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
"""
|
||||||
|
Kapitel 10: Vererbung (Inheritance) & super() 🧬🚗
|
||||||
|
===================================================
|
||||||
|
Schulabgleich: 25.0 Vererbung
|
||||||
|
|
||||||
|
Aufgabe:
|
||||||
|
Erstelle eine Hierarchie von Fahrzeugen für eine moderne Fahrzeugflotte.
|
||||||
|
Nutze Vererbung (DRY-Prinzip) und super().__init__(), um gemeinsame Attribute
|
||||||
|
und Methoden von der Basisklasse 'Fahrzeug' an 'Auto', 'ElektroAuto' und 'Lkw' zu vererben.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Fahrzeug:
|
||||||
|
"""
|
||||||
|
Basisklasse für alle Fahrzeuge.
|
||||||
|
|
||||||
|
Attribute:
|
||||||
|
marke (str): Hersteller (z.B. "BMW", "Tesla", "MAN")
|
||||||
|
modell (str): Modellbezeichnung (z.B. "i4", "Model 3", "TGX")
|
||||||
|
baujahr (int): Baujahr des Fahrzeugs (z.B. 2022)
|
||||||
|
grundpreis (float): Ursprünglicher Neupreis in Euro
|
||||||
|
kilometerstand (float): Bereits gefahrene Kilometer (Startwert: 0.0)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 1: Konstruktor __init__ der Basisklasse
|
||||||
|
# Parameter: marke (str), modell (str), baujahr (int), grundpreis (float)
|
||||||
|
# Attribute:
|
||||||
|
# - self.marke: str
|
||||||
|
# - self.modell: str
|
||||||
|
# - self.baujahr: int
|
||||||
|
# - self.grundpreis: float (als float konvertieren)
|
||||||
|
# - self.kilometerstand: float (Startwert: 0.0)
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(self, marke: str, modell: str, baujahr: int, grundpreis: float):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 2: Methode fahren(self, km: float) -> None
|
||||||
|
# Erhöht den Kilometerstand um 'km', wenn km > 0 ist.
|
||||||
|
# Bei km <= 0 passiert nichts.
|
||||||
|
# ==========================================================================
|
||||||
|
def fahren(self, km: float) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 3: Methode berechne_restwert(self, aktuelles_jahr: int) -> float
|
||||||
|
# Berechnet den aktuellen Restwert des Fahrzeugs:
|
||||||
|
# 1. Alter = max(0, aktuelles_jahr - self.baujahr)
|
||||||
|
# 2. Wertverlust = self.grundpreis * 0.05 * Alter (5% Wertverlust pro Jahr)
|
||||||
|
# 3. Restwert = self.grundpreis - Wertverlust
|
||||||
|
# 4. Mindestrestwert ist 10% des Grundpreises: max(self.grundpreis * 0.10, Restwert)
|
||||||
|
# Rückgabe: Restwert als float
|
||||||
|
# ==========================================================================
|
||||||
|
def berechne_restwert(self, aktuelles_jahr: int) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 4: Methode info(self) -> str
|
||||||
|
# Gibt einen formatierten Infostring zurück:
|
||||||
|
# Format: "{marke} {modell} ({baujahr}) - {kilometerstand:.1f} km"
|
||||||
|
# Beispiel: "BMW 320d (2020) - 45000.0 km"
|
||||||
|
# ==========================================================================
|
||||||
|
def info(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.info()
|
||||||
|
|
||||||
|
|
||||||
|
class Auto(Fahrzeug):
|
||||||
|
"""
|
||||||
|
Abgeleitete Klasse für PKWs (erbt von Fahrzeug).
|
||||||
|
|
||||||
|
Zusätzliche Attribute:
|
||||||
|
sitzplaetze (int): Anzahl der Sitze (Standardwert: 5)
|
||||||
|
anzahl_tueren (int): Anzahl der Türen (Standardwert: 5)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 5: Konstruktor __init__ von Auto
|
||||||
|
# Parameter: marke (str), modell (str), baujahr (int), grundpreis (float),
|
||||||
|
# sitzplaetze (int = 5), anzahl_tueren (int = 5)
|
||||||
|
# 1. Rufe den Konstruktor der Elternklasse mit super().__init__(...) auf!
|
||||||
|
# 2. Setze self.sitzplaetze und self.anzahl_tueren
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
marke: str,
|
||||||
|
modell: str,
|
||||||
|
baujahr: int,
|
||||||
|
grundpreis: float,
|
||||||
|
sitzplaetze: int = 5,
|
||||||
|
anzahl_tueren: int = 5,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 6: Methode hupen(self) -> str
|
||||||
|
# Gibt einen Hup-Laut mit Marke und Modell zurück:
|
||||||
|
# Format: "Hup hup! Platz da für den {marke} {modell}!"
|
||||||
|
# Beispiel: "Hup hup! Platz da für den VW Golf!"
|
||||||
|
# ==========================================================================
|
||||||
|
def hupen(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 7: Methode info(self) -> str (Method Overriding / Erweitern)
|
||||||
|
# Rufe super().info() auf und hänge " | {sitzplaetze} Sitze, {anzahl_tueren} Türen" an.
|
||||||
|
# Format: "{super().info()} | {self.sitzplaetze} Sitze, {self.anzahl_tueren} Türen"
|
||||||
|
# Beispiel: "VW Golf (2021) - 12000.0 km | 5 Sitze, 5 Türen"
|
||||||
|
# ==========================================================================
|
||||||
|
def info(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ElektroAuto(Auto):
|
||||||
|
"""
|
||||||
|
Abgeleitete Klasse für Elektroautos (Mehrstufige Vererbung: erbt von Auto).
|
||||||
|
|
||||||
|
Zusätzliche Attribute:
|
||||||
|
batterie_kapazitaet_kwh (float): Maximale Akkukapazität in Kilowattstunden
|
||||||
|
batterie_ladestand_kwh (float): Aktueller Ladestand (Startwert: voll = batterie_kapazitaet_kwh)
|
||||||
|
verbrauch_pro_100km (float): Verbrauch in kWh auf 100 km (Standardwert: 18.0)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 8: Konstruktor __init__ von ElektroAuto
|
||||||
|
# Parameter: marke (str), modell (str), baujahr (int), grundpreis (float),
|
||||||
|
# batterie_kapazitaet_kwh (float), sitzplaetze (int = 5),
|
||||||
|
# anzahl_tueren (int = 5), verbrauch_pro_100km (float = 18.0)
|
||||||
|
# 1. Rufe super().__init__(marke, modell, baujahr, grundpreis, sitzplaetze, anzahl_tueren) auf.
|
||||||
|
# 2. Setze self.batterie_kapazitaet_kwh = float(batterie_kapazitaet_kwh)
|
||||||
|
# 3. Setze self.batterie_ladestand_kwh = float(batterie_kapazitaet_kwh) (Akku ist anfangs voll)
|
||||||
|
# 4. Setze self.verbrauch_pro_100km = float(verbrauch_pro_100km)
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
marke: str,
|
||||||
|
modell: str,
|
||||||
|
baujahr: int,
|
||||||
|
grundpreis: float,
|
||||||
|
batterie_kapazitaet_kwh: float,
|
||||||
|
sitzplaetze: int = 5,
|
||||||
|
anzahl_tueren: int = 5,
|
||||||
|
verbrauch_pro_100km: float = 18.0,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 9: Methode reichweite(self) -> float
|
||||||
|
# Berechnet die verbleibende Reichweite in Kilometern:
|
||||||
|
# Formel: (aktueller_ladestand / verbrauch_pro_100km) * 100
|
||||||
|
# Wenn verbrauch_pro_100km <= 0: 0.0 zurückgeben.
|
||||||
|
# ==========================================================================
|
||||||
|
def reichweite(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 10: Methode fahren(self, km: float) -> bool (Überschreiben mit Logik)
|
||||||
|
# 1. Wenn km <= 0: False zurückgeben.
|
||||||
|
# 2. Berechne den benötigten Strom: benoetigt = (km / 100.0) * self.verbrauch_pro_100km
|
||||||
|
# 3. Wenn self.batterie_ladestand_kwh >= benoetigt:
|
||||||
|
# - Ziehe den Verbrauch vom Ladestand ab: self.batterie_ladestand_kwh -= benoetigt
|
||||||
|
# - Rufe super().fahren(km) auf (damit der Kilometerstand erhöht wird!)
|
||||||
|
# - return True
|
||||||
|
# 4. Wenn der Akku NICHT reicht:
|
||||||
|
# - Fahre nicht (keine Änderung an Kilometerstand oder Akku)
|
||||||
|
# - return False
|
||||||
|
# ==========================================================================
|
||||||
|
def fahren(self, km: float) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 11: Methode aufladen(self, kwh: float) -> float
|
||||||
|
# Lädt den Akku auf, aber maximal bis batterie_kapazitaet_kwh!
|
||||||
|
# 1. Wenn kwh <= 0: return 0.0
|
||||||
|
# 2. Berechne den freien Platz im Akku: batterie_kapazitaet_kwh - batterie_ladestand_kwh
|
||||||
|
# 3. Tatsächlich geladene Menge = min(freier_platz, kwh)
|
||||||
|
# 4. Erhöhe batterie_ladestand_kwh um die geladene Menge
|
||||||
|
# 5. Gib die tatsächlich geladene Menge (float) zurück.
|
||||||
|
# ==========================================================================
|
||||||
|
def aufladen(self, kwh: float) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 12: Methode info(self) -> str
|
||||||
|
# Format: "{super().info()} | Akku: {batterie_ladestand_kwh:.1f}/{batterie_kapazitaet_kwh:.1f} kWh ({reichweite():.1f} km Reichweite)"
|
||||||
|
# Beispiel: "Tesla Model 3 (2023) - 5000.0 km | 5 Sitze, 5 Türen | Akku: 60.0/60.0 kWh (333.3 km Reichweite)"
|
||||||
|
# ==========================================================================
|
||||||
|
def info(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Lkw(Fahrzeug):
|
||||||
|
"""
|
||||||
|
Abgeleitete Klasse für Lastkraftwagen (erbt direkt von Fahrzeug).
|
||||||
|
|
||||||
|
Zusätzliche Attribute:
|
||||||
|
max_zuladung_kg (float): Maximale Frachtkapazität in kg
|
||||||
|
aktuelle_ladung_kg (float): Aktuell geladene Fracht (Startwert: 0.0)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 13: Konstruktor __init__ von Lkw
|
||||||
|
# Parameter: marke (str), modell (str), baujahr (int), grundpreis (float),
|
||||||
|
# max_zuladung_kg (float)
|
||||||
|
# 1. Rufe super().__init__(marke, modell, baujahr, grundpreis) auf.
|
||||||
|
# 2. Setze self.max_zuladung_kg = float(max_zuladung_kg)
|
||||||
|
# 3. Setze self.aktuelle_ladung_kg = 0.0
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
marke: str,
|
||||||
|
modell: str,
|
||||||
|
baujahr: int,
|
||||||
|
grundpreis: float,
|
||||||
|
max_zuladung_kg: float,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 14: Methode beladen(self, kg: float) -> bool
|
||||||
|
# 1. Wenn kg > 0 und self.aktuelle_ladung_kg + kg <= self.max_zuladung_kg:
|
||||||
|
# - Erhöhe self.aktuelle_ladung_kg um kg
|
||||||
|
# - return True
|
||||||
|
# 2. Sonst (Überladung oder ungültiges Gewicht): return False
|
||||||
|
# ==========================================================================
|
||||||
|
def beladen(self, kg: float) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 15: Methode entladen(self, kg: float) -> float
|
||||||
|
# Entlädt maximal die vorhandene Ladung.
|
||||||
|
# 1. Wenn kg <= 0: return 0.0
|
||||||
|
# 2. tatsaechlich = min(self.aktuelle_ladung_kg, kg)
|
||||||
|
# 3. self.aktuelle_ladung_kg -= tatsaechlich
|
||||||
|
# 4. return tatsaechlich
|
||||||
|
# ==========================================================================
|
||||||
|
def entladen(self, kg: float) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 16: Methode info(self) -> str
|
||||||
|
# Format: "{super().info()} | Ladung: {aktuelle_ladung_kg:.1f}/{max_zuladung_kg:.1f} kg"
|
||||||
|
# Beispiel: "MAN TGX (2019) - 150000.0 km | Ladung: 8500.0/18000.0 kg"
|
||||||
|
# ==========================================================================
|
||||||
|
def info(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Terminal-Test zum Ausprobieren: (python3 aufgabe.py)
|
||||||
|
# ==============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("--- 🚗 Auto testen ---")
|
||||||
|
golf = Auto("VW", "Golf 8", 2021, 28000.0, sitzplaetze=5, anzahl_tueren=5)
|
||||||
|
golf.fahren(150.0)
|
||||||
|
print(golf.info())
|
||||||
|
print(golf.hupen())
|
||||||
|
print(f"Restwert im Jahr 2026: {golf.berechne_restwert(2026):.2f} €")
|
||||||
|
|
||||||
|
print("\n--- ⚡ ElektroAuto testen ---")
|
||||||
|
tesla = ElektroAuto("Tesla", "Model Y", 2023, 45000.0, batterie_kapazitaet_kwh=75.0, verbrauch_pro_100km=16.5)
|
||||||
|
print(tesla.info())
|
||||||
|
print(f"Start-Reichweite: {tesla.reichweite():.1f} km")
|
||||||
|
|
||||||
|
erfolg = tesla.fahren(200.0)
|
||||||
|
print(f"Fahrt 200 km erfolgreich? {erfolg}")
|
||||||
|
print(tesla.info())
|
||||||
|
|
||||||
|
geladen = tesla.aufladen(50.0)
|
||||||
|
print(f"Aufgeladen: {geladen:.1f} kWh")
|
||||||
|
print(tesla.info())
|
||||||
|
|
||||||
|
print("\n--- 🚛 LKW testen ---")
|
||||||
|
lkw = Lkw("Mercedes-Benz", "Actros", 2020, 110000.0, max_zuladung_kg=24000.0)
|
||||||
|
lkw.beladen(15000.0)
|
||||||
|
lkw.fahren(400.0)
|
||||||
|
print(lkw.info())
|
||||||
|
entladen = lkw.entladen(5000.0)
|
||||||
|
print(f"Entladen: {entladen:.1f} kg -> Neue Ladung: {lkw.aktuelle_ladung_kg:.1f} kg")
|
||||||
359
10_vererbung_und_super/index.html
Normal file
359
10_vererbung_und_super/index.html
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kapitel 10: Vererbung & super()</title>
|
||||||
|
<link rel="stylesheet" href="../assets/style.css">
|
||||||
|
<style>
|
||||||
|
/* Spezifische Styles für UML-Diagramme */
|
||||||
|
.uml-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 25px 0;
|
||||||
|
}
|
||||||
|
.uml-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 380px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.uml-header {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.uml-section {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
color: #334155;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.uml-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.uml-arrow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.uml-arrow::before {
|
||||||
|
content: '▲';
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.uml-arrow-line {
|
||||||
|
width: 2px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
.uml-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a href="../index.html">🏠 Zur Kursübersicht</a>
|
||||||
|
<span>›</span>
|
||||||
|
<a href="../09_eigene_unit_tests_schreiben/index.html">Kapitel 09 (Unit Tests)</a>
|
||||||
|
<span>›</span>
|
||||||
|
<span style="color: white; font-weight: 600;">Kapitel 10</span>
|
||||||
|
</nav>
|
||||||
|
<div class="header-badge">Kapitel 10 • Schulabgleich 25.0</div>
|
||||||
|
<h1>Vererbung (Inheritance) & <code>super()</code> 🧬🚗</h1>
|
||||||
|
<p>Lerne, wie Klassen Eigenschaften und Methoden an Unterklassen vererben, Code-Duplikate verschwinden und wie <code>super().__init__()</code> funktioniert.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
|
||||||
|
<!-- 1. Die Grundidee von Vererbung -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>1. Was ist Vererbung? (Das DRY-Prinzip)</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In vielen Software-Projekten gibt es Klassen, die sich sehr ähnlich sind.
|
||||||
|
Stelle dir eine Fahrzeugflotte vor: Ein <strong>Auto</strong>, ein <strong>ElektroAuto</strong> und ein <strong>Lkw</strong>
|
||||||
|
haben alle eine <em>Marke</em>, ein <em>Modell</em>, ein <em>Baujahr</em>, einen <em>Grundpreis</em> und können <em>fahren</em>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Ohne Vererbung müsstest du all diese Variablen und Methoden in <strong>jeder einzelnen Klasse kopieren</strong>.
|
||||||
|
Das verletzt eine der wichtigsten Regeln der Programmierung: <strong>DRY (Don't Repeat Yourself)</strong>!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🧬 Die DNA- & Stammbaum-Analogie:</div>
|
||||||
|
Wie in der Biologie: Kinder erben Merkmale (Augenfarbe, Blutgruppe) von ihren Eltern, entwickeln aber auch ihre ganz eigenen Fähigkeiten (z.B. Klavierspielen oder Fußballspielen).
|
||||||
|
<br><br>
|
||||||
|
In Python ist <code>Fahrzeug</code> die <strong>Elternklasse (Basisklasse)</strong> und <code>Auto</code> die <strong>Kindklasse (abgeleitete Klasse)</strong>.
|
||||||
|
Das Auto erbt automatisch alles, was ein Fahrzeug kann, und fügt eigene Besonderheiten wie <code>sitzplaetze</code> und <code>hupen()</code> hinzu!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-tipp">
|
||||||
|
<div class="box-title">🔍 "Ist-ein" (Vererbung) vs. "Hat-ein" (Komposition):</div>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Vererbung (Kapitel 10):</strong> Ein Auto <strong>ist ein</strong> Fahrzeug. Ein Elektroauto <strong>ist ein</strong> Auto.</li>
|
||||||
|
<li><strong>Komposition (Kapitel 05):</strong> Ein Auto <strong>hat einen</strong> Motor. Eine Playlist <strong>hat viele</strong> Songs.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. UML-Klassendiagramm -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>2. 📊 UML-Klassendiagramm: Die Vererbungshierarchie</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Im UML-Diagramm wird Vererbung durch einen <strong>Pfeil mit geschlossener Dreiecksspitze (▲)</strong> dargestellt.
|
||||||
|
Der Pfeil zeigt immer von der Kindklasse nach oben zur Elternklasse:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="uml-container">
|
||||||
|
<!-- Basisklasse Fahrzeug -->
|
||||||
|
<div class="uml-card">
|
||||||
|
<div class="uml-header">Fahrzeug (Basisklasse)</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
- marke: str<br>
|
||||||
|
- modell: str<br>
|
||||||
|
- baujahr: int<br>
|
||||||
|
- grundpreis: float<br>
|
||||||
|
- kilometerstand: float = 0.0
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
+ __init__(marke, modell, baujahr, grundpreis)<br>
|
||||||
|
+ fahren(km: float) -> None<br>
|
||||||
|
+ berechne_restwert(jahr: int) -> float<br>
|
||||||
|
+ info() -> str
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uml-arrow">
|
||||||
|
<div class="uml-arrow-line"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uml-row">
|
||||||
|
<!-- Kindklasse Auto -->
|
||||||
|
<div class="uml-card">
|
||||||
|
<div class="uml-header">Auto</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
- sitzplaetze: int = 5<br>
|
||||||
|
- anzahl_tueren: int = 5
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
+ __init__(..., sitzplaetze, tueren)<br>
|
||||||
|
+ hupen() -> str<br>
|
||||||
|
+ info() -> str <i>(überschrieben)</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kindklasse Lkw -->
|
||||||
|
<div class="uml-card">
|
||||||
|
<div class="uml-header">Lkw</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
- max_zuladung_kg: float<br>
|
||||||
|
- aktuelle_ladung_kg: float = 0.0
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
+ __init__(..., max_zuladung_kg)<br>
|
||||||
|
+ beladen(kg: float) -> bool<br>
|
||||||
|
+ entladen(kg: float) -> float<br>
|
||||||
|
+ info() -> str <i>(überschrieben)</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uml-arrow" style="margin-right: 200px;">
|
||||||
|
<div class="uml-arrow-line"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kindklasse ElektroAuto (erbt von Auto) -->
|
||||||
|
<div class="uml-card" style="margin-right: 200px;">
|
||||||
|
<div class="uml-header">ElektroAuto (erbt von Auto)</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
- batterie_kapazitaet_kwh: float<br>
|
||||||
|
- batterie_ladestand_kwh: float<br>
|
||||||
|
- verbrauch_pro_100km: float = 18.0
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
+ __init__(..., batterie_kapazitaet_kwh, ...)<br>
|
||||||
|
+ reichweite() -> float<br>
|
||||||
|
+ fahren(km: float) -> bool <i>(mit Stromverbrauch)</i><br>
|
||||||
|
+ aufladen(kwh: float) -> float<br>
|
||||||
|
+ info() -> str <i>(überschrieben)</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. Syntax & super() Erklärung -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>3. Syntax: Wie funktioniert <code>super()</code> in Python?</h2>
|
||||||
|
|
||||||
|
<h3>Schritt 1: Vererbung deklarieren</h3>
|
||||||
|
<p>Um von einer Klasse zu erben, schreibst du den Namen der Elternklasse in Klammern hinter den Klassennamen:</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Python (Vererbungssyntax)</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">class</span> <span class="fn">Auto</span>(<span class="fn">Fahrzeug</span>): <span class="cm"># Auto erbt alle Attribute und Methoden von Fahrzeug!</span>
|
||||||
|
<span class="kw">pass</span></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Schritt 2: Der Aufruf von <code>super().__init__(...)</code></h3>
|
||||||
|
<p>
|
||||||
|
Wenn die Kindklasse einen eigenen Konstruktor <code>__init__</code> besitzt, überschreibt sie zunächst den Eltern-Konstruktor.
|
||||||
|
Damit die Eltern-Attribute (<code>marke</code>, <code>modell</code>, etc.) trotzdem sauber initialisiert werden, rufen wir mit <code>super().__init__()</code> den Konstruktor der Elternklasse auf:
|
||||||
|
</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 (super().__init__ in Aktion)</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">class</span> <span class="fn">Auto</span>(<span class="fn">Fahrzeug</span>):
|
||||||
|
<span class="kw">def</span> <span class="fn">__init__</span>(<span class="var">self</span>, <span class="var">marke</span>: <span class="fn">str</span>, <span class="var">modell</span>: <span class="fn">str</span>, <span class="var">baujahr</span>: <span class="fn">int</span>, <span class="var">grundpreis</span>: <span class="fn">float</span>, <span class="var">sitzplaetze</span>: <span class="fn">int</span> = <span class="num">5</span>):
|
||||||
|
<span class="cm"># 1. Eltern-Konstruktor aufrufen (setzt marke, modell, baujahr, grundpreis, kilometerstand):</span>
|
||||||
|
<span class="fn">super</span>().<span class="fn">__init__</span>(<span class="var">marke</span>, <span class="var">modell</span>, <span class="var">baujahr</span>, <span class="var">grundpreis</span>)
|
||||||
|
|
||||||
|
<span class="cm"># 2. Zusätzliche, eigene Attribute der Kindklasse setzen:</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">sitzplaetze</span>: <span class="fn">int</span> = <span class="var">sitzplaetze</span></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-warning">
|
||||||
|
<div class="box-title">⚠️ Häufiger Anfängerfehler: <code>super().__init__()</code> vergessen</div>
|
||||||
|
Wenn du in der Kindklasse <code>super().__init__()</code> vergisst, existieren Attribute wie <code>self.marke</code> oder <code>self.kilometerstand</code> am Kind-Objekt überhaupt nicht! Der Code stürzt beim ersten Zugriff mit einem <code>AttributeError</code> ab.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Typ-Prüfungen: isinstance & issubclass -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>4. Typ-Prüfung: <code>isinstance()</code> und <code>issubclass()</code></h2>
|
||||||
|
|
||||||
|
<p>Weil ein <code>ElektroAuto</code> von <code>Auto</code> und dieses von <code>Fahrzeug</code> erbt, ist ein Elektroauto gleichzeitig eine Instanz aller drei Klassen:</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Python (Typ-Prüfungen)</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="var">tesla</span> = <span class="fn">ElektroAuto</span>(<span class="str">"Tesla"</span>, <span class="str">"Model 3"</span>, <span class="num">2023</span>, <span class="num">42000.0</span>, <span class="var">batterie_kapazitaet_kwh</span>=<span class="num">60.0</span>)
|
||||||
|
|
||||||
|
<span class="fn">print</span>(<span class="fn">isinstance</span>(<span class="var">tesla</span>, <span class="fn">ElektroAuto</span>)) <span class="cm"># True</span>
|
||||||
|
<span class="fn">print</span>(<span class="fn">isinstance</span>(<span class="var">tesla</span>, <span class="fn">Auto</span>)) <span class="cm"># True</span>
|
||||||
|
<span class="fn">print</span>(<span class="fn">isinstance</span>(<span class="var">tesla</span>, <span class="fn">Fahrzeug</span>)) <span class="cm"># True</span>
|
||||||
|
<span class="fn">print</span>(<span class="fn">isinstance</span>(<span class="var">tesla</span>, <span class="fn">Lkw</span>)) <span class="cm"># False (Lkw ist ein Geschwisterzweig!)</span>
|
||||||
|
|
||||||
|
<span class="fn">print</span>(<span class="fn">issubclass</span>(<span class="fn">ElektroAuto</span>, <span class="fn">Fahrzeug</span>)) <span class="cm"># True</span></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. Aufgaben -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>5. 🎯 Deine Aufgaben in <code>aufgabe.py</code></h2>
|
||||||
|
|
||||||
|
<p>Öffne die Datei <code>10_vererbung_und_super/aufgabe.py</code> und vervollständige die 4 Klassen:</p>
|
||||||
|
|
||||||
|
<ul class="task-list">
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t10_1">
|
||||||
|
<label for="t10_1" class="task-label">
|
||||||
|
<strong>TODO 1–4 (Basisklasse Fahrzeug):</strong> Konstruktor mit <code>marke</code>, <code>modell</code>, <code>baujahr</code>, <code>grundpreis</code>, <code>kilometerstand=0.0</code>, Methoden <code>fahren(km)</code>, <code>berechne_restwert(jahr)</code> und <code>info()</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t10_2">
|
||||||
|
<label for="t10_2" class="task-label">
|
||||||
|
<strong>TODO 5–7 (Kindklasse Auto):</strong> Erbt von <code>Fahrzeug</code>. Ruft <code>super().__init__()</code> auf, speichert <code>sitzplaetze</code> und <code>anzahl_tueren</code>, Methode <code>hupen()</code> und erweitert <code>info()</code> mit <code>super().info()</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t10_3">
|
||||||
|
<label for="t10_3" class="task-label">
|
||||||
|
<strong>TODO 8–12 (Kindklasse ElektroAuto):</strong> Erbt von <code>Auto</code>. Speichert Batteriekapazität & Ladestand, berechnet <code>reichweite()</code>, überschreibt <code>fahren(km)</code> mit Stromabzug und <code>super().fahren(km)</code>, implementiert <code>aufladen(kwh)</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t10_4">
|
||||||
|
<label for="t10_4" class="task-label">
|
||||||
|
<strong>TODO 13–16 (Kindklasse Lkw):</strong> Erbt von <code>Fahrzeug</code>. Speichert <code>max_zuladung_kg</code>, implementiert Frachtmanagement mit <code>beladen(kg)</code> und <code>entladen(kg)</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>💡 Lösungshinweise (Aufklappen bei Bedarf)</h3>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 3 (Restwert berechnen)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="var">alter</span> = <span class="fn">max</span>(<span class="num">0</span>, <span class="var">aktuelles_jahr</span> - <span class="var">self</span>.<span class="attr">baujahr</span>)
|
||||||
|
<span class="var">wertverlust</span> = <span class="var">self</span>.<span class="attr">grundpreis</span> * <span class="num">0.05</span> * <span class="var">alter</span>
|
||||||
|
<span class="var">restwert</span> = <span class="var">self</span>.<span class="attr">grundpreis</span> - <span class="var">wertverlust</span>
|
||||||
|
<span class="kw">return</span> <span class="fn">max</span>(<span class="var">self</span>.<span class="attr">grundpreis</span> * <span class="num">0.10</span>, <span class="var">restwert</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 10 (ElektroAuto fahren)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="kw">if</span> <span class="var">km</span> <= <span class="num">0</span>:
|
||||||
|
<span class="kw">return</span> <span class="kw">False</span>
|
||||||
|
<span class="var">benoetigt</span> = (<span class="var">km</span> / <span class="num">100.0</span>) * <span class="var">self</span>.<span class="attr">verbrauch_pro_100km</span>
|
||||||
|
<span class="kw">if</span> <span class="var">self</span>.<span class="attr">batterie_ladestand_kwh</span> >= <span class="var">benoetigt</span>:
|
||||||
|
<span class="var">self</span>.<span class="attr">batterie_ladestand_kwh</span> -= <span class="var">benoetigt</span>
|
||||||
|
<span class="fn">super</span>().<span class="fn">fahren</span>(<span class="var">km</span>) <span class="cm"># Erhöht den Kilometerstand in der Basisklasse!</span>
|
||||||
|
<span class="kw">return</span> <span class="kw">True</span>
|
||||||
|
<span class="kw">return</span> <span class="kw">False</span></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 6. Testen & Navigation -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>6. 🧪 Lösung überprüfen</h2>
|
||||||
|
<p>Führe im Terminal im Ordner <code>10_vererbung_und_super</code> folgenden Befehl aus:</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Terminal</span>
|
||||||
|
</div>
|
||||||
|
<pre><code>python3 test_aufgabe.py</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-nav">
|
||||||
|
<a href="../09_eigene_unit_tests_schreiben/index.html" class="btn btn-secondary">← Zurück zu Kapitel 09</a>
|
||||||
|
<a href="../11_polymorphie_und_interfaces/index.html" class="btn">Weiter zu Kapitel 11 (Polymorphie) →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
168
10_vererbung_und_super/musterloesung.py
Normal file
168
10_vererbung_und_super/musterloesung.py
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
"""
|
||||||
|
Kapitel 10: Vererbung & super() – Musterlösung 🧬🚗
|
||||||
|
===================================================
|
||||||
|
Schulabgleich: 25.0 Vererbung
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Fahrzeug:
|
||||||
|
"""Basisklasse für alle Fahrzeuge."""
|
||||||
|
|
||||||
|
def __init__(self, marke: str, modell: str, baujahr: int, grundpreis: float):
|
||||||
|
self.marke: str = marke
|
||||||
|
self.modell: str = modell
|
||||||
|
self.baujahr: int = int(baujahr)
|
||||||
|
self.grundpreis: float = float(grundpreis)
|
||||||
|
self.kilometerstand: float = 0.0
|
||||||
|
|
||||||
|
def fahren(self, km: float) -> None:
|
||||||
|
if km > 0:
|
||||||
|
self.kilometerstand += float(km)
|
||||||
|
|
||||||
|
def berechne_restwert(self, aktuelles_jahr: int) -> float:
|
||||||
|
alter = max(0, aktuelles_jahr - self.baujahr)
|
||||||
|
wertverlust = self.grundpreis * 0.05 * alter
|
||||||
|
restwert = self.grundpreis - wertverlust
|
||||||
|
return max(self.grundpreis * 0.10, restwert)
|
||||||
|
|
||||||
|
def info(self) -> str:
|
||||||
|
return f"{self.marke} {self.modell} ({self.baujahr}) - {self.kilometerstand:.1f} km"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.info()
|
||||||
|
|
||||||
|
|
||||||
|
class Auto(Fahrzeug):
|
||||||
|
"""Abgeleitete Klasse für PKWs (erbt von Fahrzeug)."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
marke: str,
|
||||||
|
modell: str,
|
||||||
|
baujahr: int,
|
||||||
|
grundpreis: float,
|
||||||
|
sitzplaetze: int = 5,
|
||||||
|
anzahl_tueren: int = 5,
|
||||||
|
):
|
||||||
|
super().__init__(marke, modell, baujahr, grundpreis)
|
||||||
|
self.sitzplaetze: int = int(sitzplaetze)
|
||||||
|
self.anzahl_tueren: int = int(anzahl_tueren)
|
||||||
|
|
||||||
|
def hupen(self) -> str:
|
||||||
|
return f"Hup hup! Platz da für den {self.marke} {self.modell}!"
|
||||||
|
|
||||||
|
def info(self) -> str:
|
||||||
|
return f"{super().info()} | {self.sitzplaetze} Sitze, {self.anzahl_tueren} Türen"
|
||||||
|
|
||||||
|
|
||||||
|
class ElektroAuto(Auto):
|
||||||
|
"""Abgeleitete Klasse für Elektroautos (erbt von Auto)."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
marke: str,
|
||||||
|
modell: str,
|
||||||
|
baujahr: int,
|
||||||
|
grundpreis: float,
|
||||||
|
batterie_kapazitaet_kwh: float,
|
||||||
|
sitzplaetze: int = 5,
|
||||||
|
anzahl_tueren: int = 5,
|
||||||
|
verbrauch_pro_100km: float = 18.0,
|
||||||
|
):
|
||||||
|
super().__init__(marke, modell, baujahr, grundpreis, sitzplaetze, anzahl_tueren)
|
||||||
|
self.batterie_kapazitaet_kwh: float = float(batterie_kapazitaet_kwh)
|
||||||
|
self.batterie_ladestand_kwh: float = float(batterie_kapazitaet_kwh)
|
||||||
|
self.verbrauch_pro_100km: float = float(verbrauch_pro_100km)
|
||||||
|
|
||||||
|
def reichweite(self) -> float:
|
||||||
|
if self.verbrauch_pro_100km <= 0:
|
||||||
|
return 0.0
|
||||||
|
return (self.batterie_ladestand_kwh / self.verbrauch_pro_100km) * 100.0
|
||||||
|
|
||||||
|
def fahren(self, km: float) -> bool:
|
||||||
|
if km <= 0:
|
||||||
|
return False
|
||||||
|
benoetigt = (km / 100.0) * self.verbrauch_pro_100km
|
||||||
|
if self.batterie_ladestand_kwh >= benoetigt:
|
||||||
|
self.batterie_ladestand_kwh -= benoetigt
|
||||||
|
super().fahren(km)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def aufladen(self, kwh: float) -> float:
|
||||||
|
if kwh <= 0:
|
||||||
|
return 0.0
|
||||||
|
freier_platz = self.batterie_kapazitaet_kwh - self.batterie_ladestand_kwh
|
||||||
|
tatsaechlich = min(freier_platz, float(kwh))
|
||||||
|
self.batterie_ladestand_kwh += tatsaechlich
|
||||||
|
return tatsaechlich
|
||||||
|
|
||||||
|
def info(self) -> str:
|
||||||
|
return (
|
||||||
|
f"{super().info()} | Akku: {self.batterie_ladestand_kwh:.1f}/"
|
||||||
|
f"{self.batterie_kapazitaet_kwh:.1f} kWh ({self.reichweite():.1f} km Reichweite)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Lkw(Fahrzeug):
|
||||||
|
"""Abgeleitete Klasse für Lastkraftwagen (erbt von Fahrzeug)."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
marke: str,
|
||||||
|
modell: str,
|
||||||
|
baujahr: int,
|
||||||
|
grundpreis: float,
|
||||||
|
max_zuladung_kg: float,
|
||||||
|
):
|
||||||
|
super().__init__(marke, modell, baujahr, grundpreis)
|
||||||
|
self.max_zuladung_kg: float = float(max_zuladung_kg)
|
||||||
|
self.aktuelle_ladung_kg: float = 0.0
|
||||||
|
|
||||||
|
def beladen(self, kg: float) -> bool:
|
||||||
|
if kg > 0 and (self.aktuelle_ladung_kg + kg <= self.max_zuladung_kg):
|
||||||
|
self.aktuelle_ladung_kg += float(kg)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def entladen(self, kg: float) -> float:
|
||||||
|
if kg <= 0:
|
||||||
|
return 0.0
|
||||||
|
tatsaechlich = min(self.aktuelle_ladung_kg, float(kg))
|
||||||
|
self.aktuelle_ladung_kg -= tatsaechlich
|
||||||
|
return tatsaechlich
|
||||||
|
|
||||||
|
def info(self) -> str:
|
||||||
|
return f"{super().info()} | Ladung: {self.aktuelle_ladung_kg:.1f}/{self.max_zuladung_kg:.1f} kg"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("--- 🚗 Auto testen ---")
|
||||||
|
golf = Auto("VW", "Golf 8", 2021, 28000.0, sitzplaetze=5, anzahl_tueren=5)
|
||||||
|
golf.fahren(150.0)
|
||||||
|
print(golf.info())
|
||||||
|
print(golf.hupen())
|
||||||
|
print(f"Restwert im Jahr 2026: {golf.berechne_restwert(2026):.2f} €")
|
||||||
|
|
||||||
|
print("\n--- ⚡ ElektroAuto testen ---")
|
||||||
|
tesla = ElektroAuto("Tesla", "Model Y", 2023, 45000.0, batterie_kapazitaet_kwh=75.0, verbrauch_pro_100km=16.5)
|
||||||
|
print(tesla.info())
|
||||||
|
print(f"Start-Reichweite: {tesla.reichweite():.1f} km")
|
||||||
|
|
||||||
|
erfolg = tesla.fahren(200.0)
|
||||||
|
print(f"Fahrt 200 km erfolgreich? {erfolg}")
|
||||||
|
print(tesla.info())
|
||||||
|
|
||||||
|
geladen = tesla.aufladen(50.0)
|
||||||
|
print(f"Aufgeladen: {geladen:.1f} kWh")
|
||||||
|
print(tesla.info())
|
||||||
|
|
||||||
|
print("\n--- 🚛 LKW testen ---")
|
||||||
|
lkw = Lkw("Mercedes-Benz", "Actros", 2020, 110000.0, max_zuladung_kg=24000.0)
|
||||||
|
lkw.beladen(15000.0)
|
||||||
|
lkw.fahren(400.0)
|
||||||
|
print(lkw.info())
|
||||||
|
entladen = lkw.entladen(5000.0)
|
||||||
|
print(f"Entladen: {entladen:.1f} kg -> Neue Ladung: {lkw.aktuelle_ladung_kg:.1f} kg")
|
||||||
193
10_vererbung_und_super/test_aufgabe.py
Normal file
193
10_vererbung_und_super/test_aufgabe.py
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Sicherstellen, dass das Kapitel-Verzeichnis im Suchpfad 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 TestKapitel10(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.Fahrzeug = getattr(aufgabe, "Fahrzeug", None)
|
||||||
|
self.Auto = getattr(aufgabe, "Auto", None)
|
||||||
|
self.ElektroAuto = getattr(aufgabe, "ElektroAuto", None)
|
||||||
|
self.Lkw = getattr(aufgabe, "Lkw", None)
|
||||||
|
|
||||||
|
self.assertIsNotNone(self.Fahrzeug, "Klasse 'Fahrzeug' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.Auto, "Klasse 'Auto' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.ElektroAuto, "Klasse 'ElektroAuto' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.Lkw, "Klasse 'Lkw' nicht gefunden!")
|
||||||
|
|
||||||
|
def test_01_fahrzeug_init_und_fahren(self):
|
||||||
|
"""Prüft Basisklasse Fahrzeug: Initialisierung und Fahren."""
|
||||||
|
f = self.Fahrzeug("VW", "Golf", 2020, 25000.0)
|
||||||
|
self.assertEqual(f.marke, "VW")
|
||||||
|
self.assertEqual(f.modell, "Golf")
|
||||||
|
self.assertEqual(f.baujahr, 2020)
|
||||||
|
self.assertEqual(f.grundpreis, 25000.0)
|
||||||
|
self.assertEqual(f.kilometerstand, 0.0)
|
||||||
|
|
||||||
|
f.fahren(120.5)
|
||||||
|
self.assertAlmostEqual(f.kilometerstand, 120.5)
|
||||||
|
|
||||||
|
# Negative km dürfen den Kilometerstand nicht verändern
|
||||||
|
f.fahren(-50.0)
|
||||||
|
self.assertAlmostEqual(f.kilometerstand, 120.5)
|
||||||
|
|
||||||
|
def test_02_fahrzeug_berechne_restwert(self):
|
||||||
|
"""Prüft Restwertberechnung mit 5% jährlichem Wertverlust und 10% Mindestwert."""
|
||||||
|
f = self.Fahrzeug("Audi", "A4", 2020, 40000.0)
|
||||||
|
|
||||||
|
# Gleiches Jahr (Alter 0)
|
||||||
|
self.assertAlmostEqual(f.berechne_restwert(2020), 40000.0)
|
||||||
|
|
||||||
|
# Nach 4 Jahren (20% Verlust -> 80% von 40000 = 32000)
|
||||||
|
self.assertAlmostEqual(f.berechne_restwert(2024), 32000.0)
|
||||||
|
|
||||||
|
# Nach 30 Jahren (Mehr als 90% Verlust -> Mindestwert 10% = 4000)
|
||||||
|
self.assertAlmostEqual(f.berechne_restwert(2050), 4000.0)
|
||||||
|
|
||||||
|
def test_03_fahrzeug_info_und_str(self):
|
||||||
|
"""Prüft info() und __str__() der Basisklasse Fahrzeug."""
|
||||||
|
f = self.Fahrzeug("BMW", "320d", 2020, 35000.0)
|
||||||
|
self.assertEqual(f.info(), "BMW 320d (2020) - 0.0 km")
|
||||||
|
|
||||||
|
f.fahren(5000.0)
|
||||||
|
self.assertEqual(str(f), "BMW 320d (2020) - 5000.0 km")
|
||||||
|
|
||||||
|
def test_04_auto_vererbung_und_super(self):
|
||||||
|
"""Prüft Vererbung von Fahrzeug an Auto und Aufruf von super().__init__()."""
|
||||||
|
self.assertTrue(issubclass(self.Auto, self.Fahrzeug), "Auto muss von Fahrzeug erben!")
|
||||||
|
|
||||||
|
a = self.Auto("VW", "Polo", 2022, 20000.0, sitzplaetze=5, anzahl_tueren=3)
|
||||||
|
self.assertIsInstance(a, self.Fahrzeug)
|
||||||
|
self.assertIsInstance(a, self.Auto)
|
||||||
|
|
||||||
|
# Geerbte Attribute prüfen
|
||||||
|
self.assertEqual(a.marke, "VW")
|
||||||
|
self.assertEqual(a.modell, "Polo")
|
||||||
|
self.assertEqual(a.baujahr, 2022)
|
||||||
|
self.assertEqual(a.grundpreis, 20000.0)
|
||||||
|
self.assertEqual(a.kilometerstand, 0.0)
|
||||||
|
|
||||||
|
# Neue Attribute prüfen
|
||||||
|
self.assertEqual(a.sitzplaetze, 5)
|
||||||
|
self.assertEqual(a.anzahl_tueren, 3)
|
||||||
|
|
||||||
|
def test_05_auto_methoden_und_hupen(self):
|
||||||
|
"""Prüft hupen() und die erweiterte info()-Methode von Auto."""
|
||||||
|
a = self.Auto("Porsche", "911", 2023, 120000.0, sitzplaetze=2, anzahl_tueren=2)
|
||||||
|
self.assertEqual(a.hupen(), "Hup hup! Platz da für den Porsche 911!")
|
||||||
|
|
||||||
|
a.fahren(250.0)
|
||||||
|
self.assertEqual(a.info(), "Porsche 911 (2023) - 250.0 km | 2 Sitze, 2 Türen")
|
||||||
|
|
||||||
|
def test_06_elektroauto_mehrstufige_vererbung(self):
|
||||||
|
"""Prüft mehrstufige Vererbung: ElektroAuto -> Auto -> Fahrzeug."""
|
||||||
|
self.assertTrue(issubclass(self.ElektroAuto, self.Auto), "ElektroAuto muss von Auto erben!")
|
||||||
|
self.assertTrue(issubclass(self.ElektroAuto, self.Fahrzeug), "ElektroAuto muss auch von Fahrzeug erben!")
|
||||||
|
|
||||||
|
e = self.ElektroAuto(
|
||||||
|
"Tesla", "Model 3", 2023, 42000.0,
|
||||||
|
batterie_kapazitaet_kwh=60.0,
|
||||||
|
sitzplaetze=5,
|
||||||
|
anzahl_tueren=5,
|
||||||
|
verbrauch_pro_100km=15.0
|
||||||
|
)
|
||||||
|
self.assertIsInstance(e, self.Fahrzeug)
|
||||||
|
self.assertIsInstance(e, self.Auto)
|
||||||
|
self.assertIsInstance(e, self.ElektroAuto)
|
||||||
|
|
||||||
|
self.assertEqual(e.batterie_kapazitaet_kwh, 60.0)
|
||||||
|
self.assertEqual(e.batterie_ladestand_kwh, 60.0)
|
||||||
|
self.assertEqual(e.verbrauch_pro_100km, 15.0)
|
||||||
|
|
||||||
|
def test_07_elektroauto_reichweite_und_fahren(self):
|
||||||
|
"""Prüft Reichweitenberechnung, Stromverbrauch und Fahren."""
|
||||||
|
e = self.ElektroAuto(
|
||||||
|
"Tesla", "Model 3", 2023, 42000.0,
|
||||||
|
batterie_kapazitaet_kwh=60.0,
|
||||||
|
verbrauch_pro_100km=15.0
|
||||||
|
)
|
||||||
|
# Voller Akku: (60 / 15) * 100 = 400 km
|
||||||
|
self.assertAlmostEqual(e.reichweite(), 400.0)
|
||||||
|
|
||||||
|
# 100 km fahren -> verbraucht 15 kWh -> 45 kWh übrig
|
||||||
|
erfolg = e.fahren(100.0)
|
||||||
|
self.assertTrue(erfolg, "Fahrt von 100 km sollte bei 400 km Reichweite erfolgreich sein!")
|
||||||
|
self.assertAlmostEqual(e.kilometerstand, 100.0)
|
||||||
|
self.assertAlmostEqual(e.batterie_ladestand_kwh, 45.0)
|
||||||
|
self.assertAlmostEqual(e.reichweite(), 300.0)
|
||||||
|
|
||||||
|
# Versuch, 400 km zu fahren (braucht 60 kWh, aber nur 45 kWh vorhanden)
|
||||||
|
erfolg_zu_weit = e.fahren(400.0)
|
||||||
|
self.assertFalse(erfolg_zu_weit, "Fahrt sollte fehlschlagen, wenn der Akku nicht reicht!")
|
||||||
|
# Werte dürfen sich nicht verändert haben
|
||||||
|
self.assertAlmostEqual(e.kilometerstand, 100.0)
|
||||||
|
self.assertAlmostEqual(e.batterie_ladestand_kwh, 45.0)
|
||||||
|
|
||||||
|
def test_08_elektroauto_aufladen_und_info(self):
|
||||||
|
"""Prüft Akku aufladen und die erweiterte info()-Methode."""
|
||||||
|
e = self.ElektroAuto(
|
||||||
|
"Tesla", "Model 3", 2023, 42000.0,
|
||||||
|
batterie_kapazitaet_kwh=60.0,
|
||||||
|
verbrauch_pro_100km=15.0
|
||||||
|
)
|
||||||
|
e.fahren(200.0) # Verbraucht 30 kWh -> 30 kWh übrig
|
||||||
|
|
||||||
|
# 20 kWh laden
|
||||||
|
geladen = e.aufladen(20.0)
|
||||||
|
self.assertAlmostEqual(geladen, 20.0)
|
||||||
|
self.assertAlmostEqual(e.batterie_ladestand_kwh, 50.0)
|
||||||
|
|
||||||
|
# Mehr laden als reinpasst (nur noch 10 kWh frei)
|
||||||
|
geladen_uebervoll = e.aufladen(50.0)
|
||||||
|
self.assertAlmostEqual(geladen_uebervoll, 10.0)
|
||||||
|
self.assertAlmostEqual(e.batterie_ladestand_kwh, 60.0)
|
||||||
|
|
||||||
|
info_text = e.info()
|
||||||
|
self.assertIn("Tesla Model 3", info_text)
|
||||||
|
self.assertIn("60.0/60.0 kWh", info_text)
|
||||||
|
|
||||||
|
def test_09_lkw_vererbung_und_ladung(self):
|
||||||
|
"""Prüft Lkw: direkte Vererbung von Fahrzeug, Beladen und Entladen."""
|
||||||
|
self.assertTrue(issubclass(self.Lkw, self.Fahrzeug), "Lkw muss von Fahrzeug erben!")
|
||||||
|
self.assertFalse(issubclass(self.Lkw, self.Auto), "Lkw sollte NICHT von Auto erben!")
|
||||||
|
|
||||||
|
lkw = self.Lkw("MAN", "TGX", 2021, 130000.0, max_zuladung_kg=18000.0)
|
||||||
|
self.assertIsInstance(lkw, self.Fahrzeug)
|
||||||
|
self.assertEqual(lkw.aktuelle_ladung_kg, 0.0)
|
||||||
|
self.assertEqual(lkw.max_zuladung_kg, 18000.0)
|
||||||
|
|
||||||
|
# Erfolgreich beladen
|
||||||
|
self.assertTrue(lkw.beladen(10000.0))
|
||||||
|
self.assertAlmostEqual(lkw.aktuelle_ladung_kg, 10000.0)
|
||||||
|
|
||||||
|
# Überladung ablehnen (10000 + 9000 = 19000 > 18000)
|
||||||
|
self.assertFalse(lkw.beladen(9000.0))
|
||||||
|
self.assertAlmostEqual(lkw.aktuelle_ladung_kg, 10000.0)
|
||||||
|
|
||||||
|
# Teilweise entladen
|
||||||
|
entladen = lkw.entladen(4000.0)
|
||||||
|
self.assertAlmostEqual(entladen, 4000.0)
|
||||||
|
self.assertAlmostEqual(lkw.aktuelle_ladung_kg, 6000.0)
|
||||||
|
|
||||||
|
# Mehr entladen als vorhanden (nur 6000 kg übrig)
|
||||||
|
entladen_rest = lkw.entladen(10000.0)
|
||||||
|
self.assertAlmostEqual(entladen_rest, 6000.0)
|
||||||
|
self.assertAlmostEqual(lkw.aktuelle_ladung_kg, 0.0)
|
||||||
|
|
||||||
|
# Fahren erben
|
||||||
|
lkw.fahren(300.0)
|
||||||
|
self.assertAlmostEqual(lkw.kilometerstand, 300.0)
|
||||||
|
self.assertIn("Ladung: 0.0/18000.0 kg", lkw.info())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
87
11_polymorphie_und_interfaces/README.md
Normal file
87
11_polymorphie_und_interfaces/README.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Kapitel 11: Polymorphie & Interfaces 🎭📐
|
||||||
|
|
||||||
|
Willkommen zu Kapitel 11! In diesem Kapitel lernst du das Konzept der **Polymorphie (Vielgestaltigkeit)** und **Interfaces (abstrakte Basisklassen)** in Python kennen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Was du lernst
|
||||||
|
|
||||||
|
1. **Was bedeutet Polymorphie?**
|
||||||
|
- Aus dem Griechischen: *poly* = viel, *morph* = Gestalt → "Vielgestaltigkeit".
|
||||||
|
- Verschiedene Klassen stellen Methoden mit demselben Namen und denselben Parametern bereit (`flaeche()`, `umfang()`, `info()`), berechnen ihr Ergebnis aber völlig unterschiedlich!
|
||||||
|
2. **Didaktische Analogie: Die Universalfernbedienung & Mediaplayer:**
|
||||||
|
- Wenn du auf der Fernbedienung auf "Power" drückst, schaltet sich der Fernseher, die Soundbar oder die Spielkonsole ein – jedes Gerät auf seine Art.
|
||||||
|
- Ein Mediaplayer drückt einfach auf `medium.abspielen()` – egal ob MP3, MP4, FLAC oder Stream.
|
||||||
|
3. **Abstrakte Basisklassen & Interfaces (`abc.ABC`):**
|
||||||
|
- Mit `from abc import ABC, abstractmethod` zwingst du abgeleitete Klassen dazu, bestimmte Methoden zu implementieren.
|
||||||
|
- Wird eine abstrakte Methode nicht implementiert, verhindert Python das Erstellen von Objekten mit einem `TypeError`.
|
||||||
|
4. **Duck Typing in Python:**
|
||||||
|
- *"If it walks like a duck and quacks like a duck, it is a duck."*
|
||||||
|
- In Python zählt das Verhalten eines Objekts: Solange ein Objekt eine Methode `flaeche()` besitzt, kann es in polymorphen Berechnungen verwendet werden.
|
||||||
|
5. **Polymorphe Sammlungen (Manager-Klassen):**
|
||||||
|
- Eine Liste mit vielen unterschiedlichen Objekten (`[Rechteck, Kreis, Dreieck]`) durchlaufen und einheitlich `form.flaeche()` aufrufen – ohne eine einzige `if isinstance(...)` Abfrage!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 UML-Klassendiagramm
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────┐
|
||||||
|
│ «abstract» Form │
|
||||||
|
├───────────────────────────────────────────────┤
|
||||||
|
│ - farbe: str │
|
||||||
|
├───────────────────────────────────────────────┤
|
||||||
|
│ + __init__(farbe: str = "schwarz") │
|
||||||
|
│ + {abstract} flaeche() -> float │
|
||||||
|
│ + {abstract} umfang() -> float │
|
||||||
|
│ + info() -> str │
|
||||||
|
└───────────────────────▲───────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────────┼───────────────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌───────────┴───────────┐ ┌─────────┴─────────┐ ┌───────────┴───────────┐
|
||||||
|
│ Rechteck │ │ Kreis │ │ Dreieck │
|
||||||
|
├───────────────────────┤ ├───────────────────┤ ├───────────────────────┤
|
||||||
|
│ - breite: float │ │ - radius: float │ │ - seite_a: float │
|
||||||
|
│ - hoehe: float │ ├───────────────────┤ │ - seite_b: float │
|
||||||
|
├───────────────────────┤ │ + flaeche() │ ├───────────────────────┤
|
||||||
|
│ + flaeche() │ │ + umfang() │ │ + hypotenuse() │
|
||||||
|
│ + umfang() │ │ + info() │ │ + flaeche() │
|
||||||
|
│ + info() │ └───────────────────┘ │ + umfang() │
|
||||||
|
└───────────────────────┘ │ + info() │
|
||||||
|
└───────────────────────┘
|
||||||
|
|
||||||
|
┌───────────────────────────────────────────────────────────┐
|
||||||
|
│ Zeichenflaeche │
|
||||||
|
├───────────────────────────────────────────────────────────┤
|
||||||
|
│ - name: str │
|
||||||
|
│ - formen: list[Form] │
|
||||||
|
├───────────────────────────────────────────────────────────┤
|
||||||
|
│ + hinzufuegen(form: Form) -> None │
|
||||||
|
│ + anzahl_formen() -> int │
|
||||||
|
│ + gesamte_flaeche() -> float <-- ruft form.flaeche() auf│
|
||||||
|
│ + gesamter_umfang() -> float <-- ruft form.umfang() auf │
|
||||||
|
│ + formen_nach_farbe(farbe) -> list[Form] │
|
||||||
|
│ + groesste_form() -> Form | None │
|
||||||
|
│ + report() -> list[str] │
|
||||||
|
└───────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Die Dateien in diesem Ordner
|
||||||
|
|
||||||
|
- **`index.html`**: Die interaktive Lernseite mit visuellen Erklärungen, Analogien, UML-Diagrammen und Lösungstipps.
|
||||||
|
- **`aufgabe.py`**: Dein Arbeitsblatt mit den Klassen `Form`, `Rechteck`, `Kreis`, `Dreieck` und `Zeichenflaeche`.
|
||||||
|
- **`test_aufgabe.py`**: Automatische Tests (`python3 test_aufgabe.py`).
|
||||||
|
- **`musterloesung.py`**: Vollständig ausprogrammierte Beispiellösung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bearbeite aufgabe.py
|
||||||
|
# 2. Teste deine Lösung:
|
||||||
|
python3 test_aufgabe.py
|
||||||
|
```
|
||||||
309
11_polymorphie_und_interfaces/aufgabe.py
Normal file
309
11_polymorphie_und_interfaces/aufgabe.py
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
"""
|
||||||
|
Kapitel 11: Polymorphie & Interfaces 🎭📐
|
||||||
|
=========================================
|
||||||
|
Schulabgleich: 25.1 Polymorphie
|
||||||
|
|
||||||
|
Aufgabe:
|
||||||
|
Erstelle ein geometrisches Grafik- & Zeichenflächen-System mit Polymorphie.
|
||||||
|
Alle Formen (Rechteck, Kreis, Dreieck) besitzen dieselben Methoden (flaeche, umfang, info),
|
||||||
|
berechnen ihre Ergebnisse jedoch auf ihre jeweils eigene Weise.
|
||||||
|
Die Klasse 'Zeichenflaeche' kann beliebige Formen verwalten, ohne wissen zu müssen,
|
||||||
|
um welche konkrete Form es sich handelt!
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Form(ABC):
|
||||||
|
"""
|
||||||
|
Abstrakte Basisklasse (Interface) für alle geometrischen Formen.
|
||||||
|
|
||||||
|
Attribute:
|
||||||
|
farbe (str): Die Zeichenfarbe der Form (z.B. "rot", "blau", "gruen")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 1: Konstruktor __init__ der Basisklasse Form
|
||||||
|
# Parameter: farbe (str = "schwarz")
|
||||||
|
# Setze self.farbe = farbe
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(self, farbe: str = "schwarz"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 2: Abstrakte Methode flaeche(self) -> float
|
||||||
|
# Deklariere die Methode mit @abstractmethod.
|
||||||
|
# Da es eine abstrakte Methode ist, enthält der Rumpf nur 'pass'.
|
||||||
|
# ==========================================================================
|
||||||
|
@abstractmethod
|
||||||
|
def flaeche(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 3: Abstrakte Methode umfang(self) -> float
|
||||||
|
# Deklariere die Methode mit @abstractmethod.
|
||||||
|
# Rumpf: 'pass'
|
||||||
|
# ==========================================================================
|
||||||
|
@abstractmethod
|
||||||
|
def umfang(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 4: Methode info(self) -> str
|
||||||
|
# Gibt einen allgemeinen Infostring zurück:
|
||||||
|
# Format: "{Klassenname} ({farbe}) - Fläche: {flaeche():.2f}, Umfang: {umfang():.2f}"
|
||||||
|
# Tipp: self.__class__.__name__ liefert den Namen der jeweiligen Kindklasse!
|
||||||
|
# Beispiel: "Form (schwarz) - Fläche: 0.00, Umfang: 0.00"
|
||||||
|
# ==========================================================================
|
||||||
|
def info(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.info()
|
||||||
|
|
||||||
|
|
||||||
|
class Rechteck(Form):
|
||||||
|
"""
|
||||||
|
Rechteck-Form (erbt von Form).
|
||||||
|
|
||||||
|
Attribute:
|
||||||
|
breite (float): Breite des Rechtecks
|
||||||
|
hoehe (float): Höhe des Rechtecks
|
||||||
|
farbe (str): Farbe (Standardwert: "rot")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 5: Konstruktor __init__ von Rechteck
|
||||||
|
# Parameter: breite (float), hoehe (float), farbe (str = "rot")
|
||||||
|
# 1. Rufe super().__init__(farbe) auf.
|
||||||
|
# 2. Setze self.breite = float(breite)
|
||||||
|
# 3. Setze self.hoehe = float(hoehe)
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(self, breite: float, hoehe: float, farbe: str = "rot"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 6: Methode flaeche(self) -> float (Überschreiben)
|
||||||
|
# Formel: breite * hoehe
|
||||||
|
# ==========================================================================
|
||||||
|
def flaeche(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 7: Methode umfang(self) -> float (Überschreiben)
|
||||||
|
# Formel: 2 * (breite + hoehe)
|
||||||
|
# ==========================================================================
|
||||||
|
def umfang(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 8: Methode info(self) -> str (Überschreiben)
|
||||||
|
# Format: "Rechteck ({farbe}, {breite:.1f}x{hoehe:.1f}) - Fläche: {flaeche():.2f}, Umfang: {umfang():.2f}"
|
||||||
|
# Beispiel: "Rechteck (rot, 4.0x5.0) - Fläche: 20.00, Umfang: 18.00"
|
||||||
|
# ==========================================================================
|
||||||
|
def info(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Kreis(Form):
|
||||||
|
"""
|
||||||
|
Kreis-Form (erbt von Form).
|
||||||
|
|
||||||
|
Attribute:
|
||||||
|
radius (float): Radius des Kreises
|
||||||
|
farbe (str): Farbe (Standardwert: "blau")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 9: Konstruktor __init__ von Kreis
|
||||||
|
# Parameter: radius (float), farbe (str = "blau")
|
||||||
|
# 1. Rufe super().__init__(farbe) auf.
|
||||||
|
# 2. Setze self.radius = float(radius)
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(self, radius: float, farbe: str = "blau"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 10: Methode flaeche(self) -> float (Überschreiben)
|
||||||
|
# Formel: math.pi * (radius ** 2)
|
||||||
|
# ==========================================================================
|
||||||
|
def flaeche(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 11: Methode umfang(self) -> float (Überschreiben)
|
||||||
|
# Formel: 2 * math.pi * radius
|
||||||
|
# ==========================================================================
|
||||||
|
def umfang(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 12: Methode info(self) -> str (Überschreiben)
|
||||||
|
# Format: "Kreis ({farbe}, r={radius:.1f}) - Fläche: {flaeche():.2f}, Umfang: {umfang():.2f}"
|
||||||
|
# Beispiel: "Kreis (blau, r=3.0) - Fläche: 28.27, Umfang: 18.85"
|
||||||
|
# ==========================================================================
|
||||||
|
def info(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Dreieck(Form):
|
||||||
|
"""
|
||||||
|
Rechtwinkliges Dreieck (erbt von Form) mit Katheten a und b.
|
||||||
|
|
||||||
|
Attribute:
|
||||||
|
seite_a (float): Länge der ersten Kathete (Grundseite)
|
||||||
|
seite_b (float): Länge der zweiten Kathete (Höhe)
|
||||||
|
farbe (str): Farbe (Standardwert: "gruen")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 13: Konstruktor __init__ von Dreieck
|
||||||
|
# Parameter: seite_a (float), seite_b (float), farbe (str = "gruen")
|
||||||
|
# 1. Rufe super().__init__(farbe) auf.
|
||||||
|
# 2. Setze self.seite_a = float(seite_a)
|
||||||
|
# 3. Setze self.seite_b = float(seite_b)
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(self, seite_a: float, seite_b: float, farbe: str = "gruen"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 14: Methode hypotenuse(self) -> float
|
||||||
|
# Berechnet die dritte Seite c mit dem Satz des Pythagoras:
|
||||||
|
# Formel: math.sqrt(seite_a**2 + seite_b**2)
|
||||||
|
# ==========================================================================
|
||||||
|
def hypotenuse(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 15: Methode flaeche(self) -> float (Überschreiben)
|
||||||
|
# Formel: 0.5 * seite_a * seite_b
|
||||||
|
# ==========================================================================
|
||||||
|
def flaeche(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 16: Methode umfang(self) -> float (Überschreiben)
|
||||||
|
# Formel: seite_a + seite_b + self.hypotenuse()
|
||||||
|
# ==========================================================================
|
||||||
|
def umfang(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 17: Methode info(self) -> str (Überschreiben)
|
||||||
|
# Format: "Dreieck ({farbe}, a={seite_a:.1f}, b={seite_b:.1f}, c={hypotenuse():.1f}) - Fläche: {flaeche():.2f}, Umfang: {umfang():.2f}"
|
||||||
|
# Beispiel: "Dreieck (gruen, a=3.0, b=4.0, c=5.0) - Fläche: 6.00, Umfang: 12.00"
|
||||||
|
# ==========================================================================
|
||||||
|
def info(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Zeichenflaeche:
|
||||||
|
"""
|
||||||
|
Manager-Klasse zur polymorphen Verwaltung beliebig vieler geometrischer Formen.
|
||||||
|
|
||||||
|
Attribute:
|
||||||
|
name (str): Name der Zeichenfläche
|
||||||
|
formen (list[Form]): Liste aller hinzugefügten Formen
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 18: Konstruktor __init__ von Zeichenflaeche
|
||||||
|
# Parameter: name (str = "Meine Zeichenfläche")
|
||||||
|
# Attribute:
|
||||||
|
# - self.name: str = name
|
||||||
|
# - self.formen: list[Form] = [] (Startwert: leere Liste)
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(self, name: str = "Meine Zeichenfläche"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 19: Methode hinzufuegen(self, form: Form) -> None
|
||||||
|
# Fügt die übergebene Form zur Liste self.formen hinzu.
|
||||||
|
# ==========================================================================
|
||||||
|
def hinzufuegen(self, form: Form) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 20: Methode anzahl_formen(self) -> int
|
||||||
|
# Gibt die Anzahl der aktuell enthaltenen Formen zurück.
|
||||||
|
# ==========================================================================
|
||||||
|
def anzahl_formen(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 21: Methode gesamte_flaeche(self) -> float (Polymorphie in Aktion!)
|
||||||
|
# Berechnet die Summe aller Flächen der enthaltenen Formen.
|
||||||
|
# Nutze eine Schleife oder sum(...), um form.flaeche() für jede Form aufzurufen.
|
||||||
|
# Wenn self.formen leer ist: 0.0 zurückgeben.
|
||||||
|
# ==========================================================================
|
||||||
|
def gesamte_flaeche(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 22: Methode gesamter_umfang(self) -> float (Polymorphie in Aktion!)
|
||||||
|
# Berechnet die Summe aller Umfänge der enthaltenen Formen.
|
||||||
|
# Nutze form.umfang() für jedes Objekt in self.formen.
|
||||||
|
# Wenn self.formen leer ist: 0.0 zurückgeben.
|
||||||
|
# ==========================================================================
|
||||||
|
def gesamter_umfang(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 23: Methode formen_nach_farbe(self, farbe: str) -> list[Form]
|
||||||
|
# Filtert alle Formen heraus, die die angegebene Farbe haben.
|
||||||
|
# WICHTIG: Groß-/Kleinschreibung ignorieren (z.B. mit farbe.lower())!
|
||||||
|
# ==========================================================================
|
||||||
|
def formen_nach_farbe(self, farbe: str) -> List[Form]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 24: Methode groesste_form(self) -> Form | None
|
||||||
|
# Findet die Form mit dem größten Flächeninhalt (form.flaeche()).
|
||||||
|
# Wenn die Liste leer ist: return None
|
||||||
|
# Tipp: Nutze max(self.formen, key=lambda f: f.flaeche())
|
||||||
|
# ==========================================================================
|
||||||
|
def groesste_form(self) -> Optional[Form]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 25: Methode report(self) -> list[str]
|
||||||
|
# Gibt eine Liste der info()-Texte aller enthaltenen Formen zurück.
|
||||||
|
# ==========================================================================
|
||||||
|
def report(self) -> List[str]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Terminal-Test zum Ausprobieren: (python3 aufgabe.py)
|
||||||
|
# ==============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("--- 🎭 Polymorphie Demo: Geometrische Formen ---")
|
||||||
|
|
||||||
|
r = Rechteck(4.0, 5.0, farbe="rot")
|
||||||
|
k = Kreis(3.0, farbe="blau")
|
||||||
|
d = Dreieck(3.0, 4.0, farbe="gruen")
|
||||||
|
|
||||||
|
# Polymorphe Liste durchlaufen:
|
||||||
|
formen_liste: List[Form] = [r, k, d]
|
||||||
|
|
||||||
|
for form in formen_liste:
|
||||||
|
print(form.info())
|
||||||
|
|
||||||
|
print("\n--- 🖼️ Zeichenfläche Manager ---")
|
||||||
|
leinwand = Zeichenflaeche("Mathe-Poster")
|
||||||
|
leinwand.hinzufuegen(r)
|
||||||
|
leinwand.hinzufuegen(k)
|
||||||
|
leinwand.hinzufuegen(d)
|
||||||
|
leinwand.hinzufuegen(Kreis(1.5, farbe="rot"))
|
||||||
|
|
||||||
|
print(f"Anzahl Formen: {leinwand.anzahl_formen()}")
|
||||||
|
print(f"Gesamtfläche: {leinwand.gesamte_flaeche():.2f} cm²")
|
||||||
|
print(f"Gesamtumfang: {leinwand.gesamter_umfang():.2f} cm")
|
||||||
|
|
||||||
|
rote_formen = leinwand.formen_nach_farbe("rot")
|
||||||
|
print(f"Rote Formen ({len(rote_formen)}): {[f.info() for f in rote_formen]}")
|
||||||
|
|
||||||
|
groesste = leinwand.groesste_form()
|
||||||
|
print(f"Größte Form: {groesste.info() if groesste else 'keine'}")
|
||||||
392
11_polymorphie_und_interfaces/index.html
Normal file
392
11_polymorphie_und_interfaces/index.html
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kapitel 11: Polymorphie & Interfaces</title>
|
||||||
|
<link rel="stylesheet" href="../assets/style.css">
|
||||||
|
<style>
|
||||||
|
/* Spezifische Styles für UML-Diagramme */
|
||||||
|
.uml-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 25px 0;
|
||||||
|
}
|
||||||
|
.uml-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 380px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.uml-header {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.uml-section {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
color: #334155;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.uml-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.uml-arrow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.uml-arrow::before {
|
||||||
|
content: '▲';
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.uml-arrow-line {
|
||||||
|
width: 2px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
.uml-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a href="../index.html">🏠 Zur Kursübersicht</a>
|
||||||
|
<span>›</span>
|
||||||
|
<a href="../10_vererbung_und_super/index.html">Kapitel 10 (Vererbung)</a>
|
||||||
|
<span>›</span>
|
||||||
|
<span style="color: white; font-weight: 600;">Kapitel 11</span>
|
||||||
|
</nav>
|
||||||
|
<div class="header-badge">Kapitel 11 • Schulabgleich 25.1</div>
|
||||||
|
<h1>Polymorphie & Interfaces: Viele Gestalten, ein Befehl 🎭📐</h1>
|
||||||
|
<p>Verstehe das Geheimnis der Vielgestaltigkeit: Gleicher Methodenname, unterschiedliches Verhalten und saubere Schnittstellen mit ABC und Duck Typing.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
|
||||||
|
<!-- 1. Was ist Polymorphie? -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>1. Was bedeutet Polymorphie? ("Vielgestaltigkeit")</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Das Wort <strong>Polymorphie</strong> stammt aus dem Griechischen:
|
||||||
|
<br>
|
||||||
|
<code>poly</code> = <em>viel</em> | <code>morphe</code> = <em>Gestalt, Form</em> → <strong>"Vielgestaltigkeit"</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In der Objektorientierten Programmierung bedeutet das:
|
||||||
|
<strong>Verschiedene Klassen besitzen Methoden mit demselben Namen und derselben Signatur</strong>,
|
||||||
|
führen die Aufgabe im Hintergrund jedoch auf ihre ganz eigene Art und Weise aus.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">📺 Die Universalfernbedienung-Analogie:</div>
|
||||||
|
Stelle dir eine Universal-Fernbedienung mit einer roten <strong>"Power"-Taste</strong> vor.
|
||||||
|
<br><br>
|
||||||
|
Wenn du auf "Power" drückst, sendet die Fernbedienung an alle Geräte den Befehl <code>einschalten()</code>:
|
||||||
|
<ul style="margin-left: 20px; margin-top: 8px;">
|
||||||
|
<li>Der <strong>Fernseher</strong> aktiviert sein OLED-Display.</li>
|
||||||
|
<li>Die <strong>Soundbar</strong> startet den Verstärker und verbindet Bluetooth.</li>
|
||||||
|
<li>Die <strong>Spielkonsole</strong> lädt das Betriebssystem und meldet den Controller an.</li>
|
||||||
|
</ul>
|
||||||
|
<br>
|
||||||
|
Die Fernbedienung muss nicht wissen, <em>wie</em> das Einschalten intern funktioniert – sie weiß nur, dass jedes Gerät eine Methode <code>einschalten()</code> besitzt!
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. UML-Klassendiagramm -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>2. 📊 UML-Klassendiagramm: Formen & Zeichenfläche</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In unserem Praxisbeispiel ist <code>Form</code> ein <strong>Interface / eine abstrakte Basisklasse</strong>.
|
||||||
|
<code>Rechteck</code>, <code>Kreis</code> und <code>Dreieck</code> implementieren jeweils ihre eigene <code>flaeche()</code> und <code>umfang()</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="uml-container">
|
||||||
|
<!-- Abstrakte Basisklasse Form -->
|
||||||
|
<div class="uml-card">
|
||||||
|
<div class="uml-header">«abstract» Form</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
- farbe: str = "schwarz"
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
+ __init__(farbe: str)<br>
|
||||||
|
+ <i>{abstract}</i> flaeche() -> float<br>
|
||||||
|
+ <i>{abstract}</i> umfang() -> float<br>
|
||||||
|
+ info() -> str
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uml-arrow">
|
||||||
|
<div class="uml-arrow-line"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uml-row">
|
||||||
|
<!-- Rechteck -->
|
||||||
|
<div class="uml-card" style="max-width: 270px;">
|
||||||
|
<div class="uml-header">Rechteck</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
- breite: float<br>
|
||||||
|
- hoehe: float
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
+ flaeche() -> float<br>
|
||||||
|
+ umfang() -> float<br>
|
||||||
|
+ info() -> str
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kreis -->
|
||||||
|
<div class="uml-card" style="max-width: 270px;">
|
||||||
|
<div class="uml-header">Kreis</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
- radius: float
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
+ flaeche() -> float<br>
|
||||||
|
+ umfang() -> float<br>
|
||||||
|
+ info() -> str
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dreieck -->
|
||||||
|
<div class="uml-card" style="max-width: 270px;">
|
||||||
|
<div class="uml-header">Dreieck</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
- seite_a: float<br>
|
||||||
|
- seite_b: float
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
+ hypotenuse() -> float<br>
|
||||||
|
+ flaeche() -> float<br>
|
||||||
|
+ umfang() -> float<br>
|
||||||
|
+ info() -> str
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zeichenflaeche Manager -->
|
||||||
|
<div class="uml-card" style="max-width: 450px; margin-top: 15px; border-color: var(--secondary);">
|
||||||
|
<div class="uml-header" style="background: var(--secondary);">Zeichenflaeche (Manager)</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
- name: str<br>
|
||||||
|
- formen: list[Form]
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
+ hinzufuegen(form: Form) -> None<br>
|
||||||
|
+ gesamte_flaeche() -> float <i>(ruft form.flaeche() auf)</i><br>
|
||||||
|
+ gesamter_umfang() -> float <i>(ruft form.umfang() auf)</i><br>
|
||||||
|
+ formen_nach_farbe(farbe) -> list[Form]<br>
|
||||||
|
+ groesste_form() -> Form | None<br>
|
||||||
|
+ report() -> list[str]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. Wie funktioniert Polymorphie in Python? -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>3. Wie funktioniert Polymorphie in Code?</h2>
|
||||||
|
|
||||||
|
<h3>Der Clou: Schleifen über gemischte Objekte ohne <code>if/else</code>!</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Ohne Polymorphie müsstest du für jeden Typ eine eigene <code>if/elif</code>-Weiche schreiben:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>❌ Schlechter Stil: Starre Typ-Prüfungen</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="cm"># So bitte NICHT programmieren:</span>
|
||||||
|
<span class="kw">for</span> <span class="var">form</span> <span class="kw">in</span> <span class="var">formen</span>:
|
||||||
|
<span class="kw">if</span> <span class="fn">isinstance</span>(<span class="var">form</span>, <span class="fn">Rechteck</span>):
|
||||||
|
<span class="var">f</span> = <span class="var">form</span>.<span class="attr">breite</span> * <span class="var">form</span>.<span class="attr">hoehe</span>
|
||||||
|
<span class="kw">elif</span> <span class="fn">isinstance</span>(<span class="var">form</span>, <span class="fn">Kreis</span>):
|
||||||
|
<span class="var">f</span> = <span class="num">3.14159</span> * <span class="var">form</span>.<span class="attr">radius</span> ** <span class="num">2</span>
|
||||||
|
<span class="cm"># Bei jeder neuen Form (z.B. Dreieck) muss überall neuer Code eingefügt werden!</span></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Mit <strong>Polymorphie</strong> rufen wir einfach dieselbe Methode auf allen Objekten auf – Python wählt automatisch die richtige Version:
|
||||||
|
</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>✅ Eleganter Stil: Echte Polymorphie</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="var">formen</span> = [
|
||||||
|
<span class="fn">Rechteck</span>(<span class="num">4.0</span>, <span class="num">5.0</span>),
|
||||||
|
<span class="fn">Kreis</span>(<span class="num">3.0</span>),
|
||||||
|
<span class="fn">Dreieck</span>(<span class="num">3.0</span>, <span class="num">4.0</span>)
|
||||||
|
]
|
||||||
|
|
||||||
|
<span class="cm"># Ein einziger, eleganter Methodenaufruf für alle Formen:</span>
|
||||||
|
<span class="kw">for</span> <span class="var">form</span> <span class="kw">in</span> <span class="var">formen</span>:
|
||||||
|
<span class="fn">print</span>(<span class="str">f"{form.info()} -> Fläche: {form.flaeche():.2f}"</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Abstrakte Basisklassen (ABC) & Duck Typing -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>4. Abstrakte Basisklassen (<code>abc.ABC</code>) & Duck Typing</h2>
|
||||||
|
|
||||||
|
<div class="box box-tipp">
|
||||||
|
<div class="box-title">🦆 Was ist Duck Typing?</div>
|
||||||
|
<em>"If it walks like a duck and quacks like a duck, it's a duck."</em>
|
||||||
|
<br><br>
|
||||||
|
Python ist dynamisch typisiert: Solange ein Objekt eine Methode <code>flaeche()</code> besitzt, kann es auf der <code>Zeichenflaeche</code> platziert werden.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Verträge erzwingen mit <code>abc.ABC</code></h3>
|
||||||
|
<p>
|
||||||
|
Um sicherzustellen, dass Entwickler nicht vergessen, wichtige Methoden wie <code>flaeche()</code> zu schreiben, nutzen wir Pythons <code>abc</code>-Modul (<strong>A</strong>bstract <strong>B</strong>ase <strong>C</strong>lasses):
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Python (Abstrakte Methoden definieren)</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">from</span> <span class="var">abc</span> <span class="kw">import</span> <span class="fn">ABC</span>, <span class="fn">abstractmethod</span>
|
||||||
|
|
||||||
|
<span class="kw">class</span> <span class="fn">Form</span>(<span class="fn">ABC</span>):
|
||||||
|
<span class="kw">def</span> <span class="fn">__init__</span>(<span class="var">self</span>, <span class="var">farbe</span>: <span class="fn">str</span> = <span class="str">"schwarz"</span>):
|
||||||
|
<span class="var">self</span>.<span class="attr">farbe</span> = <span class="var">farbe</span>
|
||||||
|
|
||||||
|
<span class="attr">@abstractmethod</span>
|
||||||
|
<span class="kw">def</span> <span class="fn">flaeche</span>(<span class="var">self</span>) -> <span class="fn">float</span>:
|
||||||
|
<span class="kw">pass</span> <span class="cm"># Jede Kindklasse MUSS diese Methode überschreiben!</span></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-warning">
|
||||||
|
<div class="box-title">⚠️ Was passiert, wenn eine Kindklasse die abstrakte Methode vergisst?</div>
|
||||||
|
Wenn eine Kindklasse <code>flaeche()</code> nicht implementiert, verhindert Python das Erstellen von Objekten mit einem klaren Fehler:
|
||||||
|
<br>
|
||||||
|
<code>TypeError: Can't instantiate abstract class MeineForm with abstract method flaeche</code>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. Aufgaben -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>5. 🎯 Deine Aufgaben in <code>aufgabe.py</code></h2>
|
||||||
|
|
||||||
|
<p>Öffne die Datei <code>11_polymorphie_und_interfaces/aufgabe.py</code> und vervollständige das System:</p>
|
||||||
|
|
||||||
|
<ul class="task-list">
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t11_1">
|
||||||
|
<label for="t11_1" class="task-label">
|
||||||
|
<strong>TODO 1–4 (Abstrakte Basisklasse Form):</strong> Konstruktor mit <code>farbe="schwarz"</code>, abstrakte Methoden <code>@abstractmethod flaeche()</code> und <code>@abstractmethod umfang()</code> sowie gemeinsame <code>info()</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t11_2">
|
||||||
|
<label for="t11_2" class="task-label">
|
||||||
|
<strong>TODO 5–8 (Rechteck):</strong> Erbt von <code>Form</code>. Speichert <code>breite</code> und <code>hoehe</code>, implementiert <code>flaeche()</code> (<code>breite * hoehe</code>) und <code>umfang()</code> (<code>2 * (breite + hoehe)</code>).
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t11_3">
|
||||||
|
<label for="t11_3" class="task-label">
|
||||||
|
<strong>TODO 9–12 (Kreis):</strong> Erbt von <code>Form</code>. Speichert <code>radius</code>, implementiert <code>flaeche()</code> (<code>math.pi * radius**2</code>) und <code>umfang()</code> (<code>2 * math.pi * radius</code>).
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t11_4">
|
||||||
|
<label for="t11_4" class="task-label">
|
||||||
|
<strong>TODO 13–17 (Dreieck):</strong> Rechtwinkliges Dreieck mit Katheten <code>seite_a</code> und <code>seite_b</code>. Berechnet <code>hypotenuse()</code> mit Pythagoras (<code>math.sqrt(a**2 + b**2)</code>), <code>flaeche()</code> und <code>umfang()</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t11_5">
|
||||||
|
<label for="t11_5" class="task-label">
|
||||||
|
<strong>TODO 18–25 (Manager-Klasse Zeichenflaeche):</strong> Verwaltet eine polymorphe Liste von Formen. Berechnet <code>gesamte_flaeche()</code>, <code>gesamter_umfang()</code>, filtert mit <code>formen_nach_farbe(farbe)</code> und ermittelt die <code>groesste_form()</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>💡 Lösungshinweise (Aufklappen bei Bedarf)</h3>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 14 (Hypotenuse berechnen)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="kw">def</span> <span class="fn">hypotenuse</span>(<span class="var">self</span>) -> <span class="fn">float</span>:
|
||||||
|
<span class="kw">return</span> <span class="var">math</span>.<span class="fn">sqrt</span>(<span class="var">self</span>.<span class="attr">seite_a</span> ** <span class="num">2</span> + <span class="var">self</span>.<span class="attr">seite_b</span> ** <span class="num">2</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 21 (Polymorphe Gesamtfläche)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="kw">def</span> <span class="fn">gesamte_flaeche</span>(<span class="var">self</span>) -> <span class="fn">float</span>:
|
||||||
|
<span class="kw">return</span> <span class="fn">sum</span>(<span class="var">form</span>.<span class="fn">flaeche</span>() <span class="kw">for</span> <span class="var">form</span> <span class="kw">in</span> <span class="var">self</span>.<span class="attr">formen</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 23 (Farben filtern ohne Groß-/Kleinschreibung)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="kw">def</span> <span class="fn">formen_nach_farbe</span>(<span class="var">self</span>, <span class="var">farbe</span>: <span class="fn">str</span>) -> <span class="fn">list</span>[<span class="fn">Form</span>]:
|
||||||
|
<span class="kw">return</span> [<span class="var">f</span> <span class="kw">for</span> <span class="var">f</span> <span class="kw">in</span> <span class="var">self</span>.<span class="attr">formen</span> <span class="kw">if</span> <span class="var">f</span>.<span class="attr">farbe</span>.<span class="fn">lower</span>() == <span class="var">farbe</span>.<span class="fn">lower</span>()]</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 6. Testen & Navigation -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>6. 🧪 Lösung überprüfen</h2>
|
||||||
|
<p>Führe im Terminal im Ordner <code>11_polymorphie_und_interfaces</code> folgenden Befehl aus:</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Terminal</span>
|
||||||
|
</div>
|
||||||
|
<pre><code>python3 test_aufgabe.py</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-nav">
|
||||||
|
<a href="../10_vererbung_und_super/index.html" class="btn btn-secondary">← Zurück zu Kapitel 10</a>
|
||||||
|
<a href="../12_exceptions_und_fehlerbehandlung/index.html" class="btn">Weiter zu Kapitel 12 (Exceptions) →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
151
11_polymorphie_und_interfaces/musterloesung.py
Normal file
151
11_polymorphie_und_interfaces/musterloesung.py
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
"""
|
||||||
|
Kapitel 11: Polymorphie & Interfaces – Musterlösung 🎭📐
|
||||||
|
=========================================================
|
||||||
|
Schulabgleich: 25.1 Polymorphie
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Form(ABC):
|
||||||
|
"""Abstrakte Basisklasse (Interface) für alle geometrischen Formen."""
|
||||||
|
|
||||||
|
def __init__(self, farbe: str = "schwarz"):
|
||||||
|
self.farbe: str = farbe
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def flaeche(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def umfang(self) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def info(self) -> str:
|
||||||
|
return f"{self.__class__.__name__} ({self.farbe}) - Fläche: {self.flaeche():.2f}, Umfang: {self.umfang():.2f}"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.info()
|
||||||
|
|
||||||
|
|
||||||
|
class Rechteck(Form):
|
||||||
|
"""Rechteck-Form (erbt von Form)."""
|
||||||
|
|
||||||
|
def __init__(self, breite: float, hoehe: float, farbe: str = "rot"):
|
||||||
|
super().__init__(farbe)
|
||||||
|
self.breite: float = float(breite)
|
||||||
|
self.hoehe: float = float(hoehe)
|
||||||
|
|
||||||
|
def flaeche(self) -> float:
|
||||||
|
return self.breite * self.hoehe
|
||||||
|
|
||||||
|
def umfang(self) -> float:
|
||||||
|
return 2.0 * (self.breite + self.hoehe)
|
||||||
|
|
||||||
|
def info(self) -> str:
|
||||||
|
return f"Rechteck ({self.farbe}, {self.breite:.1f}x{self.hoehe:.1f}) - Fläche: {self.flaeche():.2f}, Umfang: {self.umfang():.2f}"
|
||||||
|
|
||||||
|
|
||||||
|
class Kreis(Form):
|
||||||
|
"""Kreis-Form (erbt von Form)."""
|
||||||
|
|
||||||
|
def __init__(self, radius: float, farbe: str = "blau"):
|
||||||
|
super().__init__(farbe)
|
||||||
|
self.radius: float = float(radius)
|
||||||
|
|
||||||
|
def flaeche(self) -> float:
|
||||||
|
return math.pi * (self.radius ** 2)
|
||||||
|
|
||||||
|
def umfang(self) -> float:
|
||||||
|
return 2.0 * math.pi * self.radius
|
||||||
|
|
||||||
|
def info(self) -> str:
|
||||||
|
return f"Kreis ({self.farbe}, r={self.radius:.1f}) - Fläche: {self.flaeche():.2f}, Umfang: {self.umfang():.2f}"
|
||||||
|
|
||||||
|
|
||||||
|
class Dreieck(Form):
|
||||||
|
"""Rechtwinkliges Dreieck (erbt von Form) mit Katheten a und b."""
|
||||||
|
|
||||||
|
def __init__(self, seite_a: float, seite_b: float, farbe: str = "gruen"):
|
||||||
|
super().__init__(farbe)
|
||||||
|
self.seite_a: float = float(seite_a)
|
||||||
|
self.seite_b: float = float(seite_b)
|
||||||
|
|
||||||
|
def hypotenuse(self) -> float:
|
||||||
|
return math.sqrt(self.seite_a ** 2 + self.seite_b ** 2)
|
||||||
|
|
||||||
|
def flaeche(self) -> float:
|
||||||
|
return 0.5 * self.seite_a * self.seite_b
|
||||||
|
|
||||||
|
def umfang(self) -> float:
|
||||||
|
return self.seite_a + self.seite_b + self.hypotenuse()
|
||||||
|
|
||||||
|
def info(self) -> str:
|
||||||
|
return (
|
||||||
|
f"Dreieck ({self.farbe}, a={self.seite_a:.1f}, b={self.seite_b:.1f}, "
|
||||||
|
f"c={self.hypotenuse():.1f}) - Fläche: {self.flaeche():.2f}, Umfang: {self.umfang():.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Zeichenflaeche:
|
||||||
|
"""Manager-Klasse zur polymorphen Verwaltung beliebig vieler geometrischer Formen."""
|
||||||
|
|
||||||
|
def __init__(self, name: str = "Meine Zeichenfläche"):
|
||||||
|
self.name: str = name
|
||||||
|
self.formen: List[Form] = []
|
||||||
|
|
||||||
|
def hinzufuegen(self, form: Form) -> None:
|
||||||
|
self.formen.append(form)
|
||||||
|
|
||||||
|
def anzahl_formen(self) -> int:
|
||||||
|
return len(self.formen)
|
||||||
|
|
||||||
|
def gesamte_flaeche(self) -> float:
|
||||||
|
return sum(f.flaeche() for f in self.formen)
|
||||||
|
|
||||||
|
def gesamter_umfang(self) -> float:
|
||||||
|
return sum(f.umfang() for f in self.formen)
|
||||||
|
|
||||||
|
def formen_nach_farbe(self, farbe: str) -> List[Form]:
|
||||||
|
return [f for f in self.formen if f.farbe.lower() == farbe.lower()]
|
||||||
|
|
||||||
|
def groesste_form(self) -> Optional[Form]:
|
||||||
|
if not self.formen:
|
||||||
|
return None
|
||||||
|
return max(self.formen, key=lambda f: f.flaeche())
|
||||||
|
|
||||||
|
def report(self) -> List[str]:
|
||||||
|
return [f.info() for f in self.formen]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("--- 🎭 Polymorphie Demo: Geometrische Formen ---")
|
||||||
|
|
||||||
|
r = Rechteck(4.0, 5.0, farbe="rot")
|
||||||
|
k = Kreis(3.0, farbe="blau")
|
||||||
|
d = Dreieck(3.0, 4.0, farbe="gruen")
|
||||||
|
|
||||||
|
# Polymorphe Liste durchlaufen:
|
||||||
|
formen_liste: List[Form] = [r, k, d]
|
||||||
|
|
||||||
|
for form in formen_liste:
|
||||||
|
print(form.info())
|
||||||
|
|
||||||
|
print("\n--- 🖼️ Zeichenfläche Manager ---")
|
||||||
|
leinwand = Zeichenflaeche("Mathe-Poster")
|
||||||
|
leinwand.hinzufuegen(r)
|
||||||
|
leinwand.hinzufuegen(k)
|
||||||
|
leinwand.hinzufuegen(d)
|
||||||
|
leinwand.hinzufuegen(Kreis(1.5, farbe="rot"))
|
||||||
|
|
||||||
|
print(f"Anzahl Formen: {leinwand.anzahl_formen()}")
|
||||||
|
print(f"Gesamtfläche: {leinwand.gesamte_flaeche():.2f} cm²")
|
||||||
|
print(f"Gesamtumfang: {leinwand.gesamter_umfang():.2f} cm")
|
||||||
|
|
||||||
|
rote_formen = leinwand.formen_nach_farbe("rot")
|
||||||
|
print(f"Rote Formen ({len(rote_formen)}): {[f.info() for f in rote_formen]}")
|
||||||
|
|
||||||
|
groesste = leinwand.groesste_form()
|
||||||
|
print(f"Größte Form: {groesste.info() if groesste else 'keine'}")
|
||||||
178
11_polymorphie_und_interfaces/test_aufgabe.py
Normal file
178
11_polymorphie_und_interfaces/test_aufgabe.py
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import unittest
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Sicherstellen, dass das Kapitel-Verzeichnis im Suchpfad 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 TestKapitel11(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.Form = getattr(aufgabe, "Form", None)
|
||||||
|
self.Rechteck = getattr(aufgabe, "Rechteck", None)
|
||||||
|
self.Kreis = getattr(aufgabe, "Kreis", None)
|
||||||
|
self.Dreieck = getattr(aufgabe, "Dreieck", None)
|
||||||
|
self.Zeichenflaeche = getattr(aufgabe, "Zeichenflaeche", None)
|
||||||
|
|
||||||
|
self.assertIsNotNone(self.Form, "Klasse 'Form' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.Rechteck, "Klasse 'Rechteck' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.Kreis, "Klasse 'Kreis' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.Dreieck, "Klasse 'Dreieck' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.Zeichenflaeche, "Klasse 'Zeichenflaeche' nicht gefunden!")
|
||||||
|
|
||||||
|
def test_01_form_ist_abstrakt(self):
|
||||||
|
"""Prüft, ob Form eine echte abstrakte Klasse ist (kann nicht direkt instanziiert werden)."""
|
||||||
|
with self.assertRaises(TypeError, msg="Abstrakte Klasse 'Form' darf nicht direkt instanziiert werden können!"):
|
||||||
|
self.Form("gelb")
|
||||||
|
|
||||||
|
def test_02_rechteck_berechnungen_und_vererbung(self):
|
||||||
|
"""Prüft Rechteck: Vererbung von Form, Flächen- und Umfangsberechnung."""
|
||||||
|
self.assertTrue(issubclass(self.Rechteck, self.Form), "Rechteck muss von Form erben!")
|
||||||
|
|
||||||
|
r = self.Rechteck(5.0, 3.0, farbe="rot")
|
||||||
|
self.assertIsInstance(r, self.Form)
|
||||||
|
self.assertEqual(r.farbe, "rot")
|
||||||
|
self.assertEqual(r.breite, 5.0)
|
||||||
|
self.assertEqual(r.hoehe, 3.0)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(r.flaeche(), 15.0)
|
||||||
|
self.assertAlmostEqual(r.umfang(), 16.0)
|
||||||
|
|
||||||
|
info = r.info()
|
||||||
|
self.assertIn("Rechteck", info)
|
||||||
|
self.assertIn("rot", info)
|
||||||
|
self.assertIn("15.00", info)
|
||||||
|
self.assertIn("16.00", info)
|
||||||
|
|
||||||
|
def test_03_kreis_berechnungen_und_vererbung(self):
|
||||||
|
"""Prüft Kreis: Vererbung von Form, Flächen- und Umfangsberechnung mit math.pi."""
|
||||||
|
self.assertTrue(issubclass(self.Kreis, self.Form), "Kreis muss von Form erben!")
|
||||||
|
|
||||||
|
k = self.Kreis(2.0, farbe="blau")
|
||||||
|
self.assertIsInstance(k, self.Form)
|
||||||
|
self.assertEqual(k.farbe, "blau")
|
||||||
|
self.assertEqual(k.radius, 2.0)
|
||||||
|
|
||||||
|
erwartete_flaeche = math.pi * (2.0 ** 2)
|
||||||
|
erwarteter_umfang = 2.0 * math.pi * 2.0
|
||||||
|
self.assertAlmostEqual(k.flaeche(), erwartete_flaeche)
|
||||||
|
self.assertAlmostEqual(k.umfang(), erwarteter_umfang)
|
||||||
|
|
||||||
|
info = k.info()
|
||||||
|
self.assertIn("Kreis", info)
|
||||||
|
self.assertIn("blau", info)
|
||||||
|
self.assertIn("r=2.0", info)
|
||||||
|
|
||||||
|
def test_04_dreieck_berechnungen_und_pythagoras(self):
|
||||||
|
"""Prüft rechtwinkliges Dreieck: Hypotenuse, Fläche und Umfang."""
|
||||||
|
self.assertTrue(issubclass(self.Dreieck, self.Form), "Dreieck muss von Form erben!")
|
||||||
|
|
||||||
|
d = self.Dreieck(3.0, 4.0, farbe="gruen")
|
||||||
|
self.assertIsInstance(d, self.Form)
|
||||||
|
self.assertEqual(d.farbe, "gruen")
|
||||||
|
self.assertEqual(d.seite_a, 3.0)
|
||||||
|
self.assertEqual(d.seite_b, 4.0)
|
||||||
|
|
||||||
|
# Hypotenuse: sqrt(3^2 + 4^2) = sqrt(9 + 16) = 5.0
|
||||||
|
self.assertAlmostEqual(d.hypotenuse(), 5.0)
|
||||||
|
# Fläche: 0.5 * 3 * 4 = 6.0
|
||||||
|
self.assertAlmostEqual(d.flaeche(), 6.0)
|
||||||
|
# Umfang: 3 + 4 + 5 = 12.0
|
||||||
|
self.assertAlmostEqual(d.umfang(), 12.0)
|
||||||
|
|
||||||
|
info = d.info()
|
||||||
|
self.assertIn("Dreieck", info)
|
||||||
|
self.assertIn("gruen", info)
|
||||||
|
self.assertIn("c=5.0", info)
|
||||||
|
|
||||||
|
def test_05_polymorphismus_in_schleife(self):
|
||||||
|
"""Prüft, ob eine heterogene Liste von Formen einheitlich polymorph durchlaufen werden kann."""
|
||||||
|
formen = [
|
||||||
|
self.Rechteck(4.0, 2.0),
|
||||||
|
self.Kreis(1.0),
|
||||||
|
self.Dreieck(3.0, 4.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Polymorpher Aufruf von flaeche() auf jedem Element
|
||||||
|
flaechen = [f.flaeche() for f in formen]
|
||||||
|
self.assertAlmostEqual(flaechen[0], 8.0)
|
||||||
|
self.assertAlmostEqual(flaechen[1], math.pi)
|
||||||
|
self.assertAlmostEqual(flaechen[2], 6.0)
|
||||||
|
|
||||||
|
def test_06_zeichenflaeche_grundlagen(self):
|
||||||
|
"""Prüft Zeichenflaeche Initialisierung und leeren Zustand."""
|
||||||
|
zf = self.Zeichenflaeche("Mein Bild")
|
||||||
|
self.assertEqual(zf.name, "Mein Bild")
|
||||||
|
self.assertEqual(zf.anzahl_formen(), 0)
|
||||||
|
self.assertAlmostEqual(zf.gesamte_flaeche(), 0.0)
|
||||||
|
self.assertAlmostEqual(zf.gesamter_umfang(), 0.0)
|
||||||
|
self.assertIsNone(zf.groesste_form())
|
||||||
|
self.assertEqual(zf.report(), [])
|
||||||
|
|
||||||
|
def test_07_zeichenflaeche_summen_berechnung(self):
|
||||||
|
"""Prüft polymorphe Summenberechnung von gesamte_flaeche() und gesamter_umfang()."""
|
||||||
|
zf = self.Zeichenflaeche()
|
||||||
|
r = self.Rechteck(4.0, 5.0) # Fläche: 20, Umfang: 18
|
||||||
|
d = self.Dreieck(3.0, 4.0) # Fläche: 6, Umfang: 12
|
||||||
|
k = self.Kreis(2.0) # Fläche: 4*pi, Umfang: 4*pi
|
||||||
|
|
||||||
|
zf.hinzufuegen(r)
|
||||||
|
zf.hinzufuegen(d)
|
||||||
|
zf.hinzufuegen(k)
|
||||||
|
|
||||||
|
self.assertEqual(zf.anzahl_formen(), 3)
|
||||||
|
self.assertAlmostEqual(zf.gesamte_flaeche(), 20.0 + 6.0 + (math.pi * 4.0))
|
||||||
|
self.assertAlmostEqual(zf.gesamter_umfang(), 18.0 + 12.0 + (2.0 * math.pi * 2.0))
|
||||||
|
|
||||||
|
def test_08_zeichenflaeche_filtern_und_groesste(self):
|
||||||
|
"""Prüft Filtern nach Farbe (case-insensitive) und Ermittlung der größten Form."""
|
||||||
|
zf = self.Zeichenflaeche()
|
||||||
|
r1 = self.Rechteck(2.0, 2.0, farbe="Rot") # Fläche 4
|
||||||
|
r2 = self.Rechteck(10.0, 5.0, farbe="rot") # Fläche 50 (Größte)
|
||||||
|
k = self.Kreis(2.0, farbe="Blau") # Fläche ~12.57
|
||||||
|
d = self.Dreieck(3.0, 4.0, farbe="ROT") # Fläche 6
|
||||||
|
|
||||||
|
zf.hinzufuegen(r1)
|
||||||
|
zf.hinzufuegen(r2)
|
||||||
|
zf.hinzufuegen(k)
|
||||||
|
zf.hinzufuegen(d)
|
||||||
|
|
||||||
|
# Farbfilterung (case-insensitive: Rot, rot, ROT)
|
||||||
|
rote = zf.formen_nach_farbe("rot")
|
||||||
|
self.assertEqual(len(rote), 3)
|
||||||
|
self.assertIn(r1, rote)
|
||||||
|
self.assertIn(r2, rote)
|
||||||
|
self.assertIn(d, rote)
|
||||||
|
|
||||||
|
blaue = zf.formen_nach_farbe("BLAU")
|
||||||
|
self.assertEqual(len(blaue), 1)
|
||||||
|
self.assertIn(k, blaue)
|
||||||
|
|
||||||
|
gelbe = zf.formen_nach_farbe("gelb")
|
||||||
|
self.assertEqual(len(gelbe), 0)
|
||||||
|
|
||||||
|
# Größte Form finden
|
||||||
|
groesste = zf.groesste_form()
|
||||||
|
self.assertIs(groesste, r2)
|
||||||
|
|
||||||
|
def test_09_zeichenflaeche_report(self):
|
||||||
|
"""Prüft report() Ausgabe."""
|
||||||
|
zf = self.Zeichenflaeche()
|
||||||
|
zf.hinzufuegen(self.Rechteck(2.0, 3.0, farbe="gelb"))
|
||||||
|
zf.hinzufuegen(self.Kreis(1.0, farbe="pink"))
|
||||||
|
|
||||||
|
berichte = zf.report()
|
||||||
|
self.assertEqual(len(berichte), 2)
|
||||||
|
self.assertIn("Rechteck", berichte[0])
|
||||||
|
self.assertIn("Kreis", berichte[1])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
44
12_exceptions_und_fehlerbehandlung/README.md
Normal file
44
12_exceptions_und_fehlerbehandlung/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Kapitel 12: Exceptions & Fehlerbehandlung 🛡️🛑
|
||||||
|
|
||||||
|
In diesem Kapitel lernst du, wie du deine Programme vor unkontrollierten Abstürzen schützt und saubere, selbsterklärende Fehlermeldungen mit **benutzerdefinierten Exceptions** wirfst.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Was du lernst
|
||||||
|
|
||||||
|
1. **Die Notbremse im Code (`raise`):** Wenn eine Funktion ungültige Daten erhält (z.B. negativer Betrag), wirft sie gezielt eine Exception aus.
|
||||||
|
2. **Sauberes Abfangen (`try / except`):** Wie du Fehler auffängst, ohne dass das gesamte Programm abstürzt.
|
||||||
|
3. **Vollständige Kontrollstruktur:**
|
||||||
|
- `try`: Code, der fehlschlagen könnte.
|
||||||
|
- `except ExceptionTyp as e`: Fehler fangen und behandeln.
|
||||||
|
- `else`: Wird nur ausgeführt, wenn **kein** Fehler aufgetreten ist.
|
||||||
|
- `finally`: Wird **immer** ausgeführt (z.B. Karte entnehmen, Dateien schließen).
|
||||||
|
4. **Eigene Exception-Klassen definieren:**
|
||||||
|
```python
|
||||||
|
class BankFehler(Exception):
|
||||||
|
"""Basis-Fehlerklasse für unsere Bank-App."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class KontoGesperrtError(BankFehler):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
5. **Praxisbeispiel:** Ein robuster **Bankkonto- und Geldautomat-Simulator** mit PIN-Sicherheit, Kontosperrung nach 3 Fehlversuchen und Deckungsprüfung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Die Dateien in diesem Ordner
|
||||||
|
|
||||||
|
- **`index.html`**: Die interaktive Lernseite mit visueller Notbremse-Analogie, Flowchart, Codebeispielen und Checkliste.
|
||||||
|
- **`aufgabe.py`**: Dein Arbeitsblatt zum Implementieren der Exception-Klassen, des `Bankkonto`s und der Geldautomat-Funktion.
|
||||||
|
- **`test_aufgabe.py`**: Automatische Unittest-Suite (`python3 test_aufgabe.py`).
|
||||||
|
- **`musterloesung.py`**: Vollständig ausprogrammierte Beispiellösung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bearbeite aufgabe.py
|
||||||
|
# 2. Teste deine Lösung:
|
||||||
|
python3 test_aufgabe.py
|
||||||
|
```
|
||||||
191
12_exceptions_und_fehlerbehandlung/aufgabe.py
Normal file
191
12_exceptions_und_fehlerbehandlung/aufgabe.py
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
"""
|
||||||
|
Kapitel 12: Exceptions & Fehlerbehandlung 🛡️🛑
|
||||||
|
=============================================
|
||||||
|
Schulabgleich: 26.0 Exceptions & Fehlerklassen
|
||||||
|
|
||||||
|
Aufgabe:
|
||||||
|
Erstelle ein sicheres Bankkonto- & Geldautomat-System mit benutzerdefinierten
|
||||||
|
Exceptions und sauberer Fehlerbehandlung über try-except-else-finally.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# TODO 1: Eigene Exception-Klassen definieren
|
||||||
|
# ==============================================================================
|
||||||
|
# Erstelle eine Basis-Exception-Klasse 'BankFehler', die von 'Exception' erbt.
|
||||||
|
# Erstelle dann 4 abgeleitete Exception-Klassen, die alle von 'BankFehler' erben:
|
||||||
|
# 1. UngueltigePinError
|
||||||
|
# 2. KontoGesperrtError
|
||||||
|
# 3. NichtGenugGuthabenError
|
||||||
|
# 4. UngueltigerBetragError
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class BankFehler(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UngueltigePinError(BankFehler):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KontoGesperrtError(BankFehler):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NichtGenugGuthabenError(BankFehler):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UngueltigerBetragError(BankFehler):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# TODO 2: Klasse Bankkonto
|
||||||
|
# ==============================================================================
|
||||||
|
class Bankkonto:
|
||||||
|
def __init__(self, inhaber: str, pin: str, kontostand: float = 0.0):
|
||||||
|
"""
|
||||||
|
Initialisiert ein Bankkonto.
|
||||||
|
Attribute:
|
||||||
|
- self.inhaber: str = inhaber
|
||||||
|
- self.pin: str = pin
|
||||||
|
- self.kontostand: float = float(kontostand)
|
||||||
|
- self.gesperrt: bool = False (Standardwert)
|
||||||
|
- self.fehlversuche: int = 0 (Zählt falsche PIN-Eingaben)
|
||||||
|
|
||||||
|
WICHTIG:
|
||||||
|
- Falls kontostand < 0 ist, wirf UngueltigerBetragError!
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pin_pruefen(self, eingegebene_pin: str) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft die eingegebene PIN.
|
||||||
|
|
||||||
|
Ablauf:
|
||||||
|
1. Wenn self.gesperrt True ist:
|
||||||
|
-> Wirf sofort KontoGesperrtError("Konto ist gesperrt!")
|
||||||
|
2. Wenn eingegebene_pin == self.pin:
|
||||||
|
-> Setze self.fehlversuche = 0 zurück
|
||||||
|
-> Gib True zurück
|
||||||
|
3. Wenn eingegebene_pin != self.pin:
|
||||||
|
-> Erhöhe self.fehlversuche um 1
|
||||||
|
-> Wenn self.fehlversuche >= 3:
|
||||||
|
- Setze self.gesperrt = True
|
||||||
|
- Wirf KontoGesperrtError("PIN 3-mal falsch. Konto wurde gesperrt!")
|
||||||
|
-> Andernfalls:
|
||||||
|
- Wirf UngueltigePinError(f"Falsche PIN! Noch {3 - self.fehlversuche} Versuche.")
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def einzahlen(self, betrag: float) -> float:
|
||||||
|
"""
|
||||||
|
Zahlt einen Betrag auf das Konto ein.
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
1. Wenn self.gesperrt:
|
||||||
|
-> Wirf KontoGesperrtError
|
||||||
|
2. Wenn betrag <= 0:
|
||||||
|
-> Wirf UngueltigerBetragError
|
||||||
|
3. Erhöhe self.kontostand um betrag und gib den neuen kontostand zurück.
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def abheben(self, betrag: float, pin: str) -> float:
|
||||||
|
"""
|
||||||
|
Hebt Geld vom Konto ab.
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
1. Prüfe zuerst die PIN mit self.pin_pruefen(pin)
|
||||||
|
(wirft bei falscher PIN oder Sperre automatisch die passende Exception)
|
||||||
|
2. Wenn betrag <= 0:
|
||||||
|
-> Wirf UngueltigerBetragError
|
||||||
|
3. Wenn betrag > self.kontostand:
|
||||||
|
-> Wirf NichtGenugGuthabenError
|
||||||
|
4. Ziehe betrag von self.kontostand ab und gib den neuen kontostand zurück.
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sperren(self) -> None:
|
||||||
|
"""Sperrt das Konto manuell."""
|
||||||
|
self.gesperrt = True
|
||||||
|
|
||||||
|
def entsperren(self, admin_passwort: str) -> bool:
|
||||||
|
"""
|
||||||
|
Entsperrt das Konto, wenn admin_passwort == 'ADMIN123' ist.
|
||||||
|
Setzt self.gesperrt = False und self.fehlversuche = 0.
|
||||||
|
Falls das Passwort falsch ist, wirf UngueltigePinError.
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""
|
||||||
|
Gibt einen Status-String zurück:
|
||||||
|
z.B. "Bankkonto(Max, Stand: 150.00€, Status: AKTIV)"
|
||||||
|
bzw. "Bankkonto(Max, Stand: 150.00€, Status: GESPERRT)"
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# TODO 3: Geldautomat-Auszahlung mit try - except - else - finally
|
||||||
|
# ==============================================================================
|
||||||
|
def geldautomat_abheben(konto: Bankkonto, betrag: float, pin: str) -> str:
|
||||||
|
"""
|
||||||
|
Führt eine Abhebung an einem Geldautomaten durch und fängt alle Fehler sauber ab.
|
||||||
|
|
||||||
|
Verwende die vollständige try-except-else-finally-Struktur:
|
||||||
|
|
||||||
|
Ablauf:
|
||||||
|
1. Erstelle eine leere Liste 'meldungen = []'
|
||||||
|
2. try:
|
||||||
|
konto.abheben(betrag, pin)
|
||||||
|
except UngueltigePinError as e:
|
||||||
|
meldungen.append(f"PIN-Fehler: {e}")
|
||||||
|
except KontoGesperrtError as e:
|
||||||
|
meldungen.append(f"Sicherheits-Fehler: {e}")
|
||||||
|
except NichtGenugGuthabenError as e:
|
||||||
|
meldungen.append(f"Deckungs-Fehler: {e}")
|
||||||
|
except UngueltigerBetragError as e:
|
||||||
|
meldungen.append(f"Betrags-Fehler: {e}")
|
||||||
|
except BankFehler as e:
|
||||||
|
meldungen.append(f"Bank-Fehler: {e}")
|
||||||
|
else:
|
||||||
|
meldungen.append(f"Auszahlung erfolgreich: {betrag:.2f}€ ausgezahlt. Neuer Stand: {konto.kontostand:.2f}€")
|
||||||
|
finally:
|
||||||
|
meldungen.append("Bitte Karte entnehmen.")
|
||||||
|
|
||||||
|
3. Gib die Meldungen mit ' | '.join(meldungen) als einzelnen String zurück.
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Kleiner Test zum Ausprobieren im Terminal:
|
||||||
|
# (python3 aufgabe.py)
|
||||||
|
# ==============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
konto = Bankkonto("Anna", "1234", 250.0)
|
||||||
|
print("Konto erstellt:", konto)
|
||||||
|
|
||||||
|
print("\n--- Test 1: Erfolgreiche Abhebung ---")
|
||||||
|
resultat = geldautomat_abheben(konto, 50.0, "1234")
|
||||||
|
print(resultat)
|
||||||
|
|
||||||
|
print("\n--- Test 2: Falsche PIN ---")
|
||||||
|
resultat = geldautomat_abheben(konto, 50.0, "9999")
|
||||||
|
print(resultat)
|
||||||
|
|
||||||
|
print("\n--- Test 3: Zu viel abheben ---")
|
||||||
|
resultat = geldautomat_abheben(konto, 500.0, "1234")
|
||||||
|
print(resultat)
|
||||||
259
12_exceptions_und_fehlerbehandlung/index.html
Normal file
259
12_exceptions_und_fehlerbehandlung/index.html
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kapitel 12: Exceptions & Fehlerbehandlung</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="../11_polymorphie_und_interfaces/index.html">Kapitel 11</a>
|
||||||
|
<span>›</span>
|
||||||
|
<span style="color: white; font-weight: 600;">Kapitel 12</span>
|
||||||
|
</nav>
|
||||||
|
<div class="header-badge">Kapitel 12 • Schulabgleich 26.0</div>
|
||||||
|
<h1>Exceptions & Fehlerbehandlung 🛡️🛑</h1>
|
||||||
|
<p>Lerne, wie du Programme vor unkontrollierten Abstürzen schützt, saubere Kontrollflüsse mit <code>try-except-else-finally</code> baust und eigene Exception-Klassen definierst.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
|
||||||
|
<!-- 1. Das Konzept: Die Notbremse-Analogie -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>1. Was ist eine Exception? (Die Notbremse im Code)</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In der realen Welt läuft nicht immer alles nach Plan: Eine Datei existiert nicht, der Benutzer tippt Buchstaben statt Zahlen ein, oder das Bankkonto hat nicht genug Guthaben.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🚆 Die Notbremse-Analogie:</div>
|
||||||
|
Stelle dir einen Zug vor. Normalerweise fährt der Zug geradeaus weiter.
|
||||||
|
Wenn plötzlich ein Baum auf den Schienen liegt, zieht der Lokführer die <strong>Notbremse</strong> (in Python: <code>raise Exception()</code>).
|
||||||
|
Statt dass der Zug ungebremst entgleist (Absturz mit rotem Traceback), fängt das Rettungsteam die Situation geordnet ab (in Python: <code>try ... except</code>).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Typische Python-Standard-Exceptions im Überblick:</h3>
|
||||||
|
|
||||||
|
<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);">Exception-Typ</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">Wann tritt sie auf?</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">Beispiel</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>ValueError</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Richtiger Datentyp, aber unpassender Wert</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>int("hallo")</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>TypeError</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Falscher Datentyp für eine Operation</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>"Zahl: " + 5</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>IndexError</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Zugriff auf einen nicht existierenden Listen-Index</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>liste[99]</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>KeyError</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Schlüssel in einem Dictionary nicht vorhanden</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>dict["unbekannt"]</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>ZeroDivisionError</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Division durch die Zahl 0</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>10 / 0</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. Die vollständige try-except-else-finally Struktur -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>2. Die 4 Säulen der Fehlerbehandlung</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Python bietet mit <code>try</code>, <code>except</code>, <code>else</code> und <code>finally</code> eine extrem saubere Struktur, um auf jede Eventualität vorbereitet zu sein:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Struktur: try - except - else - finally</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">try</span>:
|
||||||
|
<span class="cm"># 1. Versuche diesen Code auszuführen (kann fehlschlagen)</span>
|
||||||
|
<span class="var">eingabe</span> = <span class="fn">int</span>(<span class="fn">input</span>(<span class="str">"Betrag eingeben: "</span>))
|
||||||
|
<span class="var">ergebnis</span> = <span class="num">100</span> / <span class="var">eingabe</span>
|
||||||
|
|
||||||
|
<span class="kw">except</span> <span class="fn">ValueError</span> <span class="kw">as</span> <span class="var">e</span>:
|
||||||
|
<span class="cm"># 2a. Wird NUR ausgeführt, wenn ein ValueError auftrat</span>
|
||||||
|
<span class="fn">print</span>(<span class="str">f"Ungültige Eingabe: Keine Zahl! ({e})"</span>)
|
||||||
|
|
||||||
|
<span class="kw">except</span> <span class="fn">ZeroDivisionError</span>:
|
||||||
|
<span class="cm"># 2b. Wird NUR ausgeführt, wenn durch 0 geteilt wurde</span>
|
||||||
|
<span class="fn">print</span>(<span class="str">"Division durch 0 ist nicht erlaubt!"</span>)
|
||||||
|
|
||||||
|
<span class="kw">else</span>:
|
||||||
|
<span class="cm"># 3. Wird NUR ausgeführt, wenn ALLES GLATT ging (kein Fehler)</span>
|
||||||
|
<span class="fn">print</span>(<span class="str">f"Erfolg! Das Ergebnis ist: {ergebnis}"</span>)
|
||||||
|
|
||||||
|
<span class="kw">finally</span>:
|
||||||
|
<span class="cm"># 4. Wird IMMER ausgeführt (egal ob Fehler oder Erfolg!)</span>
|
||||||
|
<span class="fn">print</span>(<span class="str">"Aufräumarbeiten beendet. Vorgang abgeschlossen."</span>)</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-tipp">
|
||||||
|
<div class="box-title">💡 Warum <code>finally</code>?</div>
|
||||||
|
<code>finally</code> ist perfekt für <strong>Aufräumarbeiten</strong>, die unter keinen Umständen vergessen werden dürfen – z.B. das Auswerfen der EC-Karte am Geldautomaten oder das Schließen einer offenen Datenbankverbindung!
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. Eigene Exception-Klassen definieren -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>3. Eigene Exception-Klassen definieren & werfen (<code>raise</code>)</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In professionellen OOP-Anwendungen nutzt man nicht nur die Standard-Fehler von Python, sondern baut <strong>eigene Fehlerklassen</strong>.
|
||||||
|
Dadurch weiß jeder sofort, worum es in deiner Fachdomäne geht (z.B. <code>KontoGesperrtError</code> statt nur ungenauem <code>Exception</code>).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Eigene Exception-Hierarchie</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="cm"># 1. Basisklasse für alle Fehler in der Bank-Anwendung</span>
|
||||||
|
<span class="kw">class</span> <span class="fn">BankFehler</span>(<span class="fn">Exception</span>):
|
||||||
|
<span class="str">"""Basis-Exception für alle Bank-bezogenen Probleme."""</span>
|
||||||
|
<span class="kw">pass</span>
|
||||||
|
|
||||||
|
<span class="cm"># 2. Spezifische Fehler erben von unserer Basisklasse</span>
|
||||||
|
<span class="kw">class</span> <span class="fn">KontoGesperrtError</span>(<span class="fn">BankFehler</span>):
|
||||||
|
<span class="kw">pass</span>
|
||||||
|
|
||||||
|
<span class="kw">class</span> <span class="fn">NichtGenugGuthabenError</span>(<span class="fn">BankFehler</span>):
|
||||||
|
<span class="kw">pass</span>
|
||||||
|
|
||||||
|
<span class="kw">class</span> <span class="fn">UngueltigePinError</span>(<span class="fn">BankFehler</span>):
|
||||||
|
<span class="kw">pass</span></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Einen Fehler mit <code>raise</code> auslösen:</h3>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Fehler gezielt werfen</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">def</span> <span class="fn">abheben</span>(<span class="var">self</span>, <span class="var">betrag</span>: <span class="fn">float</span>) -> <span class="fn">float</span>:
|
||||||
|
<span class="kw">if</span> <span class="var">betrag</span> > <span class="var">self</span>.<span class="attr">kontostand</span>:
|
||||||
|
<span class="kw">raise</span> <span class="fn">NichtGenugGuthabenError</span>(<span class="str">f"Nur noch {self.kontostand}€ verfügbar!"</span>)
|
||||||
|
|
||||||
|
<span class="var">self</span>.<span class="attr">kontostand</span> -= <span class="var">betrag</span>
|
||||||
|
<span class="kw">return</span> <span class="var">self</span>.<span class="attr">kontostand</span></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Deine Aufgaben -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>4. 🎯 Deine Aufgaben in <code>aufgabe.py</code></h2>
|
||||||
|
|
||||||
|
<p>Öffne die Datei <code>12_exceptions_und_fehlerbehandlung/aufgabe.py</code> und baue den sicheren Geldautomaten-Simulator:</p>
|
||||||
|
|
||||||
|
<ul class="task-list">
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t12_1">
|
||||||
|
<label for="t12_1" class="task-label">
|
||||||
|
<strong>TODO 1:</strong> Definiere die Exception-Hierarchie: <code>BankFehler(Exception)</code> als Basis und die 4 abgeleiteten Klassen <code>UngueltigePinError</code>, <code>KontoGesperrtError</code>, <code>NichtGenugGuthabenError</code>, <code>UngueltigerBetragError</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t12_2">
|
||||||
|
<label for="t12_2" class="task-label">
|
||||||
|
<strong>TODO 2:</strong> Implementiere <code>Bankkonto.__init__</code> mit <code>inhaber</code>, <code>pin</code>, <code>kontostand</code>, <code>gesperrt = False</code> und <code>fehlversuche = 0</code>. Wirf <code>UngueltigerBetragError</code> bei negativem Startguthaben!
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t12_3">
|
||||||
|
<label for="t12_3" class="task-label">
|
||||||
|
<strong>TODO 3:</strong> Implementiere <code>pin_pruefen(self, eingegebene_pin: str)</code>. Nach 3 falschen Versuchen wird das Konto gesperrt (<code>self.gesperrt = True</code>) und <code>KontoGesperrtError</code> geworfen.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t12_4">
|
||||||
|
<label for="t12_4" class="task-label">
|
||||||
|
<strong>TODO 4:</strong> Implementiere <code>einzahlen(self, betrag)</code> und <code>abheben(self, betrag, pin)</code> mit sauberer Überprüfung aller Randfälle.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t12_5">
|
||||||
|
<label for="t12_5" class="task-label">
|
||||||
|
<strong>TODO 5:</strong> Implementiere <code>geldautomat_abheben(konto, betrag, pin)</code> mit der vollständigen <code>try-except-else-finally</code>-Struktur!
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>💡 Lösungshinweise (Aufklappen bei Bedarf)</h3>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zur PIN-Prüfung & Fehlversuchen</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="kw">if</span> <span class="var">self</span>.<span class="attr">gesperrt</span>:
|
||||||
|
<span class="kw">raise</span> <span class="fn">KontoGesperrtError</span>(<span class="str">"Konto ist gesperrt!"</span>)
|
||||||
|
|
||||||
|
<span class="kw">if</span> <span class="var">eingegebene_pin</span> == <span class="var">self</span>.<span class="attr">pin</span>:
|
||||||
|
<span class="var">self</span>.<span class="attr">fehlversuche</span> = <span class="num">0</span>
|
||||||
|
<span class="kw">return</span> <span class="kw">True</span>
|
||||||
|
<span class="kw">else</span>:
|
||||||
|
<span class="var">self</span>.<span class="attr">fehlversuche</span> += <span class="num">1</span>
|
||||||
|
<span class="kw">if</span> <span class="var">self</span>.<span class="attr">fehlversuche</span> >= <span class="num">3</span>:
|
||||||
|
<span class="var">self</span>.<span class="attr">gesperrt</span> = <span class="kw">True</span>
|
||||||
|
<span class="kw">raise</span> <span class="fn">KontoGesperrtError</span>(<span class="str">"PIN 3-mal falsch. Konto wurde gesperrt!"</span>)
|
||||||
|
<span class="kw">raise</span> <span class="fn">UngueltigePinError</span>(<span class="str">f"Falsche PIN! Noch {3 - self.fehlversuche} Versuche."</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu geldautomat_abheben (try-except-else-finally)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<p>
|
||||||
|
Im <code>try</code>-Block rufst du einfach <code>konto.abheben(betrag, pin)</code> auf. Fange die verschiedenen Exception-Typen in separaten <code>except</code>-Blöcken ab und füge die Meldung zu deiner Liste hinzu.
|
||||||
|
Im <code>else</code>-Block meldest du den Erfolg und im <code>finally</code>-Block "Bitte Karte entnehmen.".
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. Testen & Navigation -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>5. 🧪 Lösung überprüfen</h2>
|
||||||
|
<p>Führe im Terminal im Ordner <code>12_exceptions_und_fehlerbehandlung</code> folgenden Befehl aus:</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Terminal</span>
|
||||||
|
</div>
|
||||||
|
<pre><code>python3 test_aufgabe.py</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-nav">
|
||||||
|
<a href="../11_polymorphie_und_interfaces/index.html" class="btn btn-secondary">← Zurück zu Kapitel 11</a>
|
||||||
|
<a href="../13_persistenz_json_und_csv/index.html" class="btn">Weiter zu Kapitel 13 →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
141
12_exceptions_und_fehlerbehandlung/musterloesung.py
Normal file
141
12_exceptions_und_fehlerbehandlung/musterloesung.py
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
"""
|
||||||
|
Kapitel 12: Exceptions & Fehlerbehandlung 🛡️🛑
|
||||||
|
=============================================
|
||||||
|
Musterlösung für die Bankkonto- und Geldautomat-Simulation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 1. Eigene Exception-Hierarchie
|
||||||
|
# ==============================================================================
|
||||||
|
class BankFehler(Exception):
|
||||||
|
"""Basisklasse für alle Fehler im Bankkonto-System."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UngueltigePinError(BankFehler):
|
||||||
|
"""Wird geworfen, wenn die eingegebene PIN falsch ist."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KontoGesperrtError(BankFehler):
|
||||||
|
"""Wird geworfen, wenn das Konto gesperrt ist (oder nach 3 Fehlversuchen gesperrt wird)."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NichtGenugGuthabenError(BankFehler):
|
||||||
|
"""Wird geworfen, wenn das Guthaben für eine Abhebung nicht ausreicht."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UngueltigerBetragError(BankFehler):
|
||||||
|
"""Wird geworfen, wenn ein ungültiger (z.B. <= 0) Betrag übergeben wird."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 2. Klasse Bankkonto
|
||||||
|
# ==============================================================================
|
||||||
|
class Bankkonto:
|
||||||
|
def __init__(self, inhaber: str, pin: str, kontostand: float = 0.0):
|
||||||
|
"""Initialisiert ein Bankkonto mit Inhaber, PIN und Startguthaben."""
|
||||||
|
if kontostand < 0:
|
||||||
|
raise UngueltigerBetragError("Der Anfangskontostand darf nicht negativ sein!")
|
||||||
|
self.inhaber: str = inhaber
|
||||||
|
self.pin: str = pin
|
||||||
|
self.kontostand: float = float(kontostand)
|
||||||
|
self.gesperrt: bool = False
|
||||||
|
self.fehlversuche: int = 0
|
||||||
|
|
||||||
|
def pin_pruefen(self, eingegebene_pin: str) -> bool:
|
||||||
|
"""Prüft die PIN, zählt Fehlversuche und sperrt ggf. das Konto."""
|
||||||
|
if self.gesperrt:
|
||||||
|
raise KontoGesperrtError(f"Das Konto von {self.inhaber} ist gesperrt!")
|
||||||
|
|
||||||
|
if eingegebene_pin == self.pin:
|
||||||
|
self.fehlversuche = 0
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.fehlversuche += 1
|
||||||
|
if self.fehlversuche >= 3:
|
||||||
|
self.gesperrt = True
|
||||||
|
raise KontoGesperrtError("PIN 3-mal falsch eingegeben! Das Konto wurde aus Sicherheitsgründen gesperrt.")
|
||||||
|
raise UngueltigePinError(f"Falsche PIN! Verbleibende Versuche: {3 - self.fehlversuche}")
|
||||||
|
|
||||||
|
def einzahlen(self, betrag: float) -> float:
|
||||||
|
"""Zahlt Geld auf das Konto ein."""
|
||||||
|
if self.gesperrt:
|
||||||
|
raise KontoGesperrtError(f"Das Konto von {self.inhaber} ist gesperrt!")
|
||||||
|
if betrag <= 0:
|
||||||
|
raise UngueltigerBetragError("Einzahlungsbetrag muss größer als 0 sein!")
|
||||||
|
self.kontostand += betrag
|
||||||
|
return self.kontostand
|
||||||
|
|
||||||
|
def abheben(self, betrag: float, pin: str) -> float:
|
||||||
|
"""Hebt Geld nach erfolgreicher PIN- und Guthabenprüfung ab."""
|
||||||
|
# 1. PIN & Sperre prüfen (wirft automatisch passende Fehler)
|
||||||
|
self.pin_pruefen(pin)
|
||||||
|
|
||||||
|
# 2. Betrag prüfen
|
||||||
|
if betrag <= 0:
|
||||||
|
raise UngueltigerBetragError("Abhebebetrag muss größer als 0 sein!")
|
||||||
|
|
||||||
|
# 3. Deckung prüfen
|
||||||
|
if betrag > self.kontostand:
|
||||||
|
raise NichtGenugGuthabenError(f"Nicht genug Guthaben! Verfügbar: {self.kontostand:.2f}€, Angefordert: {betrag:.2f}€")
|
||||||
|
|
||||||
|
self.kontostand -= betrag
|
||||||
|
return self.kontostand
|
||||||
|
|
||||||
|
def sperren(self) -> None:
|
||||||
|
"""Sperrt das Konto manuell."""
|
||||||
|
self.gesperrt = True
|
||||||
|
|
||||||
|
def entsperren(self, admin_passwort: str) -> bool:
|
||||||
|
"""Entsperrt das Konto bei korrektem Admin-Passwort."""
|
||||||
|
if admin_passwort == "ADMIN123":
|
||||||
|
self.gesperrt = False
|
||||||
|
self.fehlversuche = 0
|
||||||
|
return True
|
||||||
|
raise UngueltigePinError("Falsches Admin-Passwort zum Entsperren!")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""String-Repräsentation des Kontos."""
|
||||||
|
status = "GESPERRT" if self.gesperrt else "AKTIV"
|
||||||
|
return f"Bankkonto({self.inhaber}, Stand: {self.kontostand:.2f}€, Status: {status})"
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 3. Geldautomat mit vollständiger try-except-else-finally-Struktur
|
||||||
|
# ==============================================================================
|
||||||
|
def geldautomat_abheben(konto: Bankkonto, betrag: float, pin: str) -> str:
|
||||||
|
"""Führt eine sichere Abhebung mit try-except-else-finally durch."""
|
||||||
|
meldungen = []
|
||||||
|
try:
|
||||||
|
konto.abheben(betrag, pin)
|
||||||
|
except UngueltigePinError as e:
|
||||||
|
meldungen.append(f"PIN-Fehler: {e}")
|
||||||
|
except KontoGesperrtError as e:
|
||||||
|
meldungen.append(f"Sicherheits-Fehler: {e}")
|
||||||
|
except NichtGenugGuthabenError as e:
|
||||||
|
meldungen.append(f"Deckungs-Fehler: {e}")
|
||||||
|
except UngueltigerBetragError as e:
|
||||||
|
meldungen.append(f"Betrags-Fehler: {e}")
|
||||||
|
except BankFehler as e:
|
||||||
|
meldungen.append(f"Bank-Fehler: {e}")
|
||||||
|
else:
|
||||||
|
meldungen.append(f"Auszahlung erfolgreich: {betrag:.2f}€ ausgezahlt. Neuer Stand: {konto.kontostand:.2f}€")
|
||||||
|
finally:
|
||||||
|
meldungen.append("Bitte Karte entnehmen.")
|
||||||
|
|
||||||
|
return " | ".join(meldungen)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
konto = Bankkonto("Anna", "1234", 250.0)
|
||||||
|
print(konto)
|
||||||
|
print(geldautomat_abheben(konto, 50.0, "1234"))
|
||||||
|
print(geldautomat_abheben(konto, 50.0, "9999"))
|
||||||
|
print(geldautomat_abheben(konto, 500.0, "1234"))
|
||||||
166
12_exceptions_und_fehlerbehandlung/test_aufgabe.py
Normal file
166
12_exceptions_und_fehlerbehandlung/test_aufgabe.py
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
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 TestKapitel12(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Klassen und Funktionen dynamisch aus aufgabe laden
|
||||||
|
self.BankFehler = getattr(aufgabe, "BankFehler", None)
|
||||||
|
self.UngueltigePinError = getattr(aufgabe, "UngueltigePinError", None)
|
||||||
|
self.KontoGesperrtError = getattr(aufgabe, "KontoGesperrtError", None)
|
||||||
|
self.NichtGenugGuthabenError = getattr(aufgabe, "NichtGenugGuthabenError", None)
|
||||||
|
self.UngueltigerBetragError = getattr(aufgabe, "UngueltigerBetragError", None)
|
||||||
|
self.Bankkonto = getattr(aufgabe, "Bankkonto", None)
|
||||||
|
self.geldautomat_abheben = getattr(aufgabe, "geldautomat_abheben", None)
|
||||||
|
|
||||||
|
self.assertIsNotNone(self.BankFehler, "Klasse 'BankFehler' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.UngueltigePinError, "Klasse 'UngueltigePinError' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.KontoGesperrtError, "Klasse 'KontoGesperrtError' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.NichtGenugGuthabenError, "Klasse 'NichtGenugGuthabenError' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.UngueltigerBetragError, "Klasse 'UngueltigerBetragError' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.Bankkonto, "Klasse 'Bankkonto' nicht gefunden!")
|
||||||
|
|
||||||
|
def test_01_exception_hierarchie(self):
|
||||||
|
"""Prüft, ob die Exception-Klassen korrekt voneinander und von Exception erben."""
|
||||||
|
self.assertTrue(issubclass(self.BankFehler, Exception), "BankFehler muss von Exception erben!")
|
||||||
|
self.assertTrue(issubclass(self.UngueltigePinError, self.BankFehler), "UngueltigePinError muss von BankFehler erben!")
|
||||||
|
self.assertTrue(issubclass(self.KontoGesperrtError, self.BankFehler), "KontoGesperrtError muss von BankFehler erben!")
|
||||||
|
self.assertTrue(issubclass(self.NichtGenugGuthabenError, self.BankFehler), "NichtGenugGuthabenError muss von BankFehler erben!")
|
||||||
|
self.assertTrue(issubclass(self.UngueltigerBetragError, self.BankFehler), "UngueltigerBetragError muss von BankFehler erben!")
|
||||||
|
|
||||||
|
def test_02_bankkonto_init(self):
|
||||||
|
"""Prüft Initialisierung des Bankkontos und Validierung des Startguthabens."""
|
||||||
|
k = self.Bankkonto("Max Mustermann", "4321", 100.0)
|
||||||
|
self.assertEqual(k.inhaber, "Max Mustermann")
|
||||||
|
self.assertEqual(k.pin, "4321")
|
||||||
|
self.assertEqual(k.kontostand, 100.0)
|
||||||
|
self.assertFalse(k.gesperrt)
|
||||||
|
self.assertEqual(k.fehlversuche, 0)
|
||||||
|
|
||||||
|
# Negatives Startguthaben muss UngueltigerBetragError werfen
|
||||||
|
with self.assertRaises(self.UngueltigerBetragError):
|
||||||
|
self.Bankkonto("Schuldner", "1111", -50.0)
|
||||||
|
|
||||||
|
def test_03_pin_pruefen_und_sperre(self):
|
||||||
|
"""Prüft PIN-Validierung, Fehlversuch-Zähler und automatische Kontosperre nach 3 Versuchen."""
|
||||||
|
k = self.Bankkonto("Lisa", "1234", 200.0)
|
||||||
|
|
||||||
|
# Richtige PIN
|
||||||
|
self.assertTrue(k.pin_pruefen("1234"))
|
||||||
|
self.assertEqual(k.fehlversuche, 0)
|
||||||
|
|
||||||
|
# 1. Fehlversuch
|
||||||
|
with self.assertRaises(self.UngueltigePinError):
|
||||||
|
k.pin_pruefen("0000")
|
||||||
|
self.assertEqual(k.fehlversuche, 1)
|
||||||
|
self.assertFalse(k.gesperrt)
|
||||||
|
|
||||||
|
# 2. Fehlversuch
|
||||||
|
with self.assertRaises(self.UngueltigePinError):
|
||||||
|
k.pin_pruefen("1111")
|
||||||
|
self.assertEqual(k.fehlversuche, 2)
|
||||||
|
self.assertFalse(k.gesperrt)
|
||||||
|
|
||||||
|
# 3. Fehlversuch -> muss KontoGesperrtError werfen und Konto sperren
|
||||||
|
with self.assertRaises(self.KontoGesperrtError):
|
||||||
|
k.pin_pruefen("2222")
|
||||||
|
self.assertEqual(k.fehlversuche, 3)
|
||||||
|
self.assertTrue(k.gesperrt)
|
||||||
|
|
||||||
|
# Weiterer Versuch auf gesperrtem Konto wirft sofort KontoGesperrtError (selbst bei richtiger PIN)
|
||||||
|
with self.assertRaises(self.KontoGesperrtError):
|
||||||
|
k.pin_pruefen("1234")
|
||||||
|
|
||||||
|
def test_04_einzahlen(self):
|
||||||
|
"""Prüft Einzahlungen und Validierung von Beträgen."""
|
||||||
|
k = self.Bankkonto("Tom", "1234", 50.0)
|
||||||
|
neuer_stand = k.einzahlen(30.0)
|
||||||
|
self.assertEqual(neuer_stand, 80.0)
|
||||||
|
self.assertEqual(k.kontostand, 80.0)
|
||||||
|
|
||||||
|
# Ungültige Beträge
|
||||||
|
with self.assertRaises(self.UngueltigerBetragError):
|
||||||
|
k.einzahlen(0.0)
|
||||||
|
with self.assertRaises(self.UngueltigerBetragError):
|
||||||
|
k.einzahlen(-20.0)
|
||||||
|
|
||||||
|
# Einzahlen auf gesperrtem Konto
|
||||||
|
k.sperren()
|
||||||
|
with self.assertRaises(self.KontoGesperrtError):
|
||||||
|
k.einzahlen(50.0)
|
||||||
|
|
||||||
|
def test_05_abheben(self):
|
||||||
|
"""Prüft Abhebungen, PIN-Prüfung, Guthaben-Checks und Betragsprüfung."""
|
||||||
|
k = self.Bankkonto("Sarah", "9876", 150.0)
|
||||||
|
|
||||||
|
# Erfolgreiche Abhebung
|
||||||
|
neuer_stand = k.abheben(50.0, "9876")
|
||||||
|
self.assertEqual(neuer_stand, 100.0)
|
||||||
|
self.assertEqual(k.kontostand, 100.0)
|
||||||
|
|
||||||
|
# Falsche PIN beim Abheben
|
||||||
|
with self.assertRaises(self.UngueltigePinError):
|
||||||
|
k.abheben(20.0, "0000")
|
||||||
|
|
||||||
|
# Zu viel Geld abheben (Nicht genug Guthaben)
|
||||||
|
with self.assertRaises(self.NichtGenugGuthabenError):
|
||||||
|
k.abheben(500.0, "9876")
|
||||||
|
|
||||||
|
# Ungültiger Abhebebetrag
|
||||||
|
with self.assertRaises(self.UngueltigerBetragError):
|
||||||
|
k.abheben(-10.0, "9876")
|
||||||
|
with self.assertRaises(self.UngueltigerBetragError):
|
||||||
|
k.abheben(0.0, "9876")
|
||||||
|
|
||||||
|
def test_06_sperren_und_entsperren(self):
|
||||||
|
"""Prüft manuelles Sperren und Entsperren mit Admin-Code."""
|
||||||
|
k = self.Bankkonto("Felix", "1234", 100.0)
|
||||||
|
k.sperren()
|
||||||
|
self.assertTrue(k.gesperrt)
|
||||||
|
|
||||||
|
# Entsperren mit falschem Passwort
|
||||||
|
with self.assertRaises(self.UngueltigePinError):
|
||||||
|
k.entsperren("FALSCH")
|
||||||
|
self.assertTrue(k.gesperrt)
|
||||||
|
|
||||||
|
# Entsperren mit korrektem Passwort
|
||||||
|
erfolg = k.entsperren("ADMIN123")
|
||||||
|
self.assertTrue(erfolg)
|
||||||
|
self.assertFalse(k.gesperrt)
|
||||||
|
self.assertEqual(k.fehlversuche, 0)
|
||||||
|
|
||||||
|
def test_07_geldautomat_abheben_funktion(self):
|
||||||
|
"""Prüft die Geldautomat-Funktion mit try-except-else-finally."""
|
||||||
|
self.assertIsNotNone(self.geldautomat_abheben, "Funktion 'geldautomat_abheben' fehlt!")
|
||||||
|
|
||||||
|
k = self.Bankkonto("Elena", "5555", 200.0)
|
||||||
|
|
||||||
|
# 1. Erfolgreicher Vorgang (else + finally aktiv)
|
||||||
|
msg_erfolg = self.geldautomat_abheben(k, 50.0, "5555")
|
||||||
|
self.assertIn("Auszahlung erfolgreich", msg_erfolg)
|
||||||
|
self.assertIn("Bitte Karte entnehmen", msg_erfolg)
|
||||||
|
self.assertEqual(k.kontostand, 150.0)
|
||||||
|
|
||||||
|
# 2. Falsche PIN (except UngueltigePinError + finally aktiv)
|
||||||
|
msg_pin_err = self.geldautomat_abheben(k, 50.0, "1111")
|
||||||
|
self.assertIn("PIN-Fehler", msg_pin_err)
|
||||||
|
self.assertIn("Bitte Karte entnehmen", msg_pin_err)
|
||||||
|
|
||||||
|
# 3. Nicht genug Guthaben (except NichtGenugGuthabenError + finally aktiv)
|
||||||
|
msg_deck_err = self.geldautomat_abheben(k, 1000.0, "5555")
|
||||||
|
self.assertIn("Deckungs-Fehler", msg_deck_err)
|
||||||
|
self.assertIn("Bitte Karte entnehmen", msg_deck_err)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
44
13_persistenz_json_und_csv/README.md
Normal file
44
13_persistenz_json_und_csv/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Kapitel 13: Datei-Persistenz (JSON & CSV) 💾📁
|
||||||
|
|
||||||
|
In diesem Kapitel lernst du, wie du deine Python-Objekte dauerhaft auf der Festplatte speicherst, sodass deine Daten auch nach dem Beenden des Skripts oder Ausschalten des Computers erhalten bleiben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Was du lernst
|
||||||
|
|
||||||
|
1. **Flüchtiger RAM vs. Dauerhafte Festplatte:** Warum Daten im Arbeitsspeicher beim Programmende verschwinden und wie Dateien für dauerhafte Speicherung (Persistenz) sorgen.
|
||||||
|
2. **Sauberer Dateizugriff mit `with open(...)`:**
|
||||||
|
```python
|
||||||
|
with open("savegame.json", "w", encoding="utf-8") as f:
|
||||||
|
# Datei wird automatisch sicher geschlossen, selbst bei Fehlern!
|
||||||
|
```
|
||||||
|
3. **JSON (JavaScript Object Notation):**
|
||||||
|
- Das Standard-Format für verschachtelte Daten und Konfigurationen.
|
||||||
|
- `json.dump(daten, f, indent=4, ensure_ascii=False)` zum Schreiben.
|
||||||
|
- `daten = json.load(f)` zum Einlesen.
|
||||||
|
4. **Objekt-Serialisierung in OOP:**
|
||||||
|
- `to_dict(self)`: Wandelt ein Objekt in ein serialisierbares Dictionary um.
|
||||||
|
- `from_dict(cls, data)`: Erstellt aus einem Dictionary wieder eine echte Klasseninstanz.
|
||||||
|
5. **CSV (Comma-Separated Values):**
|
||||||
|
- Ideal für flache Tabellendaten (z.B. für Excel, Google Sheets oder Numbers).
|
||||||
|
- `csv.DictWriter` und `csv.DictReader`.
|
||||||
|
6. **Praxisbeispiel:** Ein **Savegame- und Highscore-Manager** für ein RPG / Spiel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Die Dateien in diesem Ordner
|
||||||
|
|
||||||
|
- **`index.html`**: Die interaktive Lernseite mit Tagebuch-Analogie, JSON- vs. CSV-Vergleichstabelle, Codebeispielen und Checkliste.
|
||||||
|
- **`aufgabe.py`**: Dein Arbeitsblatt zum Implementieren der Klassen `Spieler` und `Spielstand`.
|
||||||
|
- **`test_aufgabe.py`**: Automatische Unittest-Suite (`python3 test_aufgabe.py`).
|
||||||
|
- **`musterloesung.py`**: Vollständig ausprogrammierte Beispiellösung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bearbeite aufgabe.py
|
||||||
|
# 2. Teste deine Lösung:
|
||||||
|
python3 test_aufgabe.py
|
||||||
|
```
|
||||||
194
13_persistenz_json_und_csv/aufgabe.py
Normal file
194
13_persistenz_json_und_csv/aufgabe.py
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
"""
|
||||||
|
Kapitel 13: Datei-Persistenz (JSON & CSV) 💾📁
|
||||||
|
==============================================
|
||||||
|
Schulabgleich: 14.0 CSV & Dateien / Savegame-Persistenz
|
||||||
|
|
||||||
|
Aufgabe:
|
||||||
|
Implementiere ein modulares Savegame- und Highscore-System:
|
||||||
|
1. Klasse 'Spieler': mit Serialisierung (to_dict, from_dict).
|
||||||
|
2. Klasse 'Spielstand': mit JSON-Speichern/Laden und CSV-Tabellen-Export.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# TODO 1: Klasse Spieler mit Serialisierung
|
||||||
|
# ==============================================================================
|
||||||
|
class Spieler:
|
||||||
|
def __init__(self, name: str, level: int, punkte: int, inventar: Optional[List[str]] = None):
|
||||||
|
"""
|
||||||
|
Initialisiert einen Spieler.
|
||||||
|
Attribute:
|
||||||
|
- self.name: str = name
|
||||||
|
- self.level: int = int(level)
|
||||||
|
- self.punkte: int = int(punkte)
|
||||||
|
- self.inventar: list[str] = inventar (oder [] falls None)
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Wandelt das Spieler-Objekt in ein Dictionary um, damit es als JSON
|
||||||
|
gespeichert werden kann.
|
||||||
|
|
||||||
|
Rückgabe-Format:
|
||||||
|
{
|
||||||
|
"name": self.name,
|
||||||
|
"level": self.level,
|
||||||
|
"punkte": self.punkte,
|
||||||
|
"inventar": self.inventar
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, daten: Dict[str, Any]) -> "Spieler":
|
||||||
|
"""
|
||||||
|
Erstellt ein neues Spieler-Objekt aus einem Dictionary.
|
||||||
|
Beispiel: Spieler.from_dict({"name": "Alex", "level": 5, "punkte": 1200, "inventar": ["Schwert"]})
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Spieler(name='{self.name}', level={self.level}, punkte={self.punkte}, inventar={self.inventar})"
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, Spieler):
|
||||||
|
return False
|
||||||
|
return (
|
||||||
|
self.name == other.name
|
||||||
|
and self.level == other.level
|
||||||
|
and self.punkte == other.punkte
|
||||||
|
and self.inventar == other.inventar
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# TODO 2: Klasse Spielstand mit JSON- und CSV-Persistenz
|
||||||
|
# ==============================================================================
|
||||||
|
class Spielstand:
|
||||||
|
def __init__(self, titel: str = "Neuer Spielstand"):
|
||||||
|
"""
|
||||||
|
Initialisiert einen Spielstand.
|
||||||
|
Attribute:
|
||||||
|
- self.titel: str = titel
|
||||||
|
- self.spieler_liste: list[Spieler] = []
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def spieler_hinzufuegen(self, spieler: Spieler) -> None:
|
||||||
|
"""Fügt ein Spieler-Objekt zur internen Spieler-Liste hinzu."""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def spieler_suchen(self, name: str) -> Optional[Spieler]:
|
||||||
|
"""
|
||||||
|
Sucht einen Spieler nach Name.
|
||||||
|
Gibt das gefundene Spieler-Objekt zurück oder None, wenn nicht vorhanden.
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def bester_spieler(self) -> Optional[Spieler]:
|
||||||
|
"""
|
||||||
|
Gibt den Spieler mit den meisten Punkten zurück.
|
||||||
|
Gibt None zurück, wenn die Liste leer ist.
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def speichern_als_json(self, dateipfad: str | Path) -> None:
|
||||||
|
"""
|
||||||
|
Speichert den gesamten Spielstand als formatierte JSON-Datei ab.
|
||||||
|
|
||||||
|
Struktur der JSON-Datei:
|
||||||
|
{
|
||||||
|
"titel": self.titel,
|
||||||
|
"spieler": [
|
||||||
|
{ "name": ..., "level": ..., "punkte": ..., "inventar": [...] },
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
WICHTIG:
|
||||||
|
- Nutze 'with open(dateipfad, "w", encoding="utf-8") as f:'
|
||||||
|
- Nutze 'json.dump(daten, f, indent=4, ensure_ascii=False)'
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def laden_aus_json(self, dateipfad: str | Path) -> None:
|
||||||
|
"""
|
||||||
|
Lädt den Spielstand aus einer JSON-Datei und stellt alle Spieler wieder her.
|
||||||
|
|
||||||
|
Ablauf:
|
||||||
|
1. Öffne die Datei mit 'with open(dateipfad, "r", encoding="utf-8") as f:'
|
||||||
|
2. Lade die Daten mit 'json.load(f)'
|
||||||
|
3. Setze self.titel auf den geladenen Titel
|
||||||
|
4. Erstelle für jeden Eintrag in der Spieler-Liste ein neues Spieler-Objekt
|
||||||
|
mittels 'Spieler.from_dict(d)' und weise die neue Liste 'self.spieler_liste' zu.
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def exportiere_als_csv(self, dateipfad: str | Path) -> None:
|
||||||
|
"""
|
||||||
|
Exportiert eine Tabellen-Übersicht aller Spieler als CSV-Datei.
|
||||||
|
|
||||||
|
Spalten (Header):
|
||||||
|
["name", "level", "punkte", "inventar_anzahl"]
|
||||||
|
|
||||||
|
WICHTIG:
|
||||||
|
- Nutze 'with open(dateipfad, "w", newline="", encoding="utf-8") as f:'
|
||||||
|
- Nutze 'csv.DictWriter(f, fieldnames=...)'
|
||||||
|
- Schreibe zuerst den Header mit 'writer.writeheader()'
|
||||||
|
- Schreibe dann für jeden Spieler eine Zeile (inventar_anzahl = len(s.inventar))
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
def importiere_aus_csv(self, dateipfad: str | Path) -> int:
|
||||||
|
"""
|
||||||
|
Liest Spieler aus einer CSV-Datei ein und fügt sie zur spieler_liste hinzu.
|
||||||
|
(Hinweis: Das Inventar wird beim einfachen CSV-Import als leere Liste [] initialisiert).
|
||||||
|
|
||||||
|
Gibt die Anzahl der neu importierten Spieler zurück.
|
||||||
|
"""
|
||||||
|
# TODO: Implementieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Kleiner Test zum Ausprobieren im Terminal:
|
||||||
|
# (python3 aufgabe.py)
|
||||||
|
# ==============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
spielstand = Spielstand("Highscore-Tabelle RPG")
|
||||||
|
|
||||||
|
p1 = Spieler("Arthur", 12, 4500, ["Excalibur", "Heiltrank"])
|
||||||
|
p2 = Spieler("Morgana", 15, 6200, ["Zauberstab", "Manatrank", "Schriftrolle"])
|
||||||
|
p3 = Spieler("Lancelot", 10, 3800, ["Schild"])
|
||||||
|
|
||||||
|
spielstand.spieler_hinzufuegen(p1)
|
||||||
|
spielstand.spieler_hinzufuegen(p2)
|
||||||
|
spielstand.spieler_hinzufuegen(p3)
|
||||||
|
|
||||||
|
print("Bester Spieler:", spielstand.bester_spieler())
|
||||||
|
|
||||||
|
# JSON Test
|
||||||
|
json_pfad = Path("test_savegame.json")
|
||||||
|
spielstand.speichern_als_json(json_pfad)
|
||||||
|
print(f"Gespeichert in {json_pfad}!")
|
||||||
|
|
||||||
|
# CSV Test
|
||||||
|
csv_pfad = Path("test_export.csv")
|
||||||
|
spielstand.exportiere_als_csv(csv_pfad)
|
||||||
|
print(f"Exportiert nach {csv_pfad}!")
|
||||||
247
13_persistenz_json_und_csv/index.html
Normal file
247
13_persistenz_json_und_csv/index.html
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kapitel 13: Datei-Persistenz (JSON & CSV)</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="../12_exceptions_und_fehlerbehandlung/index.html">Kapitel 12</a>
|
||||||
|
<span>›</span>
|
||||||
|
<span style="color: white; font-weight: 600;">Kapitel 13</span>
|
||||||
|
</nav>
|
||||||
|
<div class="header-badge">Kapitel 13 • Schulabgleich 14.0</div>
|
||||||
|
<h1>Datei-Persistenz: JSON & CSV 💾📁</h1>
|
||||||
|
<p>Lerne, wie du Daten dauerhaft auf der Festplatte speicherst, Objekte mit <code>to_dict()</code> und <code>from_dict()</code> serialisierst und Tabellen als CSV exportierst.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
|
||||||
|
<!-- 1. Das Konzept: RAM vs Festplatte -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>1. Warum brauchen wir Persistenz? (Die Tagebuch-Analogie)</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Bislang haben alle unsere Programme ihre Daten nur im <strong>Arbeitsspeicher (RAM)</strong> gehalten.
|
||||||
|
Das Problem: Sobald das Python-Programm beendet wird oder der Strom ausfällt, ist der gesamte RAM blitzschnell gelöscht!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">📓 Die Tagebuch-Analogie:</div>
|
||||||
|
Der Arbeitsspeicher (RAM) ist wie dein <strong>Kurzzeitgedächtnis</strong>: blitzschnell, aber nach dem Schlafen (Programmende) ist alles weg.
|
||||||
|
Die Festplatte (SSD) ist dein <strong>Tagebuch / Notizbuch</strong>: Was du einmal mit Tinte hineingeschrieben hast, bleibt auch Jahre später noch genau so stehen!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Der goldene Standard: Dateien sicher öffnen mit <code>with open()</code></h3>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Kontext-Manager mit open()</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="cm"># 'w' = Write (Schreiben / Überschreiben)</span>
|
||||||
|
<span class="cm"># 'r' = Read (Lesen)</span>
|
||||||
|
<span class="cm"># 'a' = Append (Am Ende anhängen)</span>
|
||||||
|
|
||||||
|
<span class="kw">with</span> <span class="fn">open</span>(<span class="str">"notizen.txt"</span>, <span class="str">"w"</span>, <span class="var">encoding</span>=<span class="str">"utf-8"</span>) <span class="kw">as</span> <span class="var">datei</span>:
|
||||||
|
<span class="var">datei</span>.<span class="fn">write</span>(<span class="str">"Hallo Welt mit Umlauten: ä, ö, ü!\n"</span>)
|
||||||
|
<span class="cm"># Hier wird die Datei automatisch und sicher geschlossen – selbst wenn ein Fehler auftritt!</span></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. JSON vs CSV Vergleich -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>2. JSON vs. CSV: Welches Format für welchen Zweck?</h2>
|
||||||
|
|
||||||
|
<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);">Kriterium</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">JSON (JavaScript Object Notation)</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">CSV (Comma-Separated Values)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Ideal für...</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Komplexe, verschachtelte Objekte & Savegames</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Flache Tabellen (Excel, Google Sheets)</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Verschachtelung</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Ja (Listen von Objekten mit Unter-Listen)</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Nein (nur Zeilen und Spalten)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Python-Modul</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>import json</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>import csv</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Hauptbefehle</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>json.dump()</code> / <code>json.load()</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>csv.DictWriter</code> / <code>csv.DictReader</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. Objekt-Serialisierung in OOP -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>3. Objekt-Serialisierung: <code>to_dict()</code> und <code>from_dict()</code></h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Ein Python-Klassenobjekt (z.B. <code>Spieler</code>) kann nicht direkt als reiner Text gespeichert werden.
|
||||||
|
Daher übersetzen wir das Objekt in ein einfaches Dictionary (<strong>Serialisierung</strong>) und bauen es beim Laden wieder als echtes Objekt zusammen (<strong>Deserialisierung</strong>).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Serialisierungsmuster in Klassen</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">class</span> <span class="fn">Spieler</span>:
|
||||||
|
<span class="kw">def</span> <span class="fn">__init__</span>(<span class="var">self</span>, <span class="var">name</span>: <span class="fn">str</span>, <span class="var">level</span>: <span class="fn">int</span>, <span class="var">punkte</span>: <span class="fn">int</span>):
|
||||||
|
<span class="var">self</span>.<span class="attr">name</span> = <span class="var">name</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">level</span> = <span class="var">level</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">punkte</span> = <span class="var">punkte</span>
|
||||||
|
|
||||||
|
<span class="cm"># 1. In Dictionary umwandeln (zum Speichern)</span>
|
||||||
|
<span class="kw">def</span> <span class="fn">to_dict</span>(<span class="var">self</span>) -> <span class="fn">dict</span>:
|
||||||
|
<span class="kw">return</span> {
|
||||||
|
<span class="str">"name"</span>: <span class="var">self</span>.<span class="attr">name</span>,
|
||||||
|
<span class="str">"level"</span>: <span class="var">self</span>.<span class="attr">level</span>,
|
||||||
|
<span class="str">"punkte"</span>: <span class="var">self</span>.<span class="attr">punkte</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="cm"># 2. Aus Dictionary neues Objekt instanziieren (beim Laden)</span>
|
||||||
|
<span class="kw">@classmethod</span>
|
||||||
|
<span class="kw">def</span> <span class="fn">from_dict</span>(<span class="var">cls</span>, <span class="var">daten</span>: <span class="fn">dict</span>) -> <span class="str">'Spieler'</span>:
|
||||||
|
<span class="kw">return</span> <span class="fn">cls</span>(
|
||||||
|
<span class="var">name</span>=<span class="var">daten</span>[<span class="str">"name"</span>],
|
||||||
|
<span class="var">level</span>=<span class="var">daten</span>[<span class="str">"level"</span>],
|
||||||
|
<span class="var">punkte</span>=<span class="var">daten</span>[<span class="str">"punkte"</span>]
|
||||||
|
)</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Speichern und Laden mit JSON</h3>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>JSON Speichern und Laden</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">import</span> <span class="var">json</span>
|
||||||
|
|
||||||
|
<span class="cm"># Speichern:</span>
|
||||||
|
<span class="kw">with</span> <span class="fn">open</span>(<span class="str">"savegame.json"</span>, <span class="str">"w"</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">json</span>.<span class="fn">dump</span>(<span class="var">spieler</span>.<span class="fn">to_dict</span>(), <span class="var">f</span>, <span class="var">indent</span>=<span class="num">4</span>, <span class="var">ensure_ascii</span>=<span class="kw">False</span>)
|
||||||
|
|
||||||
|
<span class="cm"># Laden:</span>
|
||||||
|
<span class="kw">with</span> <span class="fn">open</span>(<span class="str">"savegame.json"</span>, <span class="str">"r"</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">daten</span> = <span class="var">json</span>.<span class="fn">load</span>(<span class="var">f</span>)
|
||||||
|
<span class="var">geladener_spieler</span> = <span class="fn">Spieler</span>.<span class="fn">from_dict</span>(<span class="var">daten</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Deine Aufgaben -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>4. 🎯 Deine Aufgaben in <code>aufgabe.py</code></h2>
|
||||||
|
|
||||||
|
<p>Öffne die Datei <code>13_persistenz_json_und_csv/aufgabe.py</code> und vervollständige das Savegame-System:</p>
|
||||||
|
|
||||||
|
<ul class="task-list">
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t13_1">
|
||||||
|
<label for="t13_1" class="task-label">
|
||||||
|
<strong>TODO 1:</strong> <code>Spieler</code>-Konstruktor sowie <code>to_dict(self)</code> und <code>from_dict(cls, daten)</code> mit <code>name</code>, <code>level</code>, <code>punkte</code> und <code>inventar</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t13_2">
|
||||||
|
<label for="t13_2" class="task-label">
|
||||||
|
<strong>TODO 2:</strong> <code>Spielstand</code>-Verwaltungsmethoden: <code>spieler_hinzufuegen</code>, <code>spieler_suchen</code> und <code>bester_spieler</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t13_3">
|
||||||
|
<label for="t13_3" class="task-label">
|
||||||
|
<strong>TODO 3:</strong> <code>speichern_als_json(dateipfad)</code>: Wandelt alle Spieler in Dicts um und schreibt die JSON-Datei mit <code>json.dump</code> formatiert auf die Festplatte.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t13_4">
|
||||||
|
<label for="t13_4" class="task-label">
|
||||||
|
<strong>TODO 4:</strong> <code>laden_aus_json(dateipfad)</code>: Liest mit <code>json.load</code> die Datei ein und stellt die <code>spieler_liste</code> mit <code>Spieler.from_dict(...)</code> wieder her.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t13_5">
|
||||||
|
<label for="t13_5" class="task-label">
|
||||||
|
<strong>TODO 5:</strong> <code>exportiere_als_csv(dateipfad)</code> und <code>importiere_aus_csv(dateipfad)</code> mit <code>csv.DictWriter</code> und <code>csv.DictReader</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>💡 Lösungshinweise (Aufklappen bei Bedarf)</h3>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zum JSON-Speichern</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="var">daten</span> = {
|
||||||
|
<span class="str">"titel"</span>: <span class="var">self</span>.<span class="attr">titel</span>,
|
||||||
|
<span class="str">"spieler"</span>: [<span class="var">s</span>.<span class="fn">to_dict</span>() <span class="kw">for</span> <span class="var">s</span> <span class="kw">in</span> <span class="var">self</span>.<span class="attr">spieler_liste</span>]
|
||||||
|
}
|
||||||
|
<span class="kw">with</span> <span class="fn">open</span>(<span class="var">dateipfad</span>, <span class="str">"w"</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">json</span>.<span class="fn">dump</span>(<span class="var">daten</span>, <span class="var">f</span>, <span class="var">indent</span>=<span class="num">4</span>, <span class="var">ensure_ascii</span>=<span class="kw">False</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zum CSV-Export</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="var">feldnamen</span> = [<span class="str">"name"</span>, <span class="str">"level"</span>, <span class="str">"punkte"</span>, <span class="str">"inventar_anzahl"</span>]
|
||||||
|
<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">DictWriter</span>(<span class="var">f</span>, <span class="var">fieldnames</span>=<span class="var">feldnamen</span>)
|
||||||
|
<span class="var">writer</span>.<span class="fn">writeheader</span>()
|
||||||
|
<span class="kw">for</span> <span class="var">s</span> <span class="kw">in</span> <span class="var">self</span>.<span class="attr">spieler_liste</span>:
|
||||||
|
<span class="var">writer</span>.<span class="fn">writerow</span>({
|
||||||
|
<span class="str">"name"</span>: <span class="var">s</span>.<span class="attr">name</span>,
|
||||||
|
<span class="str">"level"</span>: <span class="var">s</span>.<span class="attr">level</span>,
|
||||||
|
<span class="str">"punkte"</span>: <span class="var">s</span>.<span class="attr">punkte</span>,
|
||||||
|
<span class="str">"inventar_anzahl"</span>: <span class="fn">len</span>(<span class="var">s</span>.<span class="attr">inventar</span>)
|
||||||
|
})</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. Testen & Navigation -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>5. 🧪 Lösung überprüfen</h2>
|
||||||
|
<p>Führe im Terminal im Ordner <code>13_persistenz_json_und_csv</code> folgenden Befehl aus:</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Terminal</span>
|
||||||
|
</div>
|
||||||
|
<pre><code>python3 test_aufgabe.py</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-nav">
|
||||||
|
<a href="../12_exceptions_und_fehlerbehandlung/index.html" class="btn btn-secondary">← Zurück zu Kapitel 12</a>
|
||||||
|
<a href="../14_gui_mit_tkinter/index.html" class="btn">Weiter zu Kapitel 14 →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
143
13_persistenz_json_und_csv/musterloesung.py
Normal file
143
13_persistenz_json_und_csv/musterloesung.py
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
"""
|
||||||
|
Kapitel 13: Datei-Persistenz (JSON & CSV) 💾📁
|
||||||
|
==============================================
|
||||||
|
Musterlösung für JSON- und CSV-Persistenz in Python OOP.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 1. Klasse Spieler mit to_dict / from_dict
|
||||||
|
# ==============================================================================
|
||||||
|
class Spieler:
|
||||||
|
def __init__(self, name: str, level: int, punkte: int, inventar: Optional[List[str]] = None):
|
||||||
|
"""Initialisiert ein Spieler-Objekt."""
|
||||||
|
self.name: str = name
|
||||||
|
self.level: int = int(level)
|
||||||
|
self.punkte: int = int(punkte)
|
||||||
|
self.inventar: List[str] = list(inventar) if inventar is not None else []
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Konvertiert das Objekt in ein speicherbares Dictionary."""
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"level": self.level,
|
||||||
|
"punkte": self.punkte,
|
||||||
|
"inventar": self.inventar,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, daten: Dict[str, Any]) -> "Spieler":
|
||||||
|
"""Erstellt ein neues Spieler-Objekt aus einem Dictionary."""
|
||||||
|
return cls(
|
||||||
|
name=daten["name"],
|
||||||
|
level=int(daten["level"]),
|
||||||
|
punkte=int(daten["punkte"]),
|
||||||
|
inventar=list(daten.get("inventar", [])),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Spieler(name='{self.name}', level={self.level}, punkte={self.punkte}, inventar={self.inventar})"
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, Spieler):
|
||||||
|
return False
|
||||||
|
return (
|
||||||
|
self.name == other.name
|
||||||
|
and self.level == other.level
|
||||||
|
and self.punkte == other.punkte
|
||||||
|
and self.inventar == other.inventar
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 2. Klasse Spielstand mit JSON & CSV Persistenz
|
||||||
|
# ==============================================================================
|
||||||
|
class Spielstand:
|
||||||
|
def __init__(self, titel: str = "Neuer Spielstand"):
|
||||||
|
"""Initialisiert einen Spielstand mit Titel und leerer Spielerliste."""
|
||||||
|
self.titel: str = titel
|
||||||
|
self.spieler_liste: List[Spieler] = []
|
||||||
|
|
||||||
|
def spieler_hinzufuegen(self, spieler: Spieler) -> None:
|
||||||
|
"""Fügt einen Spieler hinzu."""
|
||||||
|
self.spieler_liste.append(spieler)
|
||||||
|
|
||||||
|
def spieler_suchen(self, name: str) -> Optional[Spieler]:
|
||||||
|
"""Findet einen Spieler anhand seines Namens."""
|
||||||
|
for s in self.spieler_liste:
|
||||||
|
if s.name == name:
|
||||||
|
return s
|
||||||
|
return None
|
||||||
|
|
||||||
|
def bester_spieler(self) -> Optional[Spieler]:
|
||||||
|
"""Gibt den Spieler mit der höchsten Punktzahl zurück."""
|
||||||
|
if not self.spieler_liste:
|
||||||
|
return None
|
||||||
|
return max(self.spieler_liste, key=lambda s: s.punkte)
|
||||||
|
|
||||||
|
def speichern_als_json(self, dateipfad: str | Path) -> None:
|
||||||
|
"""Speichert den gesamten Zustand als JSON auf der Festplatte."""
|
||||||
|
pfad = Path(dateipfad)
|
||||||
|
daten = {
|
||||||
|
"titel": self.titel,
|
||||||
|
"spieler": [s.to_dict() for s in self.spieler_liste],
|
||||||
|
}
|
||||||
|
with open(pfad, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(daten, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
def laden_aus_json(self, dateipfad: str | Path) -> None:
|
||||||
|
"""Lädt den Spielstand aus einer JSON-Datei."""
|
||||||
|
pfad = Path(dateipfad)
|
||||||
|
with open(pfad, "r", encoding="utf-8") as f:
|
||||||
|
daten = json.load(f)
|
||||||
|
|
||||||
|
self.titel = daten.get("titel", self.titel)
|
||||||
|
spieler_rohdaten = daten.get("spieler", [])
|
||||||
|
self.spieler_liste = [Spieler.from_dict(d) for d in spieler_rohdaten]
|
||||||
|
|
||||||
|
def exportiere_als_csv(self, dateipfad: str | Path) -> None:
|
||||||
|
"""Exportiert eine tabellarische Zusammenfassung als CSV-Datei."""
|
||||||
|
pfad = Path(dateipfad)
|
||||||
|
feldnamen = ["name", "level", "punkte", "inventar_anzahl"]
|
||||||
|
|
||||||
|
with open(pfad, "w", newline="", encoding="utf-8") as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=feldnamen)
|
||||||
|
writer.writeheader()
|
||||||
|
for s in self.spieler_liste:
|
||||||
|
writer.writerow({
|
||||||
|
"name": s.name,
|
||||||
|
"level": s.level,
|
||||||
|
"punkte": s.punkte,
|
||||||
|
"inventar_anzahl": len(s.inventar),
|
||||||
|
})
|
||||||
|
|
||||||
|
def importiere_aus_csv(self, dateipfad: str | Path) -> int:
|
||||||
|
"""Importiert Spieler aus einer CSV-Datei."""
|
||||||
|
pfad = Path(dateipfad)
|
||||||
|
importierte_anzahl = 0
|
||||||
|
|
||||||
|
with open(pfad, "r", newline="", encoding="utf-8") as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for zeile in reader:
|
||||||
|
neuer_spieler = Spieler(
|
||||||
|
name=zeile["name"],
|
||||||
|
level=int(zeile["level"]),
|
||||||
|
punkte=int(zeile["punkte"]),
|
||||||
|
inventar=[]
|
||||||
|
)
|
||||||
|
self.spieler_hinzufuegen(neuer_spieler)
|
||||||
|
importierte_anzahl += 1
|
||||||
|
|
||||||
|
return importierte_anzahl
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
spielstand = Spielstand("Musterloesung Spielstand")
|
||||||
|
spielstand.spieler_hinzufuegen(Spieler("Gandalf", 99, 99999, ["Stab", "Ring"]))
|
||||||
|
spielstand.speichern_als_json("test_muster.json")
|
||||||
|
print("Erfolgreich ausgeführt!")
|
||||||
144
13_persistenz_json_und_csv/test_aufgabe.py
Normal file
144
13_persistenz_json_und_csv/test_aufgabe.py
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import csv
|
||||||
|
import tempfile
|
||||||
|
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 TestKapitel13(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.Spieler = getattr(aufgabe, "Spieler", None)
|
||||||
|
self.Spielstand = getattr(aufgabe, "Spielstand", None)
|
||||||
|
|
||||||
|
self.assertIsNotNone(self.Spieler, "Klasse 'Spieler' nicht gefunden!")
|
||||||
|
self.assertIsNotNone(self.Spielstand, "Klasse 'Spielstand' nicht gefunden!")
|
||||||
|
|
||||||
|
def test_01_spieler_init_und_serialisierung(self):
|
||||||
|
"""Prüft Spieler-Initialisierung, to_dict() und from_dict()."""
|
||||||
|
s = self.Spieler("Geralt", 20, 8500, ["Silberschwert", "Trank"])
|
||||||
|
self.assertEqual(s.name, "Geralt")
|
||||||
|
self.assertEqual(s.level, 20)
|
||||||
|
self.assertEqual(s.punkte, 8500)
|
||||||
|
self.assertEqual(s.inventar, ["Silberschwert", "Trank"])
|
||||||
|
|
||||||
|
# to_dict
|
||||||
|
d = s.to_dict()
|
||||||
|
self.assertIsInstance(d, dict, "to_dict() muss ein Dictionary zurückgeben!")
|
||||||
|
self.assertEqual(d["name"], "Geralt")
|
||||||
|
self.assertEqual(d["level"], 20)
|
||||||
|
self.assertEqual(d["punkte"], 8500)
|
||||||
|
self.assertEqual(d["inventar"], ["Silberschwert", "Trank"])
|
||||||
|
|
||||||
|
# from_dict
|
||||||
|
reconstructed = self.Spieler.from_dict(d)
|
||||||
|
self.assertEqual(reconstructed.name, s.name)
|
||||||
|
self.assertEqual(reconstructed.level, s.level)
|
||||||
|
self.assertEqual(reconstructed.punkte, s.punkte)
|
||||||
|
self.assertEqual(reconstructed.inventar, s.inventar)
|
||||||
|
|
||||||
|
def test_02_spielstand_management(self):
|
||||||
|
"""Prüft Hinzufügen, Suchen und Ermittlung des besten Spielers."""
|
||||||
|
spielstand = self.Spielstand("Turnier 2026")
|
||||||
|
self.assertIsNone(spielstand.bester_spieler())
|
||||||
|
|
||||||
|
s1 = self.Spieler("Alice", 5, 1200)
|
||||||
|
s2 = self.Spieler("Bob", 8, 3400)
|
||||||
|
s3 = self.Spieler("Charlie", 6, 2100)
|
||||||
|
|
||||||
|
spielstand.spieler_hinzufuegen(s1)
|
||||||
|
spielstand.spieler_hinzufuegen(s2)
|
||||||
|
spielstand.spieler_hinzufuegen(s3)
|
||||||
|
|
||||||
|
self.assertEqual(len(spielstand.spieler_liste), 3)
|
||||||
|
self.assertEqual(spielstand.bester_spieler().name, "Bob")
|
||||||
|
self.assertEqual(spielstand.spieler_suchen("Alice").level, 5)
|
||||||
|
self.assertIsNone(spielstand.spieler_suchen("Unbekannt"))
|
||||||
|
|
||||||
|
def test_03_json_speichern_und_laden(self):
|
||||||
|
"""Prüft Speichern und Laden eines Spielstands im JSON-Format über temporäre Dateien."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
json_file = Path(tmpdir) / "savegame.json"
|
||||||
|
|
||||||
|
original_stand = self.Spielstand("Episches RPG")
|
||||||
|
original_stand.spieler_hinzufuegen(self.Spieler("Zelda", 30, 9999, ["Bogen", "Rubin"]))
|
||||||
|
original_stand.spieler_hinzufuegen(self.Spieler("Link", 25, 7500, ["Master-Schwert"]))
|
||||||
|
|
||||||
|
# Speichern
|
||||||
|
original_stand.speichern_als_json(json_file)
|
||||||
|
self.assertTrue(json_file.exists(), "JSON-Datei wurde nicht erstellt!")
|
||||||
|
|
||||||
|
# Rohinhalt der JSON prüfen
|
||||||
|
with open(json_file, "r", encoding="utf-8") as f:
|
||||||
|
rohdialog = json.load(f)
|
||||||
|
self.assertEqual(rohdialog["titel"], "Episches RPG")
|
||||||
|
self.assertEqual(len(rohdialog["spieler"]), 2)
|
||||||
|
self.assertEqual(rohdialog["spieler"][0]["name"], "Zelda")
|
||||||
|
|
||||||
|
# In neues Spielstand-Objekt laden
|
||||||
|
geladener_stand = self.Spielstand()
|
||||||
|
geladener_stand.laden_aus_json(json_file)
|
||||||
|
|
||||||
|
self.assertEqual(geladener_stand.titel, "Episches RPG")
|
||||||
|
self.assertEqual(len(geladener_stand.spieler_liste), 2)
|
||||||
|
self.assertEqual(geladener_stand.spieler_liste[0].name, "Zelda")
|
||||||
|
self.assertEqual(geladener_stand.spieler_liste[0].inventar, ["Bogen", "Rubin"])
|
||||||
|
self.assertEqual(geladener_stand.spieler_liste[1].punkte, 7500)
|
||||||
|
|
||||||
|
def test_04_csv_export(self):
|
||||||
|
"""Prüft den Export der Spieler-Tabelle als CSV-Datei."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
csv_file = Path(tmpdir) / "export.csv"
|
||||||
|
|
||||||
|
stand = self.Spielstand("Liga")
|
||||||
|
stand.spieler_hinzufuegen(self.Spieler("Mario", 10, 5000, ["Pilz", "Stern", "Blume"]))
|
||||||
|
stand.spieler_hinzufuegen(self.Spieler("Luigi", 8, 3200, ["Pilz"]))
|
||||||
|
|
||||||
|
stand.exportiere_als_csv(csv_file)
|
||||||
|
self.assertTrue(csv_file.exists(), "CSV-Datei wurde nicht erstellt!")
|
||||||
|
|
||||||
|
with open(csv_file, "r", newline="", encoding="utf-8") as f:
|
||||||
|
reader = list(csv.DictReader(f))
|
||||||
|
|
||||||
|
self.assertEqual(len(reader), 2)
|
||||||
|
self.assertEqual(reader[0]["name"], "Mario")
|
||||||
|
self.assertEqual(reader[0]["level"], "10")
|
||||||
|
self.assertEqual(reader[0]["punkte"], "5000")
|
||||||
|
self.assertEqual(reader[0]["inventar_anzahl"], "3")
|
||||||
|
|
||||||
|
self.assertEqual(reader[1]["name"], "Luigi")
|
||||||
|
self.assertEqual(reader[1]["inventar_anzahl"], "1")
|
||||||
|
|
||||||
|
def test_05_csv_import(self):
|
||||||
|
"""Prüft den Import von Daten aus einer CSV-Datei."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
csv_file = Path(tmpdir) / "import_test.csv"
|
||||||
|
|
||||||
|
# Manuell CSV-Datei vorbereiten
|
||||||
|
with open(csv_file, "w", newline="", encoding="utf-8") as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=["name", "level", "punkte", "inventar_anzahl"])
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerow({"name": "Peach", "level": "12", "punkte": "4400", "inventar_anzahl": "0"})
|
||||||
|
writer.writerow({"name": "Toad", "level": "5", "punkte": "1500", "inventar_anzahl": "0"})
|
||||||
|
|
||||||
|
stand = self.Spielstand("Neue Runde")
|
||||||
|
anzahl = stand.importiere_aus_csv(csv_file)
|
||||||
|
|
||||||
|
self.assertEqual(anzahl, 2)
|
||||||
|
self.assertEqual(len(stand.spieler_liste), 2)
|
||||||
|
self.assertEqual(stand.spieler_liste[0].name, "Peach")
|
||||||
|
self.assertEqual(stand.spieler_liste[0].level, 12)
|
||||||
|
self.assertEqual(stand.spieler_liste[0].punkte, 4400)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
126
14_gui_mit_tkinter/README.md
Normal file
126
14_gui_mit_tkinter/README.md
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
# Kapitel 14: Desktop-GUIs mit Tkinter & OOP 🖥️✨
|
||||||
|
|
||||||
|
Bisher liefen alle unsere Python-Programme im Terminal (der Konsole) ab: Text eingeben mit `input()`, Text ausgeben mit `print()`. Das ist super für den Einstieg, aber echte moderne Desktop-Programme haben grafische Benutzeroberflächen (**GUI = Graphical User Interface**) mit Fenstern, Buttons, Eingabefeldern und Maus-Klicks!
|
||||||
|
|
||||||
|
In diesem Kapitel lernst du, wie du mit Pythons integriertem Modul **`tkinter`** und **Objektorientierter Programmierung (OOP)** professionelle Desktop-Programme baust.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📺 1. Vom Schwarz-Weiß-Fernseher zum Touchscreen (Event-Driven)
|
||||||
|
|
||||||
|
| Terminal (Konsole) | Grafische Oberfläche (Tkinter GUI) |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **Linearer Ablauf:** Schritt 1 → Schritt 2 → `input()` → Ende | **Ereignisgesteuert (Event-Driven):** Das Fenster wartet in einer Dauerschleife (`mainloop`) auf Benutzeraktionen |
|
||||||
|
| Nur reiner Text | Buttons, Eingabefelder, Farben, Fenster |
|
||||||
|
| Programm beendet sich nach Ausführung | Programm bleibt geöffnet, bis das Fenster geschlossen wird |
|
||||||
|
|
||||||
|
### Der Event-Loop (`root.mainloop()`):
|
||||||
|
Ein GUI-Programm läuft nicht einfach von oben nach unten durch. Sobald das Fenster aufgebaut ist, übergibt Python die Kontrolle an den **Event-Loop**:
|
||||||
|
```python
|
||||||
|
root.mainloop() # Wartet geduldig auf Klicks, Tastaturanschläge oder Fenster-Schließen
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧱 2. Die wichtigsten Tkinter-Widgets
|
||||||
|
|
||||||
|
Ein **Widget** (Window Gadget) ist ein grafisches Bauelement:
|
||||||
|
|
||||||
|
| Widget | Klasse | Zweck | Wichtigste Optionen / Methoden |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **Hauptfenster** | `tk.Tk()` | Das Betriebssystem-Fenster | `.title("Titel")`, `.geometry("400x300")`, `.mainloop()` |
|
||||||
|
| **Textanzeige** | `tk.Label` | Zeigt Text oder Zahlen an | `text="Hallo"`, `font=("Arial", 14)`, `fg="blue"`, `bg="white"` |
|
||||||
|
| **Schaltfläche** | `tk.Button` | Klickbarer Knopf | `text="Klick"`, `command=self.methode` |
|
||||||
|
| **Eingabefeld** | `tk.Entry` | Einzeilige Texteingabe | `.get()`, `.insert(0, "Start")`, `.delete(0, "end")` |
|
||||||
|
| **Container** | `tk.Frame` | Gruppiert mehrere Widgets | Hilft beim sauberen Verschachteln von Layouts |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📐 3. Das Grid-Layout-System (Raster)
|
||||||
|
|
||||||
|
Um Elemente auf dem Bildschirm auszurichten, nutzen wir den **Grid-Layout-Manager**. Er funktioniert wie eine Tabelle oder ein Schachbrett:
|
||||||
|
|
||||||
|
```
|
||||||
|
Spalte 0 Spalte 1
|
||||||
|
┌──────────────────────┬──────────────────────┐
|
||||||
|
Zeile 0 │ 🔢 Überschrift (columnspan=2) │
|
||||||
|
├──────────────────────┼──────────────────────┤
|
||||||
|
Zeile 1 │ Große Zähleranzeige: "0" (columnspan=2) │
|
||||||
|
├──────────────────────┼──────────────────────┤
|
||||||
|
Zeile 2 │ Status: "Klicks: 0" (columnspan=2) │
|
||||||
|
├──────────────────────┬──────────────────────┤
|
||||||
|
Zeile 3 │ ➕ Erhöhen │ ➖ Verringern │
|
||||||
|
├──────────────────────┴──────────────────────┤
|
||||||
|
Zeile 4 │ ↺ Zurücksetzen (columnspan=2) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Die wichtigsten Parameter bei `.grid()`:
|
||||||
|
- `row=0, column=1`: Bestimmt Zeile und Spalte.
|
||||||
|
- `columnspan=2`: Lässt das Element über 2 Spalten spannen.
|
||||||
|
- `padx=10, pady=5`: Äußerer Abstand (Padding) in Pixeln.
|
||||||
|
- `sticky="ew"`: Dehnt das Element nach Osten und Westen (links & rechts) aus.
|
||||||
|
|
||||||
|
> ⚠️ **Goldene Regel:** Mische niemals `.pack()` und `.grid()` im selben Eltern-Container! Verwende innerhalb eines Fensters oder Frames konsistent einen Layout-Manager.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 4. Event-Driven Programming: Was passiert beim Klick?
|
||||||
|
|
||||||
|
Wenn ein Button geklickt wird, führt Tkinter die bei `command` hinterlegte Methode aus:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# RICHTIG: Referenz auf die Methode übergeben (ohne Klammern!)
|
||||||
|
self.btn_plus = tk.Button(self.root, text="+", command=self.klick_plus)
|
||||||
|
|
||||||
|
# FALSCH: Würde die Methode SOFORT beim Start aufrufen, statt erst beim Klick!
|
||||||
|
self.btn_plus = tk.Button(self.root, text="+", command=self.klick_plus())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏛️ 5. Clean Architecture: Model-View-Controller (MVC)
|
||||||
|
|
||||||
|
Warum packen wir nicht alles in eine einzige Datei/Funktion? Weil professioneller Code modular und automatisiert testbar sein muss!
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────┐ ┌────────────────────────────────┐
|
||||||
|
│ Model (ZaehlerLogik) │ │ View/Controller (ZaehlerApp) │
|
||||||
|
│ - wert: int │ │ - root: tk.Tk │
|
||||||
|
│ - schrittweite: int │ ◄────── │ - label_anzeige: tk.Label │
|
||||||
|
│ - erhoehen() -> int │ │ - btn_plus: tk.Button │
|
||||||
|
│ - verringern() -> int │ │ - klick_plus() │
|
||||||
|
│ (100% reines Python, keine GUI) │ │ (Tkinter-Widgets & Ereignisse)│
|
||||||
|
└────────────────────────────────┘ └────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **`ZaehlerLogik` (Model):** Verwaltet die Daten (Zahlen, Grenzen, Schrittweiten). Es weiß nichts von Fenstern oder Buttons und kann in Unittests in Millisekunden geprüft werden.
|
||||||
|
2. **`ZaehlerApp` (View & Controller):** Erstellt die Oberfläche und ruft bei Klicks die Methoden von `self.logik` auf.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 6. Deine Aufgaben in `aufgabe.py`
|
||||||
|
|
||||||
|
Öffne `14_gui_mit_tkinter/aufgabe.py` und vervollständige die Klassen:
|
||||||
|
|
||||||
|
1. **TODO 1:** `ZaehlerLogik.__init__` mit `startwert`, `min_wert`, `max_wert`, `schrittweite=1`, `klick_anzahl=0`.
|
||||||
|
2. **TODO 2:** `erhoehen()`, `verringern()` (mit Clamping an Min/Max) und `zuruecksetzen()`.
|
||||||
|
3. **TODO 3:** `setze_schrittweite()`, `ist_gerade()` und `__str__()`.
|
||||||
|
4. **TODO 4:** `ZaehlerApp.__init__` (Fenster-Konfiguration, Widgets anlegen und im Grid platzieren).
|
||||||
|
5. **TODO 5:** Event-Handler `klick_plus()`, `klick_minus()`, `klick_reset()`, `klick_schritt_setzen()`.
|
||||||
|
6. **TODO 6:** `aktualisiere_anzeige()` (Labels synchronisieren und farblich hervorheben).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 7. Testen & Ausführen
|
||||||
|
|
||||||
|
### Automatische Tests ausführen:
|
||||||
|
```bash
|
||||||
|
python3 test_aufgabe.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Startbare grafische Applikation ausprobieren:
|
||||||
|
```bash
|
||||||
|
python3 gui_app.py
|
||||||
|
```
|
||||||
|
*(Hinweis: Die grafische Oberfläche benötigt eine Desktop-Umgebung mit Display)*
|
||||||
303
14_gui_mit_tkinter/aufgabe.py
Normal file
303
14_gui_mit_tkinter/aufgabe.py
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
"""
|
||||||
|
Kapitel 14: Desktop-GUIs mit Tkinter & OOP 🖥️🔢
|
||||||
|
=================================================
|
||||||
|
Aufgabe: Erstelle eine modulare Klick-Zähler- & Rechner-Applikation
|
||||||
|
mit sauberer Trennung von Logik (Model) und Benutzeroberfläche (View/Controller).
|
||||||
|
|
||||||
|
1. ZaehlerLogik: Verwaltet den Wert, Grenzwerte und Schrittweiten (Headless / Reines Python).
|
||||||
|
2. ZaehlerApp: Erzeugt das Tkinter-Fenster, platziert Buttons & Labels und fängt Klicks ab.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Tkinter Import mit Fallback für Headless- / Server-Umgebungen
|
||||||
|
# ==============================================================================
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
except ImportError:
|
||||||
|
# Headless Mock-Objekte (ermöglicht Ausführung und Tests auch ohne GUI-Server)
|
||||||
|
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))
|
||||||
|
|
||||||
|
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 delete(self, first=0, last=None):
|
||||||
|
self._entry_text = ""
|
||||||
|
|
||||||
|
def insert(self, index, string):
|
||||||
|
self._entry_text = str(string)
|
||||||
|
|
||||||
|
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 _MockTkModule:
|
||||||
|
Tk = _MockTk
|
||||||
|
Label = _MockWidget
|
||||||
|
Button = _MockWidget
|
||||||
|
Entry = _MockWidget
|
||||||
|
Frame = _MockWidget
|
||||||
|
|
||||||
|
tk = _MockTkModule()
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# TEIL 1: DIE LOGIK-KLASSE (MODEL)
|
||||||
|
# ==============================================================================
|
||||||
|
class ZaehlerLogik:
|
||||||
|
"""
|
||||||
|
Das Datenmodell (Model) des Klick-Zählers.
|
||||||
|
Verwaltet den aktuellen Zählerstand, Grenzwerte und Schrittweiten.
|
||||||
|
Komplett unabhängig von Tkinter (reines Python).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 1: Konstruktor __init__
|
||||||
|
# Parameter:
|
||||||
|
# - startwert (int, Standard: 0)
|
||||||
|
# - min_wert (int, Standard: -100)
|
||||||
|
# - max_wert (int, Standard: 100)
|
||||||
|
# Attribute initialisieren:
|
||||||
|
# - self.wert: int = startwert
|
||||||
|
# - self.min_wert: int = min_wert
|
||||||
|
# - self.max_wert: int = max_wert
|
||||||
|
# - self.schrittweite: int = 1
|
||||||
|
# - self.klick_anzahl: int = 0
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(self, startwert: int = 0, min_wert: int = -100, max_wert: int = 100):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 2a: Methode erhoehen(self) -> int
|
||||||
|
# 1. Erhöhe self.klick_anzahl um 1.
|
||||||
|
# 2. Addiere self.schrittweite zu self.wert, aber maximal bis self.max_wert!
|
||||||
|
# Tipp: min(self.max_wert, self.wert + self.schrittweite)
|
||||||
|
# 3. Gib den neuen self.wert zurück.
|
||||||
|
# ==========================================================================
|
||||||
|
def erhoehen(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 2b: Methode verringern(self) -> int
|
||||||
|
# 1. Erhöhe self.klick_anzahl um 1.
|
||||||
|
# 2. Subtrahiere self.schrittweite von self.wert, aber minimal bis self.min_wert!
|
||||||
|
# Tipp: max(self.min_wert, self.wert - self.schrittweite)
|
||||||
|
# 3. Gib den neuen self.wert zurück.
|
||||||
|
# ==========================================================================
|
||||||
|
def verringern(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 2c: Methode zuruecksetzen(self) -> int
|
||||||
|
# 1. Erhöhe self.klick_anzahl um 1.
|
||||||
|
# 2. Setze self.wert auf 0 zurück.
|
||||||
|
# 3. Gib den neuen self.wert (0) zurück.
|
||||||
|
# ==========================================================================
|
||||||
|
def zuruecksetzen(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 3a: Methode setze_schrittweite(self, schritt: int) -> bool
|
||||||
|
# Wenn schritt > 0 ist:
|
||||||
|
# - Setze self.schrittweite = schritt
|
||||||
|
# - Gib True zurück
|
||||||
|
# Wenn schritt <= 0 ist:
|
||||||
|
# - Verändere die Schrittweite nicht und gib False zurück
|
||||||
|
# ==========================================================================
|
||||||
|
def setze_schrittweite(self, schritt: int) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 3b: Methode ist_gerade(self) -> bool
|
||||||
|
# Gibt True zurück, wenn self.wert eine gerade Zahl ist (wert % 2 == 0),
|
||||||
|
# andernfalls False.
|
||||||
|
# ==========================================================================
|
||||||
|
def ist_gerade(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 3c: Dunder-Methode __str__(self) -> str
|
||||||
|
# Gibt einen formatierten String zurück:
|
||||||
|
# "Zählerstand: <wert> (Schrittweite: <schrittweite>, Klicks: <klick_anzahl>)"
|
||||||
|
# ==========================================================================
|
||||||
|
def __str__(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# TEIL 2: DIE GUI-KLASSE (VIEW & CONTROLLER)
|
||||||
|
# ==============================================================================
|
||||||
|
class ZaehlerApp:
|
||||||
|
"""
|
||||||
|
Die Benutzeroberfläche (GUI) für den Klick-Zähler mit Tkinter.
|
||||||
|
Erstellt Fenster, Labels, Buttons und Eingabefelder.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 4: Konstruktor __init__(self, root: tk.Tk)
|
||||||
|
# Parameter: root (das Tkinter-Hauptfenster)
|
||||||
|
#
|
||||||
|
# 1. Speichere das Fenster:
|
||||||
|
# self.root = root
|
||||||
|
# self.root.title("Python OOP Zähler 🔢")
|
||||||
|
# self.root.geometry("380x360")
|
||||||
|
#
|
||||||
|
# 2. Erstelle eine Instanz der Logik-Klasse:
|
||||||
|
# self.logik = ZaehlerLogik(startwert=0, min_wert=-100, max_wert=100)
|
||||||
|
#
|
||||||
|
# 3. Erstelle und platziere alle Widgets:
|
||||||
|
# - self.label_titel: tk.Label(self.root, text="🔢 Python Klick-Zähler", font=("Arial", 16, "bold"))
|
||||||
|
# Platzierung mit .grid(row=0, column=0, columnspan=2, padx=15, pady=(15, 5))
|
||||||
|
#
|
||||||
|
# - self.label_anzeige: tk.Label(self.root, text="0", font=("Arial", 36, "bold"))
|
||||||
|
# Platzierung mit .grid(row=1, column=0, columnspan=2, padx=15, pady=10)
|
||||||
|
#
|
||||||
|
# - self.label_info: tk.Label(self.root, text="Klicks: 0 | Gerade Zahl", font=("Arial", 10))
|
||||||
|
# Platzierung mit .grid(row=2, column=0, columnspan=2, padx=15, pady=(0, 15))
|
||||||
|
#
|
||||||
|
# - self.btn_plus: tk.Button(self.root, text="➕ Erhöhen", font=("Arial", 11, "bold"), command=self.klick_plus)
|
||||||
|
# Platzierung mit .grid(row=3, column=0, padx=10, pady=5, sticky="ew")
|
||||||
|
#
|
||||||
|
# - self.btn_minus: tk.Button(self.root, text="➖ Verringern", font=("Arial", 11, "bold"), command=self.klick_minus)
|
||||||
|
# Platzierung mit .grid(row=3, column=1, padx=10, pady=5, sticky="ew")
|
||||||
|
#
|
||||||
|
# - self.btn_reset: tk.Button(self.root, text="↺ Zurücksetzen", font=("Arial", 10), command=self.klick_reset)
|
||||||
|
# Platzierung mit .grid(row=4, column=0, columnspan=2, padx=10, pady=5, sticky="ew")
|
||||||
|
#
|
||||||
|
# - Container für Schrittweite:
|
||||||
|
# self.frame_schritt = tk.Frame(self.root)
|
||||||
|
# self.frame_schritt.grid(row=5, column=0, columnspan=2, padx=10, pady=(15, 10))
|
||||||
|
#
|
||||||
|
# self.label_schritt = tk.Label(self.frame_schritt, text="Schrittweite:")
|
||||||
|
# self.label_schritt.pack(side="left", padx=5)
|
||||||
|
#
|
||||||
|
# self.entry_schritt = tk.Entry(self.frame_schritt, width=6, font=("Arial", 10))
|
||||||
|
# self.entry_schritt.insert(0, "1")
|
||||||
|
# self.entry_schritt.pack(side="left", padx=5)
|
||||||
|
#
|
||||||
|
# self.btn_schritt = tk.Button(self.frame_schritt, text="Setzen", command=self.klick_schritt_setzen)
|
||||||
|
# self.btn_schritt.pack(side="left", padx=5)
|
||||||
|
#
|
||||||
|
# 4. Rufe am Ende self.aktualisiere_anzeige() auf, um die Startwerte anzuzeigen!
|
||||||
|
# ==========================================================================
|
||||||
|
def __init__(self, root: tk.Tk):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 5a: Methode klick_plus(self) -> None
|
||||||
|
# 1. Rufe self.logik.erhoehen() auf.
|
||||||
|
# 2. Rufe self.aktualisiere_anzeige() auf.
|
||||||
|
# ==========================================================================
|
||||||
|
def klick_plus(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 5b: Methode klick_minus(self) -> None
|
||||||
|
# 1. Rufe self.logik.verringern() auf.
|
||||||
|
# 2. Rufe self.aktualisiere_anzeige() auf.
|
||||||
|
# ==========================================================================
|
||||||
|
def klick_minus(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 5c: Methode klick_reset(self) -> None
|
||||||
|
# 1. Rufe self.logik.zuruecksetzen() auf.
|
||||||
|
# 2. Rufe self.aktualisiere_anzeige() auf.
|
||||||
|
# ==========================================================================
|
||||||
|
def klick_reset(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 5d: Methode klick_schritt_setzen(self) -> None
|
||||||
|
# 1. Hole den Text aus self.entry_schritt mit self.entry_schritt.get().strip()
|
||||||
|
# 2. Versuche (try-except) den Text mit int() in eine Ganzzahl umzuwandeln.
|
||||||
|
# 3. Wenn die Umwandlung klappt:
|
||||||
|
# - Rufe self.logik.setze_schrittweite(neuer_wert) auf.
|
||||||
|
# - Wenn setze_schrittweite True liefert: self.aktualisiere_anzeige() aufrufen.
|
||||||
|
# - Wenn False geliefert wird: self.label_info.config(text="⚠️ Schrittweite muss > 0 sein!")
|
||||||
|
# 4. Wenn ein ValueError auftritt (z.B. Buchstabe eingegeben):
|
||||||
|
# - Setze self.label_info.config(text="⚠️ Bitte eine ganze Zahl eingeben!")
|
||||||
|
# ==========================================================================
|
||||||
|
def klick_schritt_setzen(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# TODO 6: Methode aktualisiere_anzeige(self) -> None
|
||||||
|
# Synchronisiert die Oberfläche mit den aktuellen Werten aus self.logik:
|
||||||
|
#
|
||||||
|
# 1. Hauptanzeige:
|
||||||
|
# self.label_anzeige.config(text=str(self.logik.wert))
|
||||||
|
#
|
||||||
|
# 2. Info-Text zusammenbauen:
|
||||||
|
# paritaet = "Gerade" if self.logik.ist_gerade() else "Ungerade"
|
||||||
|
# info = f"Klicks: {self.logik.klick_anzahl} | {paritaet} | Schritt: {self.logik.schrittweite}"
|
||||||
|
# if self.logik.wert >= self.logik.max_wert:
|
||||||
|
# info += " (Maximum erreicht!)"
|
||||||
|
# elif self.logik.wert <= self.logik.min_wert:
|
||||||
|
# info += " (Minimum erreicht!)"
|
||||||
|
# self.label_info.config(text=info)
|
||||||
|
# ==========================================================================
|
||||||
|
def aktualisiere_anzeige(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Startblock: Führe dieses Skript aus (python3 aufgabe.py), um die GUI zu testen!
|
||||||
|
# ==============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("🚀 Starte Tkinter Klick-Zähler...")
|
||||||
|
try:
|
||||||
|
root = tk.Tk()
|
||||||
|
app = ZaehlerApp(root)
|
||||||
|
print("💡 Fenster geöffnet! Schließe das Fenster, um das Programm zu beenden.")
|
||||||
|
root.mainloop()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ GUI konnte nicht im Display geöffnet werden ({e}).")
|
||||||
|
print("💡 Führe 'python3 test_aufgabe.py' aus, um deine Lösung automatisch zu prüfen!")
|
||||||
314
14_gui_mit_tkinter/gui_app.py
Normal file
314
14_gui_mit_tkinter/gui_app.py
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Kapitel 14: Grafische Desktop-Anwendung (Startbares Skript) 🖥️✨
|
||||||
|
================================================================
|
||||||
|
Führe diese Datei auf deinem Desktop aus (z.B. in VS Code auf Mac/Windows/Linux):
|
||||||
|
python3 gui_app.py
|
||||||
|
|
||||||
|
Features dieser erweiterten Version:
|
||||||
|
- Saubere Model-View-Controller (MVC) Architektur
|
||||||
|
- Farb-Feedback (Grün bei positiven, Rot bei negativen Zahlen)
|
||||||
|
- Tastatur-Shortcuts:
|
||||||
|
[Pfeil nach oben] -> Erhöhen
|
||||||
|
[Pfeil nach unten] -> Verringern
|
||||||
|
[R] -> Zurücksetzen
|
||||||
|
- Warnung bei Grenzwert-Überschreitungen
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox
|
||||||
|
TK_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
TK_AVAILABLE = False
|
||||||
|
class _MockWidget:
|
||||||
|
def __init__(self, *args, **kwargs): pass
|
||||||
|
def pack(self, *args, **kwargs): pass
|
||||||
|
def grid(self, *args, **kwargs): pass
|
||||||
|
def columnconfigure(self, *args, **kwargs): pass
|
||||||
|
def bind(self, *args, **kwargs): pass
|
||||||
|
def config(self, *args, **kwargs): pass
|
||||||
|
def insert(self, *args, **kwargs): pass
|
||||||
|
def delete(self, *args, **kwargs): pass
|
||||||
|
def get(self): return "1"
|
||||||
|
|
||||||
|
class _MockTkModule:
|
||||||
|
Tk = _MockWidget
|
||||||
|
Label = _MockWidget
|
||||||
|
Button = _MockWidget
|
||||||
|
Entry = _MockWidget
|
||||||
|
Frame = _MockWidget
|
||||||
|
LabelFrame = _MockWidget
|
||||||
|
|
||||||
|
tk = _MockTkModule()
|
||||||
|
messagebox = None
|
||||||
|
|
||||||
|
|
||||||
|
class ZaehlerLogik:
|
||||||
|
"""Modell für Zähler, Grenzwerte und Schrittweite."""
|
||||||
|
|
||||||
|
def __init__(self, startwert: int = 0, min_wert: int = -100, max_wert: int = 100):
|
||||||
|
self.wert: int = startwert
|
||||||
|
self.min_wert: int = min_wert
|
||||||
|
self.max_wert: int = max_wert
|
||||||
|
self.schrittweite: int = 1
|
||||||
|
self.klick_anzahl: int = 0
|
||||||
|
|
||||||
|
def erhoehen(self) -> int:
|
||||||
|
self.klick_anzahl += 1
|
||||||
|
self.wert = min(self.max_wert, self.wert + self.schrittweite)
|
||||||
|
return self.wert
|
||||||
|
|
||||||
|
def verringern(self) -> int:
|
||||||
|
self.klick_anzahl += 1
|
||||||
|
self.wert = max(self.min_wert, self.wert - self.schrittweite)
|
||||||
|
return self.wert
|
||||||
|
|
||||||
|
def zuruecksetzen(self) -> int:
|
||||||
|
self.klick_anzahl += 1
|
||||||
|
self.wert = 0
|
||||||
|
return self.wert
|
||||||
|
|
||||||
|
def setze_schrittweite(self, schritt: int) -> bool:
|
||||||
|
if schritt > 0:
|
||||||
|
self.schrittweite = schritt
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ist_gerade(self) -> bool:
|
||||||
|
return self.wert % 2 == 0
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Zählerstand: {self.wert} (Schritt: {self.schrittweite})"
|
||||||
|
|
||||||
|
|
||||||
|
class DesktopZaehlerGUI:
|
||||||
|
"""Moderne grafische Desktop-Benutzeroberfläche mit Tkinter."""
|
||||||
|
|
||||||
|
def __init__(self, root: tk.Tk):
|
||||||
|
self.root = root
|
||||||
|
self.root.title("Python OOP • Desktop Zähler 🔢")
|
||||||
|
self.root.geometry("420x440")
|
||||||
|
self.root.minsize(360, 400)
|
||||||
|
self.root.configure(bg="#f8fafc")
|
||||||
|
|
||||||
|
self.logik = ZaehlerLogik(startwert=0, min_wert=-100, max_wert=100)
|
||||||
|
|
||||||
|
# UI Komponenten aufbauen
|
||||||
|
self._erstelle_widgets()
|
||||||
|
self._binde_tasten()
|
||||||
|
self.aktualisiere_anzeige()
|
||||||
|
|
||||||
|
def _erstelle_widgets(self):
|
||||||
|
# 1. Header Frame
|
||||||
|
frame_header = tk.Frame(self.root, bg="#4f46e5", pady=14)
|
||||||
|
frame_header.pack(fill="x")
|
||||||
|
|
||||||
|
self.label_titel = tk.Label(
|
||||||
|
frame_header,
|
||||||
|
text="🔢 Python Desktop Zähler",
|
||||||
|
font=("Helvetica", 16, "bold"),
|
||||||
|
bg="#4f46e5",
|
||||||
|
fg="white",
|
||||||
|
)
|
||||||
|
self.label_titel.pack()
|
||||||
|
|
||||||
|
self.label_sub = tk.Label(
|
||||||
|
frame_header,
|
||||||
|
text="Kapitel 14 • Tkinter & OOP",
|
||||||
|
font=("Helvetica", 9),
|
||||||
|
bg="#4f46e5",
|
||||||
|
fg="#c7d2fe",
|
||||||
|
)
|
||||||
|
self.label_sub.pack()
|
||||||
|
|
||||||
|
# 2. Haupt-Anzeige Frame
|
||||||
|
frame_card = tk.Frame(self.root, bg="#ffffff", bd=1, relief="solid", padx=20, pady=15)
|
||||||
|
frame_card.pack(fill="x", padx=25, pady=15)
|
||||||
|
|
||||||
|
self.label_anzeige = tk.Label(
|
||||||
|
frame_card,
|
||||||
|
text="0",
|
||||||
|
font=("Helvetica", 42, "bold"),
|
||||||
|
bg="#ffffff",
|
||||||
|
fg="#4f46e5",
|
||||||
|
)
|
||||||
|
self.label_anzeige.pack(pady=(5, 0))
|
||||||
|
|
||||||
|
self.label_info = tk.Label(
|
||||||
|
frame_card,
|
||||||
|
text="Klicks: 0 | Gerade Zahl",
|
||||||
|
font=("Helvetica", 10),
|
||||||
|
bg="#ffffff",
|
||||||
|
fg="#64748b",
|
||||||
|
)
|
||||||
|
self.label_info.pack(pady=(0, 5))
|
||||||
|
|
||||||
|
# 3. Button Steuerungsbereich
|
||||||
|
frame_buttons = tk.Frame(self.root, bg="#f8fafc")
|
||||||
|
frame_buttons.pack(fill="x", padx=25, pady=5)
|
||||||
|
frame_buttons.columnconfigure(0, weight=1)
|
||||||
|
frame_buttons.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
self.btn_plus = tk.Button(
|
||||||
|
frame_buttons,
|
||||||
|
text="➕ Erhöhen [↑]",
|
||||||
|
font=("Helvetica", 11, "bold"),
|
||||||
|
bg="#10b981",
|
||||||
|
fg="white",
|
||||||
|
activebackground="#059669",
|
||||||
|
activeforeground="white",
|
||||||
|
relief="flat",
|
||||||
|
pady=8,
|
||||||
|
cursor="hand2",
|
||||||
|
command=self.klick_plus,
|
||||||
|
)
|
||||||
|
self.btn_plus.grid(row=0, column=0, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
self.btn_minus = tk.Button(
|
||||||
|
frame_buttons,
|
||||||
|
text="➖ Verringern [↓]",
|
||||||
|
font=("Helvetica", 11, "bold"),
|
||||||
|
bg="#ef4444",
|
||||||
|
fg="white",
|
||||||
|
activebackground="#dc2626",
|
||||||
|
activeforeground="white",
|
||||||
|
relief="flat",
|
||||||
|
pady=8,
|
||||||
|
cursor="hand2",
|
||||||
|
command=self.klick_minus,
|
||||||
|
)
|
||||||
|
self.btn_minus.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
self.btn_reset = tk.Button(
|
||||||
|
frame_buttons,
|
||||||
|
text="↺ Zurücksetzen [R]",
|
||||||
|
font=("Helvetica", 10),
|
||||||
|
bg="#e2e8f0",
|
||||||
|
fg="#1e293b",
|
||||||
|
activebackground="#cbd5e1",
|
||||||
|
relief="flat",
|
||||||
|
pady=6,
|
||||||
|
cursor="hand2",
|
||||||
|
command=self.klick_reset,
|
||||||
|
)
|
||||||
|
self.btn_reset.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
# 4. Schrittweite Konfiguration
|
||||||
|
frame_settings = tk.LabelFrame(
|
||||||
|
self.root,
|
||||||
|
text=" ⚙️ Einstellungen ",
|
||||||
|
font=("Helvetica", 9, "bold"),
|
||||||
|
bg="#f8fafc",
|
||||||
|
fg="#475569",
|
||||||
|
padx=10,
|
||||||
|
pady=8,
|
||||||
|
)
|
||||||
|
frame_settings.pack(fill="x", padx=25, pady=10)
|
||||||
|
|
||||||
|
tk.Label(frame_settings, text="Schrittweite:", bg="#f8fafc", font=("Helvetica", 10)).pack(
|
||||||
|
side="left", padx=5
|
||||||
|
)
|
||||||
|
|
||||||
|
self.entry_schritt = tk.Entry(
|
||||||
|
frame_settings, width=6, font=("Helvetica", 11), justify="center"
|
||||||
|
)
|
||||||
|
self.entry_schritt.insert(0, "1")
|
||||||
|
self.entry_schritt.pack(side="left", padx=5)
|
||||||
|
|
||||||
|
self.btn_schritt = tk.Button(
|
||||||
|
frame_settings,
|
||||||
|
text="Übernehmen",
|
||||||
|
font=("Helvetica", 9),
|
||||||
|
bg="#6366f1",
|
||||||
|
fg="white",
|
||||||
|
relief="flat",
|
||||||
|
cursor="hand2",
|
||||||
|
command=self.klick_schritt_setzen,
|
||||||
|
)
|
||||||
|
self.btn_schritt.pack(side="left", padx=5)
|
||||||
|
|
||||||
|
def _binde_tasten(self):
|
||||||
|
"""Bindet bequeme Tastatur-Shortcuts an das Fenster."""
|
||||||
|
self.root.bind("<Up>", lambda event: self.klick_plus())
|
||||||
|
self.root.bind("<Down>", lambda event: self.klick_minus())
|
||||||
|
self.root.bind("<r>", lambda event: self.klick_reset())
|
||||||
|
self.root.bind("<R>", lambda event: self.klick_reset())
|
||||||
|
self.root.bind("<Return>", lambda event: self.klick_schritt_setzen())
|
||||||
|
|
||||||
|
def klick_plus(self) -> None:
|
||||||
|
self.logik.erhoehen()
|
||||||
|
self.aktualisiere_anzeige()
|
||||||
|
|
||||||
|
def klick_minus(self) -> None:
|
||||||
|
self.logik.verringern()
|
||||||
|
self.aktualisiere_anzeige()
|
||||||
|
|
||||||
|
def klick_reset(self) -> None:
|
||||||
|
self.logik.zuruecksetzen()
|
||||||
|
self.aktualisiere_anzeige()
|
||||||
|
|
||||||
|
def klick_schritt_setzen(self) -> None:
|
||||||
|
eingabe = self.entry_schritt.get().strip()
|
||||||
|
try:
|
||||||
|
schritt = int(eingabe)
|
||||||
|
if self.logik.setze_schrittweite(schritt):
|
||||||
|
self.aktualisiere_anzeige()
|
||||||
|
else:
|
||||||
|
self.label_info.config(
|
||||||
|
text="⚠️ Schrittweite muss größer als 0 sein!", fg="#ef4444"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
self.label_info.config(
|
||||||
|
text="⚠️ Bitte eine ganze Zahl eingeben!", fg="#ef4444"
|
||||||
|
)
|
||||||
|
|
||||||
|
def aktualisiere_anzeige(self) -> None:
|
||||||
|
# Zählerwert darstellen
|
||||||
|
self.label_anzeige.config(text=str(self.logik.wert))
|
||||||
|
|
||||||
|
# Farb-Feedback
|
||||||
|
if self.logik.wert > 0:
|
||||||
|
self.label_anzeige.config(fg="#10b981") # Grün
|
||||||
|
elif self.logik.wert < 0:
|
||||||
|
self.label_anzeige.config(fg="#ef4444") # Rot
|
||||||
|
else:
|
||||||
|
self.label_anzeige.config(fg="#4f46e5") # Neutral
|
||||||
|
|
||||||
|
# Info-Text
|
||||||
|
paritaet = "Gerade" if self.logik.ist_gerade() else "Ungerade"
|
||||||
|
info = f"Klicks: {self.logik.klick_anzahl} | {paritaet} | Schritt: {self.logik.schrittweite}"
|
||||||
|
|
||||||
|
if self.logik.wert >= self.logik.max_wert:
|
||||||
|
info += " ⚠️ Max erreicht!"
|
||||||
|
elif self.logik.wert <= self.logik.min_wert:
|
||||||
|
info += " ⚠️ Min erreicht!"
|
||||||
|
|
||||||
|
self.label_info.config(text=info, fg="#64748b")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not TK_AVAILABLE:
|
||||||
|
print("=" * 60)
|
||||||
|
print("⚠️ Hinweis: Tkinter ist in dieser Linux-Umgebung nicht installiert.")
|
||||||
|
print("Um die grafische Oberfläche mit echtem Fenster zu sehen:")
|
||||||
|
print(" 1. Klone oder öffne diesen Ordner auf deinem PC/Laptop (Windows/Mac/Linux).")
|
||||||
|
print(" 2. Führe 'python3 gui_app.py' aus!")
|
||||||
|
print("=" * 60)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
root = tk.Tk()
|
||||||
|
app = DesktopZaehlerGUI(root)
|
||||||
|
print("🖥️ GUI-Fenster gestartet! Schließe das Fenster zum Beenden.")
|
||||||
|
root.mainloop()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Konnte GUI nicht initialisieren (kein X11-Display gefunden): {e}")
|
||||||
|
print("💡 Teste deinen Code headless mit: python3 test_aufgabe.py")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
351
14_gui_mit_tkinter/index.html
Normal file
351
14_gui_mit_tkinter/index.html
Normal file
|
|
@ -0,0 +1,351 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kapitel 14: Desktop-GUIs mit Tkinter & OOP</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="../13_persistenz_json_und_csv/index.html">Kapitel 13</a>
|
||||||
|
<span>›</span>
|
||||||
|
<span style="color: white; font-weight: 600;">Kapitel 14</span>
|
||||||
|
</nav>
|
||||||
|
<div class="header-badge">Kapitel 14 • Schulabgleich 27.0 Tkinter</div>
|
||||||
|
<h1>Desktop-GUIs mit Tkinter & OOP 🖥️✨</h1>
|
||||||
|
<p>Vom Text-Terminal zum echten Betriebssystem-Fenster mit Buttons, Eingabefeldern, Layout-Grids und ereignisgesteuerter OOP-Architektur.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
|
||||||
|
<!-- 1. Das Konzept: Vom Terminal zum Touchscreen -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>1. Vom Terminal zur grafischen Oberfläche (GUI)</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Bisher liefen all deine Python-Programme linear im schwarzen Terminal ab: Text einlesen mit <code>input()</code>, Text ausgeben mit <code>print()</code>.
|
||||||
|
Echte Desktop-Software (wie Spotify, VS Code oder ein Taschenrechner) funktioniert jedoch völlig anders: Sie besitzt ein <strong>Fenster</strong>, das geduldig auf <strong>Mausklicks</strong> und <strong>Tastatureingaben</strong> wartet.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">📺 Die Fernseh-Analogie: Vom Schwarz-Weiß-Fernseher zum Touchscreen</div>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Terminal (Linear):</strong> Wie ein alter Filmstreifen. Bild für Bild läuft starr von oben nach unten ab. Am Ende ist der Film vorbei.</li>
|
||||||
|
<li><strong>GUI (Ereignisgesteuert / Event-Driven):</strong> Wie ein interaktiver Touchscreen. Das System schläft im Hintergrund (im sogenannten <strong>Event-Loop</strong>). Sobald du auf einen Knopf drückst, feuert ein Signal und deine Methode reagiert blitzschnell darauf!</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Der Herzschlag einer GUI: Der Event-Loop</h3>
|
||||||
|
<p>
|
||||||
|
Ein GUI-Programm beendet sich nicht sofort, sondern übergibt die Kontrolle an <code>root.mainloop()</code>:
|
||||||
|
</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 (Minimales Tkinter-Fenster)</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">import</span> <span class="var">tkinter</span> <span class="kw">as</span> <span class="var">tk</span>
|
||||||
|
|
||||||
|
<span class="var">root</span> = <span class="var">tk</span>.<span class="fn">Tk</span>() <span class="cm"># 1. Hauptfenster erzeugen</span>
|
||||||
|
<span class="var">root</span>.<span class="fn">title</span>(<span class="str">"Mein erstes GUI-Fenster"</span>) <span class="cm"># 2. Fenstertitel festlegen</span>
|
||||||
|
<span class="var">root</span>.<span class="fn">geometry</span>(<span class="str">"400x300"</span>) <span class="cm"># 3. Fenstergröße (Breite x Höhe)</span>
|
||||||
|
|
||||||
|
<span class="var">label</span> = <span class="var">tk</span>.<span class="fn">Label</span>(<span class="var">root</span>, <span class="var">text</span>=<span class="str">"Hallo Welt! 👋"</span>, <span class="var">font</span>=(<span class="str">"Arial"</span>, <span class="num">16</span>))
|
||||||
|
<span class="var">label</span>.<span class="fn">pack</span>(<span class="var">pady</span>=<span class="num">20</span>) <span class="cm"># 4. Widget im Fenster platzieren</span>
|
||||||
|
|
||||||
|
<span class="var">root</span>.<span class="fn">mainloop</span>() <span class="cm"># 5. Ereignisschleife starten (wartet auf Events)</span></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. Die wichtigsten Widgets im Überblick -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>2. Bausteine einer GUI: Die wichtigsten Widgets</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In Tkinter heißen alle visuellen Bedienelemente <strong>Widgets</strong> (Window Gadgets). Jedes Widget ist ein Python-Objekt, das wir in unserem Fenster platzieren können:
|
||||||
|
</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);">Widget</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">Klasse</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">Zweck</th>
|
||||||
|
<th style="padding: 10px; border: 1px solid var(--border-color);">Wichtige Parameter & Methoden</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Hauptfenster</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tk.Tk()</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Rahmen & Titelleiste des Betriebssystems</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>.title()</code>, <code>.geometry("BxH")</code>, <code>.mainloop()</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Textanzeige</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tk.Label</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Zeigt Texte, Titel oder Zahlenwerte</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>text="..."</code>, <code>font=("Arial", 14)</code>, <code>fg="color"</code>, <code>.config(text=...)</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Schaltfläche</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tk.Button</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Knopf, der beim Anklicken eine Methode ausführt</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>text="..."</code>, <code>command=self.klick_methode</code>, <code>bg="color"</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background: #fafafa;">
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Eingabefeld</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tk.Entry</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Einzeiliges Textfeld für Benutzereingaben</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>.get()</code>, <code>.insert(0, text)</code>, <code>.delete(0, "end")</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Container</strong></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tk.Frame</code></td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Unsichtbare Box zum Gruppieren von Widgets</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid var(--border-color);">Unterteilt komplexe Layouts in saubere Blöcke</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. Layout Manager: Das Grid System -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>3. Layout-Manager: Das 2D-Grid-System (Raster)</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Widgets müssen auf dem Bildschirm positioniert werden. Der mächtigste und sauberste Layout-Manager in Tkinter ist <strong><code>grid()</code></strong>.
|
||||||
|
Er teilt den Raum wie eine Excel-Tabelle oder ein Schachbrett in <strong>Zeilen (<code>row</code>)</strong> und <strong>Spalten (<code>column</code>)</strong> ein:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="background: #f1f5f9; border: 2px dashed #94a3b8; border-radius: var(--radius-md); padding: 20px; margin: 20px 0; font-family: var(--font-mono); font-size: 0.9rem;">
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; text-align: center;">
|
||||||
|
<div style="grid-column: 1 / span 2; background: #e0e7ff; border: 1px solid #6366f1; padding: 12px; border-radius: 6px; font-weight: bold; color: #3730a3;">
|
||||||
|
row=0, column=0, columnspan=2 → 🔢 Titel: "Python Klick-Zähler"
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: 1 / span 2; background: #ffffff; border: 2px solid #6366f1; padding: 16px; border-radius: 6px; font-size: 1.4rem; font-weight: bold; color: #4f46e5;">
|
||||||
|
row=1, column=0, columnspan=2 → Zählerstand: "0"
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: 1 / span 2; background: #f8fafc; border: 1px solid #cbd5e1; padding: 8px; border-radius: 6px; color: #64748b;">
|
||||||
|
row=2, column=0, columnspan=2 → Status-Info: "Klicks: 0 | Gerade Zahl"
|
||||||
|
</div>
|
||||||
|
<div style="background: #d1fae5; border: 1px solid #10b981; padding: 12px; border-radius: 6px; font-weight: bold; color: #065f46;">
|
||||||
|
row=3, column=0<br>➕ Erhöhen
|
||||||
|
</div>
|
||||||
|
<div style="background: #fee2e2; border: 1px solid #ef4444; padding: 12px; border-radius: 6px; font-weight: bold; color: #991b1b;">
|
||||||
|
row=3, column=1<br>➖ Verringern
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: 1 / span 2; background: #f1f5f9; border: 1px solid #94a3b8; padding: 10px; border-radius: 6px; color: #1e293b;">
|
||||||
|
row=4, column=0, columnspan=2 → ↺ Zurücksetzen (Reset)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-warning">
|
||||||
|
<div class="box-title">⚠️ Häufigster Anfängerfehler: .pack() und .grid() niemals mischen!</div>
|
||||||
|
Verwende innerhalb eines Fensters (oder innerhalb eines Frames) <strong>entweder nur <code>grid()</code> oder nur <code>pack()</code></strong>.
|
||||||
|
Wenn du beide Layout-Manager im selben übergeordneten Fenster mischst, friert Python in einer Endlosschleife ein!
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Event-Driven & Methoden-Referenzen -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>4. Event-Driven Programming: Was geschieht beim Klick?</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Wenn der Benutzer auf einen Button klickt, soll ein Stück Code ausgeführt werden. In Tkinter übergeben wir dafür das <strong><code>command</code>-Attribut</strong>:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Python: Button-Befehle richtig übergeben</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="cm"># ✅ RICHTIG: Nur den Methodennamen übergeben (OHNE Klammern!)</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">btn_plus</span> = <span class="var">tk</span>.<span class="fn">Button</span>(<span class="var">self</span>.<span class="attr">root</span>, <span class="var">text</span>=<span class="str">"➕"</span>, <span class="var">command</span>=<span class="var">self</span>.<span class="fn">klick_plus</span>)
|
||||||
|
|
||||||
|
<span class="cm"># ❌ FALSCH: Mit Klammern wird die Methode SOFORT beim Erstellen aufgerufen!</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">btn_plus</span> = <span class="var">tk</span>.<span class="fn">Button</span>(<span class="var">self</span>.<span class="attr">root</span>, <span class="var">text</span>=<span class="str">"➕"</span>, <span class="var">command</span>=<span class="var">self</span>.<span class="fn">klick_plus</span>())</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-tipp">
|
||||||
|
<div class="box-title">💡 Warum ohne Klammern?</div>
|
||||||
|
Mit <code>self.klick_plus</code> übergibst du dem Button eine <strong>Visitenkarte (Referenz)</strong> der Methode. Der Button behält diese Visitenkarte und ruft die Methode erst dann an, wenn der Benutzer wirklich mit der Maus daraufklickt.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. Professionelle OOP-Architektur (MVC) -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>5. Saubere Software-Architektur: Model-View-Trennung (MVC) 🏛️</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In der professionellen Entwicklung schreibt man GUI-Apps niemals als wilden Spaghetticode, sondern trennt die <strong>Logik (Berechnung & Daten)</strong> strikt von der <strong>Präsentation (Fenster & Buttons)</strong>:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 15px; margin: 20px 0;">
|
||||||
|
<div style="background: #ffffff; border: 2px solid var(--primary); border-radius: var(--radius-sm); padding: 15px;">
|
||||||
|
<h4 style="color: var(--primary); margin-bottom: 8px;">1. Model: <code>ZaehlerLogik</code></h4>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--text-muted);">
|
||||||
|
Reines Python! Verwaltet <code>wert</code>, <code>schrittweite</code>, Grenzwerte und Berechnungen.<br>
|
||||||
|
<strong>Vorteil:</strong> 100% unabhängig von Bildschirmen und in Sekundenbruchteilen per Unit Test prüfbar!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #ffffff; border: 2px solid var(--success); border-radius: var(--radius-sm); padding: 15px;">
|
||||||
|
<h4 style="color: var(--success); margin-bottom: 8px;">2. View / Controller: <code>ZaehlerApp</code></h4>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--text-muted);">
|
||||||
|
Hält die Widgets (<code>Label</code>, <code>Button</code>, <code>Entry</code>).<br>
|
||||||
|
Bei einem Button-Klick ruft sie Methoden der <code>ZaehlerLogik</code> auf und spiegelt das Ergebnis im Label wider.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Struktur einer OOP-GUI-Klasse</span>
|
||||||
|
</div>
|
||||||
|
<pre><span class="kw">class</span> <span class="fn">ZaehlerApp</span>:
|
||||||
|
<span class="kw">def</span> <span class="fn">__init__</span>(<span class="var">self</span>, <span class="var">root</span>: <span class="var">tk</span>.<span class="fn">Tk</span>):
|
||||||
|
<span class="var">self</span>.<span class="attr">root</span> = <span class="var">root</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">root</span>.<span class="fn">title</span>(<span class="str">"Python Zähler"</span>)
|
||||||
|
|
||||||
|
<span class="cm"># 1. Datenmodell einbinden</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">logik</span> = <span class="fn">ZaehlerLogik</span>()
|
||||||
|
|
||||||
|
<span class="cm"># 2. Widgets erstellen</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">label_anzeige</span> = <span class="var">tk</span>.<span class="fn">Label</span>(<span class="var">self</span>.<span class="attr">root</span>, <span class="var">text</span>=<span class="str">"0"</span>, <span class="var">font</span>=(<span class="str">"Arial"</span>, <span class="num">32</span>))
|
||||||
|
<span class="var">self</span>.<span class="attr">label_anzeige</span>.<span class="fn">grid</span>(<span class="var">row</span>=<span class="num">0</span>, <span class="var">column</span>=<span class="num">0</span>)
|
||||||
|
|
||||||
|
<span class="var">self</span>.<span class="attr">btn_plus</span> = <span class="var">tk</span>.<span class="fn">Button</span>(<span class="var">self</span>.<span class="attr">root</span>, <span class="var">text</span>=<span class="str">"➕"</span>, <span class="var">command</span>=<span class="var">self</span>.<span class="fn">klick_plus</span>)
|
||||||
|
<span class="var">self</span>.<span class="attr">btn_plus</span>.<span class="fn">grid</span>(<span class="var">row</span>=<span class="num">1</span>, <span class="var">column</span>=<span class="num">0</span>)
|
||||||
|
|
||||||
|
<span class="kw">def</span> <span class="fn">klick_plus</span>(<span class="var">self</span>) -> <span class="kw">None</span>:
|
||||||
|
<span class="var">self</span>.<span class="attr">logik</span>.<span class="fn">erhoehen</span>()
|
||||||
|
<span class="var">self</span>.<span class="fn">aktualisiere_anzeige</span>()
|
||||||
|
|
||||||
|
<span class="kw">def</span> <span class="fn">aktualisiere_anzeige</span>(<span class="var">self</span>) -> <span class="kw">None</span>:
|
||||||
|
<span class="var">self</span>.<span class="attr">label_anzeige</span>.<span class="fn">config</span>(<span class="var">text</span>=<span class="fn">str</span>(<span class="var">self</span>.<span class="attr">logik</span>.<span class="attr">wert</span>))</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 6. Deine Aufgaben in aufgabe.py -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>6. 🎯 Deine Aufgaben in <code>aufgabe.py</code></h2>
|
||||||
|
|
||||||
|
<p>Öffne die Datei <code>14_gui_mit_tkinter/aufgabe.py</code> und implementiere die Klassen Schritt für Schritt:</p>
|
||||||
|
|
||||||
|
<ul class="task-list">
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t14_1">
|
||||||
|
<label for="t14_1" class="task-label">
|
||||||
|
<strong>TODO 1 (Logik-Init):</strong> <code>ZaehlerLogik.__init__</code> mit <code>startwert</code>, <code>min_wert</code>, <code>max_wert</code>, <code>schrittweite=1</code> und <code>klick_anzahl=0</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t14_2">
|
||||||
|
<label for="t14_2" class="task-label">
|
||||||
|
<strong>TODO 2 (Rechnen & Grenzen):</strong> <code>erhoehen()</code>, <code>verringern()</code> (Grenzwerte mit <code>min()</code> / <code>max()</code> einhalten!) und <code>zuruecksetzen()</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t14_3">
|
||||||
|
<label for="t14_3" class="task-label">
|
||||||
|
<strong>TODO 3 (Zusatz-Logik):</strong> <code>setze_schrittweite(schritt)</code> (nur wenn <code>schritt > 0</code>), <code>ist_gerade()</code> und Dunder <code>__str__()</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t14_4">
|
||||||
|
<label for="t14_4" class="task-label">
|
||||||
|
<strong>TODO 4 (GUI-Aufbau):</strong> <code>ZaehlerApp.__init__</code> (Fenstertitel, Geometrie, Instanziierung von <code>ZaehlerLogik</code>, Widgets erstellen und im Grid platzieren).
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t14_5">
|
||||||
|
<label for="t14_5" class="task-label">
|
||||||
|
<strong>TODO 5 (Event-Handler):</strong> Methoden <code>klick_plus</code>, <code>klick_minus</code>, <code>klick_reset</code> und <code>klick_schritt_setzen</code> mit Fehlerbehandlung für <code>Entry</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t14_6">
|
||||||
|
<label for="t14_6" class="task-label">
|
||||||
|
<strong>TODO 6 (Display-Sync):</strong> <code>aktualisiere_anzeige()</code> zur Aktualisierung der Labels <code>label_anzeige</code> und <code>label_info</code>.
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>💡 Lösungshinweise (Aufklappen bei Bedarf)</h3>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 2: Clamping an Grenzwerte (min/max)</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<p>Um einen Wert nach oben und unten sauber zu deckeln:</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="cm"># Erhöhen mit oberer Grenze:</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">wert</span> = <span class="fn">min</span>(<span class="var">self</span>.<span class="attr">max_wert</span>, <span class="var">self</span>.<span class="attr">wert</span> + <span class="var">self</span>.<span class="attr">schrittweite</span>)
|
||||||
|
|
||||||
|
<span class="cm"># Verringern mit unterer Grenze:</span>
|
||||||
|
<span class="var">self</span>.<span class="attr">wert</span> = <span class="fn">max</span>(<span class="var">self</span>.<span class="attr">min_wert</span>, <span class="var">self</span>.<span class="attr">wert</span> - <span class="var">self</span>.<span class="attr">schrittweite</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🔍 Tipp zu TODO 5d: Fehler beim Entry-Auslesen abfangen</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<p>Benutzer können Tippfehler machen (z.B. Buchstaben eingeben). Fange das mit <code>try-except</code> ab:</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<pre><span class="var">eingabe</span> = <span class="var">self</span>.<span class="attr">entry_schritt</span>.<span class="fn">get</span>().<span class="fn">strip</span>()
|
||||||
|
<span class="kw">try</span>:
|
||||||
|
<span class="var">neuer_schritt</span> = <span class="fn">int</span>(<span class="var">eingabe</span>)
|
||||||
|
<span class="kw">if</span> <span class="var">self</span>.<span class="attr">logik</span>.<span class="fn">setze_schrittweite</span>(<span class="var">neuer_schritt</span>):
|
||||||
|
<span class="var">self</span>.<span class="fn">aktualisiere_anzeige</span>()
|
||||||
|
<span class="kw">else</span>:
|
||||||
|
<span class="var">self</span>.<span class="attr">label_info</span>.<span class="fn">config</span>(<span class="var">text</span>=<span class="str">"⚠️ Schrittweite muss > 0 sein!"</span>)
|
||||||
|
<span class="kw">except</span> <span class="fn">ValueError</span>:
|
||||||
|
<span class="var">self</span>.<span class="attr">label_info</span>.<span class="fn">config</span>(<span class="var">text</span>=<span class="str">"⚠️ Bitte eine ganze Zahl eingeben!"</span>)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 7. Testen & Navigation -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>7. 🧪 Lösung überprüfen & Starten</h2>
|
||||||
|
|
||||||
|
<h3>1. Automatische Tests ausführen:</h3>
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Terminal (im Ordner 14_gui_mit_tkinter)</span>
|
||||||
|
</div>
|
||||||
|
<pre><code>python3 test_aufgabe.py</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>2. Fertige grafische Applikation starten:</h3>
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>Terminal</span>
|
||||||
|
</div>
|
||||||
|
<pre><code>python3 gui_app.py</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
296
14_gui_mit_tkinter/musterloesung.py
Normal file
296
14_gui_mit_tkinter/musterloesung.py
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
"""
|
||||||
|
Kapitel 14: Desktop-GUIs mit Tkinter & OOP 🖥️🔢
|
||||||
|
=================================================
|
||||||
|
Musterlösung: Vollständige Implementierung von ZaehlerLogik und ZaehlerApp.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Tkinter Import mit Fallback für Headless- / Server-Umgebungen
|
||||||
|
# ==============================================================================
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
except ImportError:
|
||||||
|
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))
|
||||||
|
|
||||||
|
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 delete(self, first=0, last=None):
|
||||||
|
self._entry_text = ""
|
||||||
|
|
||||||
|
def insert(self, index, string):
|
||||||
|
self._entry_text = str(string)
|
||||||
|
|
||||||
|
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 _MockTkModule:
|
||||||
|
Tk = _MockTk
|
||||||
|
Label = _MockWidget
|
||||||
|
Button = _MockWidget
|
||||||
|
Entry = _MockWidget
|
||||||
|
Frame = _MockWidget
|
||||||
|
|
||||||
|
tk = _MockTkModule()
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# TEIL 1: DIE LOGIK-KLASSE (MODEL)
|
||||||
|
# ==============================================================================
|
||||||
|
class ZaehlerLogik:
|
||||||
|
"""
|
||||||
|
Das Datenmodell (Model) des Klick-Zählers.
|
||||||
|
Verwaltet den aktuellen Zählerstand, Grenzwerte und Schrittweiten.
|
||||||
|
Komplett unabhängig von Tkinter (reines Python).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, startwert: int = 0, min_wert: int = -100, max_wert: int = 100):
|
||||||
|
self.wert: int = startwert
|
||||||
|
self.min_wert: int = min_wert
|
||||||
|
self.max_wert: int = max_wert
|
||||||
|
self.schrittweite: int = 1
|
||||||
|
self.klick_anzahl: int = 0
|
||||||
|
|
||||||
|
def erhoehen(self) -> int:
|
||||||
|
"""Erhöht den Zählerstand um die Schrittweite (maximal bis max_wert)."""
|
||||||
|
self.klick_anzahl += 1
|
||||||
|
self.wert = min(self.max_wert, self.wert + self.schrittweite)
|
||||||
|
return self.wert
|
||||||
|
|
||||||
|
def verringern(self) -> int:
|
||||||
|
"""Verringert den Zählerstand um die Schrittweite (minimal bis min_wert)."""
|
||||||
|
self.klick_anzahl += 1
|
||||||
|
self.wert = max(self.min_wert, self.wert - self.schrittweite)
|
||||||
|
return self.wert
|
||||||
|
|
||||||
|
def zuruecksetzen(self) -> int:
|
||||||
|
"""Setzt den Zählerstand auf 0 zurück."""
|
||||||
|
self.klick_anzahl += 1
|
||||||
|
self.wert = 0
|
||||||
|
return self.wert
|
||||||
|
|
||||||
|
def setze_schrittweite(self, schritt: int) -> bool:
|
||||||
|
"""Setzt eine neue positive Schrittweite (> 0)."""
|
||||||
|
if schritt > 0:
|
||||||
|
self.schrittweite = schritt
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ist_gerade(self) -> bool:
|
||||||
|
"""Gibt True zurück, wenn der Zählerstand gerade ist."""
|
||||||
|
return self.wert % 2 == 0
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Zählerstand: {self.wert} (Schrittweite: {self.schrittweite}, Klicks: {self.klick_anzahl})"
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# TEIL 2: DIE GUI-KLASSE (VIEW & CONTROLLER)
|
||||||
|
# ==============================================================================
|
||||||
|
class ZaehlerApp:
|
||||||
|
"""
|
||||||
|
Die Benutzeroberfläche (GUI) für den Klick-Zähler mit Tkinter.
|
||||||
|
Erstellt Fenster, Labels, Buttons und Eingabefelder.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, root: tk.Tk):
|
||||||
|
self.root = root
|
||||||
|
self.root.title("Python OOP Zähler 🔢")
|
||||||
|
self.root.geometry("380x360")
|
||||||
|
|
||||||
|
# 1. Datenmodell (Logik)
|
||||||
|
self.logik = ZaehlerLogik(startwert=0, min_wert=-100, max_wert=100)
|
||||||
|
|
||||||
|
# 2. Widgets erstellen & per Grid anordnen
|
||||||
|
# Titel-Label
|
||||||
|
self.label_titel = tk.Label(
|
||||||
|
self.root,
|
||||||
|
text="🔢 Python Klick-Zähler",
|
||||||
|
font=("Arial", 16, "bold"),
|
||||||
|
)
|
||||||
|
self.label_titel.grid(row=0, column=0, columnspan=2, padx=15, pady=(15, 5))
|
||||||
|
|
||||||
|
# Große Zähleranzeige
|
||||||
|
self.label_anzeige = tk.Label(
|
||||||
|
self.root,
|
||||||
|
text="0",
|
||||||
|
font=("Arial", 36, "bold"),
|
||||||
|
fg="#4f46e5",
|
||||||
|
)
|
||||||
|
self.label_anzeige.grid(row=1, column=0, columnspan=2, padx=15, pady=10)
|
||||||
|
|
||||||
|
# Info- / Statusanzeige
|
||||||
|
self.label_info = tk.Label(
|
||||||
|
self.root,
|
||||||
|
text="Klicks: 0 | Gerade Zahl",
|
||||||
|
font=("Arial", 10),
|
||||||
|
fg="#64748b",
|
||||||
|
)
|
||||||
|
self.label_info.grid(row=2, column=0, columnspan=2, padx=15, pady=(0, 15))
|
||||||
|
|
||||||
|
# Buttons zum Erhöhen und Verringern
|
||||||
|
self.btn_plus = tk.Button(
|
||||||
|
self.root,
|
||||||
|
text="➕ Erhöhen",
|
||||||
|
font=("Arial", 11, "bold"),
|
||||||
|
bg="#10b981",
|
||||||
|
fg="white",
|
||||||
|
command=self.klick_plus,
|
||||||
|
)
|
||||||
|
self.btn_plus.grid(row=3, column=0, padx=10, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
self.btn_minus = tk.Button(
|
||||||
|
self.root,
|
||||||
|
text="➖ Verringern",
|
||||||
|
font=("Arial", 11, "bold"),
|
||||||
|
bg="#ef4444",
|
||||||
|
fg="white",
|
||||||
|
command=self.klick_minus,
|
||||||
|
)
|
||||||
|
self.btn_minus.grid(row=3, column=1, padx=10, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
# Reset-Button
|
||||||
|
self.btn_reset = tk.Button(
|
||||||
|
self.root,
|
||||||
|
text="↺ Zurücksetzen",
|
||||||
|
font=("Arial", 10),
|
||||||
|
command=self.klick_reset,
|
||||||
|
)
|
||||||
|
self.btn_reset.grid(row=4, column=0, columnspan=2, padx=10, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
# Container für die Schrittweite
|
||||||
|
self.frame_schritt = tk.Frame(self.root)
|
||||||
|
self.frame_schritt.grid(row=5, column=0, columnspan=2, padx=10, pady=(15, 10))
|
||||||
|
|
||||||
|
self.label_schritt = tk.Label(self.frame_schritt, text="Schrittweite:")
|
||||||
|
self.label_schritt.pack(side="left", padx=5)
|
||||||
|
|
||||||
|
self.entry_schritt = tk.Entry(self.frame_schritt, width=6, font=("Arial", 10))
|
||||||
|
self.entry_schritt.insert(0, "1")
|
||||||
|
self.entry_schritt.pack(side="left", padx=5)
|
||||||
|
|
||||||
|
self.btn_schritt = tk.Button(
|
||||||
|
self.frame_schritt,
|
||||||
|
text="Setzen",
|
||||||
|
font=("Arial", 9),
|
||||||
|
command=self.klick_schritt_setzen,
|
||||||
|
)
|
||||||
|
self.btn_schritt.pack(side="left", padx=5)
|
||||||
|
|
||||||
|
# Initiale Anzeige synchronisieren
|
||||||
|
self.aktualisiere_anzeige()
|
||||||
|
|
||||||
|
def klick_plus(self) -> None:
|
||||||
|
"""Handler für Klick auf den Plus-Button."""
|
||||||
|
self.logik.erhoehen()
|
||||||
|
self.aktualisiere_anzeige()
|
||||||
|
|
||||||
|
def klick_minus(self) -> None:
|
||||||
|
"""Handler für Klick auf den Minus-Button."""
|
||||||
|
self.logik.verringern()
|
||||||
|
self.aktualisiere_anzeige()
|
||||||
|
|
||||||
|
def klick_reset(self) -> None:
|
||||||
|
"""Handler für Klick auf den Reset-Button."""
|
||||||
|
self.logik.zuruecksetzen()
|
||||||
|
self.aktualisiere_anzeige()
|
||||||
|
|
||||||
|
def klick_schritt_setzen(self) -> None:
|
||||||
|
"""Handler zum Auslesen und Anwenden der Schrittweite."""
|
||||||
|
eingabe = self.entry_schritt.get().strip()
|
||||||
|
try:
|
||||||
|
neuer_schritt = int(eingabe)
|
||||||
|
if self.logik.setze_schrittweite(neuer_schritt):
|
||||||
|
self.aktualisiere_anzeige()
|
||||||
|
else:
|
||||||
|
self.label_info.config(text="⚠️ Schrittweite muss größer als 0 sein!", fg="#ef4444")
|
||||||
|
except ValueError:
|
||||||
|
self.label_info.config(text="⚠️ Bitte eine ganze Zahl eingeben!", fg="#ef4444")
|
||||||
|
|
||||||
|
def aktualisiere_anzeige(self) -> None:
|
||||||
|
"""Aktualisiert alle Anzeige-Elemente mit den Daten aus dem Logik-Modell."""
|
||||||
|
# 1. Zählerstand
|
||||||
|
self.label_anzeige.config(text=str(self.logik.wert))
|
||||||
|
|
||||||
|
# 2. Farb-Feedback
|
||||||
|
if self.logik.wert > 0:
|
||||||
|
self.label_anzeige.config(fg="#10b981") # Grün
|
||||||
|
elif self.logik.wert < 0:
|
||||||
|
self.label_anzeige.config(fg="#ef4444") # Rot
|
||||||
|
else:
|
||||||
|
self.label_anzeige.config(fg="#4f46e5") # Indigo
|
||||||
|
|
||||||
|
# 3. Statusinfo
|
||||||
|
paritaet = "Gerade" if self.logik.ist_gerade() else "Ungerade"
|
||||||
|
info = f"Klicks: {self.logik.klick_anzahl} | {paritaet} | Schritt: {self.logik.schrittweite}"
|
||||||
|
|
||||||
|
if self.logik.wert >= self.logik.max_wert:
|
||||||
|
info += " (Maximum erreicht!)"
|
||||||
|
elif self.logik.wert <= self.logik.min_wert:
|
||||||
|
info += " (Minimum erreicht!)"
|
||||||
|
|
||||||
|
self.label_info.config(text=info, fg="#64748b")
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Startblock
|
||||||
|
# ==============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("🚀 Starte Tkinter Klick-Zähler (Musterlösung)...")
|
||||||
|
try:
|
||||||
|
root = tk.Tk()
|
||||||
|
app = ZaehlerApp(root)
|
||||||
|
print("💡 Fenster geöffnet! Schließe das Fenster, um das Programm zu beenden.")
|
||||||
|
root.mainloop()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ GUI konnte nicht im Display geöffnet werden ({e}).")
|
||||||
|
print("💡 Führe 'python3 test_aufgabe.py' aus, um deine Lösung automatisch zu prüfen!")
|
||||||
266
14_gui_mit_tkinter/test_aufgabe.py
Normal file
266
14_gui_mit_tkinter/test_aufgabe.py
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
🧪 Testsuite für Kapitel 14: Desktop-GUIs mit Tkinter & OOP
|
||||||
|
============================================================
|
||||||
|
Überprüft:
|
||||||
|
- ZaehlerLogik: Reines Datenmodell (Werte, Grenzen, Schrittweiten, Klicks)
|
||||||
|
- ZaehlerApp: Tkinter-Oberfläche, Widget-Erstellung, Event-Handling, MVC-Synchronisation
|
||||||
|
- Headless-Kompatibilität: Läuft ohne GUI-Blockade in jeder Testumgebung!
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Pfad zum aktuellen Kapitel hinzufü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 TestKapitel14Logik(unittest.TestCase):
|
||||||
|
"""Prüft das reine Datenmodell ZaehlerLogik."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
ZaehlerLogik = getattr(aufgabe, "ZaehlerLogik", None)
|
||||||
|
self.assertIsNotNone(
|
||||||
|
ZaehlerLogik, "Klasse 'ZaehlerLogik' in aufgabe.py nicht gefunden!"
|
||||||
|
)
|
||||||
|
self.ZaehlerLogik = ZaehlerLogik
|
||||||
|
|
||||||
|
def test_01_logik_initialisierung(self):
|
||||||
|
"""Prüft Default-Werte und benutzerdefinierte Parameter."""
|
||||||
|
# Standard-Instanziierung
|
||||||
|
z1 = self.ZaehlerLogik()
|
||||||
|
self.assertEqual(z1.wert, 0, "Default-Startwert sollte 0 sein.")
|
||||||
|
self.assertEqual(z1.min_wert, -100, "Default min_wert sollte -100 sein.")
|
||||||
|
self.assertEqual(z1.max_wert, 100, "Default max_wert sollte 100 sein.")
|
||||||
|
self.assertEqual(z1.schrittweite, 1, "Default-Schrittweite sollte 1 sein.")
|
||||||
|
self.assertEqual(z1.klick_anzahl, 0, "Default-Klickanzahl sollte 0 sein.")
|
||||||
|
|
||||||
|
# Custom Instanziierung
|
||||||
|
z2 = self.ZaehlerLogik(startwert=10, min_wert=-20, max_wert=50)
|
||||||
|
self.assertEqual(z2.wert, 10)
|
||||||
|
self.assertEqual(z2.min_wert, -20)
|
||||||
|
self.assertEqual(z2.max_wert, 50)
|
||||||
|
|
||||||
|
def test_02_logik_erhoehen_und_verringern(self):
|
||||||
|
"""Prüft das Erhöhen und Verringern des Zählerstands und Zählen der Klicks."""
|
||||||
|
z = self.ZaehlerLogik(startwert=0)
|
||||||
|
|
||||||
|
# 1. Erhöhen
|
||||||
|
res = z.erhoehen()
|
||||||
|
self.assertEqual(res, 1, "erhoehen() sollte den neuen Wert (1) zurückgeben.")
|
||||||
|
self.assertEqual(z.wert, 1)
|
||||||
|
self.assertEqual(z.klick_anzahl, 1)
|
||||||
|
|
||||||
|
z.erhoehen()
|
||||||
|
self.assertEqual(z.wert, 2)
|
||||||
|
self.assertEqual(z.klick_anzahl, 2)
|
||||||
|
|
||||||
|
# 2. Verringern
|
||||||
|
res_v = z.verringern()
|
||||||
|
self.assertEqual(res_v, 1, "verringern() sollte den neuen Wert (1) zurückgeben.")
|
||||||
|
self.assertEqual(z.wert, 1)
|
||||||
|
self.assertEqual(z.klick_anzahl, 3)
|
||||||
|
|
||||||
|
z.verringern()
|
||||||
|
z.verringern()
|
||||||
|
self.assertEqual(z.wert, -1)
|
||||||
|
self.assertEqual(z.klick_anzahl, 5)
|
||||||
|
|
||||||
|
def test_03_logik_grenzwerte_clamping(self):
|
||||||
|
"""Prüft, dass der Wert min_wert und max_wert nicht überschreitet."""
|
||||||
|
z = self.ZaehlerLogik(startwert=9, min_wert=-5, max_wert=10)
|
||||||
|
|
||||||
|
# Erhöhen bis zum Limit
|
||||||
|
z.erhoehen() # wert = 10
|
||||||
|
self.assertEqual(z.wert, 10)
|
||||||
|
|
||||||
|
# Weiteres Erhöhen darf 10 nicht überschreiten
|
||||||
|
z.erhoehen()
|
||||||
|
self.assertEqual(z.wert, 10, "Zählerstand darf max_wert nicht überschreiten!")
|
||||||
|
|
||||||
|
# Verringern bis zum Minimal-Limit
|
||||||
|
z2 = self.ZaehlerLogik(startwert=-4, min_wert=-5, max_wert=10)
|
||||||
|
z2.verringern() # wert = -5
|
||||||
|
self.assertEqual(z2.wert, -5)
|
||||||
|
|
||||||
|
z2.verringern() # bleibt bei -5
|
||||||
|
self.assertEqual(z2.wert, -5, "Zählerstand darf min_wert nicht unterschreiten!")
|
||||||
|
|
||||||
|
def test_04_logik_schrittweite(self):
|
||||||
|
"""Prüft das Setzen und Anwenden der Schrittweite."""
|
||||||
|
z = self.ZaehlerLogik(startwert=0)
|
||||||
|
|
||||||
|
# Gültige Schrittweite
|
||||||
|
ok = z.setze_schrittweite(5)
|
||||||
|
self.assertTrue(ok, "setze_schrittweite(5) sollte True zurückgeben.")
|
||||||
|
self.assertEqual(z.schrittweite, 5)
|
||||||
|
|
||||||
|
z.erhoehen()
|
||||||
|
self.assertEqual(z.wert, 5, "erhoehen() sollte bei Schrittweite 5 um 5 steigen.")
|
||||||
|
|
||||||
|
z.verringern()
|
||||||
|
self.assertEqual(z.wert, 0)
|
||||||
|
|
||||||
|
# Ungültige Schrittweiten (0 oder negativ)
|
||||||
|
ok_null = z.setze_schrittweite(0)
|
||||||
|
self.assertFalse(ok_null, "setze_schrittweite(0) sollte False zurückgeben.")
|
||||||
|
self.assertEqual(z.schrittweite, 5, "Schrittweite darf sich bei 0 nicht ändern.")
|
||||||
|
|
||||||
|
ok_neg = z.setze_schrittweite(-3)
|
||||||
|
self.assertFalse(ok_neg, "setze_schrittweite(-3) sollte False zurückgeben.")
|
||||||
|
self.assertEqual(z.schrittweite, 5, "Schrittweite darf sich bei negativen Werten nicht ändern.")
|
||||||
|
|
||||||
|
def test_05_logik_zuruecksetzen(self):
|
||||||
|
"""Prüft das Zurücksetzen auf 0."""
|
||||||
|
z = self.ZaehlerLogik(startwert=42)
|
||||||
|
res = z.zuruecksetzen()
|
||||||
|
self.assertEqual(res, 0, "zuruecksetzen() sollte 0 zurückgeben.")
|
||||||
|
self.assertEqual(z.wert, 0)
|
||||||
|
self.assertEqual(z.klick_anzahl, 1)
|
||||||
|
|
||||||
|
def test_06_logik_ist_gerade_und_str(self):
|
||||||
|
"""Prüft die Geradheits-Prüfung und String-Repräsentation."""
|
||||||
|
z = self.ZaehlerLogik(startwert=0)
|
||||||
|
self.assertTrue(z.ist_gerade(), "0 ist gerade.")
|
||||||
|
|
||||||
|
z.erhoehen() # 1
|
||||||
|
self.assertFalse(z.ist_gerade(), "1 ist ungerade.")
|
||||||
|
|
||||||
|
z.erhoehen() # 2
|
||||||
|
self.assertTrue(z.ist_gerade(), "2 ist gerade.")
|
||||||
|
|
||||||
|
# __str__ Prüfung
|
||||||
|
text = str(z)
|
||||||
|
self.assertIn("2", text, "__str__ sollte den aktuellen Zählerstand enthalten.")
|
||||||
|
|
||||||
|
|
||||||
|
class TestKapitel14GUI(unittest.TestCase):
|
||||||
|
"""Prüft die Tkinter-Benutzeroberfläche ZaehlerApp im Headless-Modus."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
ZaehlerApp = getattr(aufgabe, "ZaehlerApp", None)
|
||||||
|
self.assertIsNotNone(
|
||||||
|
ZaehlerApp, "Klasse 'ZaehlerApp' in aufgabe.py nicht gefunden!"
|
||||||
|
)
|
||||||
|
self.ZaehlerApp = ZaehlerApp
|
||||||
|
|
||||||
|
# Hauptfenster (oder MockTk) erzeugen
|
||||||
|
tk_mod = getattr(aufgabe, "tk", None)
|
||||||
|
self.assertIsNotNone(tk_mod, "Tkinter-Modul 'tk' in aufgabe.py nicht gefunden!")
|
||||||
|
self.root = tk_mod.Tk()
|
||||||
|
self.app = self.ZaehlerApp(self.root)
|
||||||
|
|
||||||
|
def _get_widget_text(self, widget) -> str:
|
||||||
|
"""Hilfsfunktion zum Auslesen des Texts eines Widgets."""
|
||||||
|
if widget is None:
|
||||||
|
return ""
|
||||||
|
if hasattr(widget, "cget"):
|
||||||
|
try:
|
||||||
|
return str(widget.cget("text"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if hasattr(widget, "_text"):
|
||||||
|
return str(widget._text)
|
||||||
|
if isinstance(widget, dict):
|
||||||
|
return str(widget.get("text", ""))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def test_07_gui_initialisierung(self):
|
||||||
|
"""Prüft, ob alle Widgets angelegt wurden und die Startwerte stimmen."""
|
||||||
|
self.assertIsNotNone(getattr(self.app, "root", None), "app.root fehlt!")
|
||||||
|
self.assertIsNotNone(getattr(self.app, "logik", None), "app.logik fehlt!")
|
||||||
|
|
||||||
|
# Widgets überprüfen
|
||||||
|
self.assertTrue(
|
||||||
|
hasattr(self.app, "label_anzeige"), "app.label_anzeige fehlt!"
|
||||||
|
)
|
||||||
|
self.assertTrue(hasattr(self.app, "label_info"), "app.label_info fehlt!")
|
||||||
|
self.assertTrue(hasattr(self.app, "btn_plus"), "app.btn_plus fehlt!")
|
||||||
|
self.assertTrue(hasattr(self.app, "btn_minus"), "app.btn_minus fehlt!")
|
||||||
|
self.assertTrue(hasattr(self.app, "btn_reset"), "app.btn_reset fehlt!")
|
||||||
|
self.assertTrue(hasattr(self.app, "entry_schritt"), "app.entry_schritt fehlt!")
|
||||||
|
|
||||||
|
# Initialer Text im Anzeige-Label sollte "0" sein
|
||||||
|
anzeige_text = self._get_widget_text(self.app.label_anzeige)
|
||||||
|
self.assertEqual(
|
||||||
|
anzeige_text, "0", f"Startwert im Anzeige-Label sollte '0' sein, war aber '{anzeige_text}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_08_gui_klick_plus_und_minus(self):
|
||||||
|
"""Prüft, ob klick_plus und klick_minus die Logik und das Label aktualisieren."""
|
||||||
|
# 1. Plus-Klick auslösen
|
||||||
|
self.app.klick_plus()
|
||||||
|
self.assertEqual(self.app.logik.wert, 1, "Logik-Wert sollte 1 sein.")
|
||||||
|
self.assertEqual(
|
||||||
|
self._get_widget_text(self.app.label_anzeige),
|
||||||
|
"1",
|
||||||
|
"Anzeige-Label sollte nach Plus-Klick '1' zeigen.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Weiterer Plus-Klick
|
||||||
|
self.app.klick_plus()
|
||||||
|
self.assertEqual(self.app.logik.wert, 2)
|
||||||
|
self.assertEqual(self._get_widget_text(self.app.label_anzeige), "2")
|
||||||
|
|
||||||
|
# 3. Minus-Klick
|
||||||
|
self.app.klick_minus()
|
||||||
|
self.assertEqual(self.app.logik.wert, 1)
|
||||||
|
self.assertEqual(self._get_widget_text(self.app.label_anzeige), "1")
|
||||||
|
|
||||||
|
def test_09_gui_klick_reset(self):
|
||||||
|
"""Prüft, ob klick_reset den Zähler und das Label zurücksetzt."""
|
||||||
|
self.app.klick_plus()
|
||||||
|
self.app.klick_plus()
|
||||||
|
self.assertEqual(self.app.logik.wert, 2)
|
||||||
|
|
||||||
|
self.app.klick_reset()
|
||||||
|
self.assertEqual(self.app.logik.wert, 0, "Logik-Wert sollte 0 nach Reset sein.")
|
||||||
|
self.assertEqual(
|
||||||
|
self._get_widget_text(self.app.label_anzeige),
|
||||||
|
"0",
|
||||||
|
"Anzeige-Label sollte nach Reset '0' zeigen.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_10_gui_schrittweite_setzen(self):
|
||||||
|
"""Prüft, ob klick_schritt_setzen den Wert aus entry_schritt übernimmt."""
|
||||||
|
# Wert im Entry-Feld anpassen
|
||||||
|
self.app.entry_schritt.delete(0, "end")
|
||||||
|
self.app.entry_schritt.insert(0, "10")
|
||||||
|
|
||||||
|
# Setzen auslösen
|
||||||
|
self.app.klick_schritt_setzen()
|
||||||
|
self.assertEqual(
|
||||||
|
self.app.logik.schrittweite,
|
||||||
|
10,
|
||||||
|
"Schrittweite in Logik sollte auf 10 gesetzt werden.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Plus klicken -> Sprung um 10
|
||||||
|
self.app.klick_plus()
|
||||||
|
self.assertEqual(self.app.logik.wert, 10)
|
||||||
|
self.assertEqual(self._get_widget_text(self.app.label_anzeige), "10")
|
||||||
|
|
||||||
|
def test_11_gui_ungueltige_eingabe_abfangen(self):
|
||||||
|
"""Prüft robuste Fehlerbehandlung bei ungültigen Entry-Eingaben."""
|
||||||
|
# Text statt Zahl eingeben
|
||||||
|
self.app.entry_schritt.delete(0, "end")
|
||||||
|
self.app.entry_schritt.insert(0, "keine_zahl")
|
||||||
|
|
||||||
|
# Darf keinen Absturz/Exception erzeugen
|
||||||
|
try:
|
||||||
|
self.app.klick_schritt_setzen()
|
||||||
|
except Exception as e:
|
||||||
|
self.fail(f"klick_schritt_setzen() ist bei ungültiger Eingabe abgestürzt: {e}")
|
||||||
|
|
||||||
|
# Schrittweite sollte unverändert geblieben sein
|
||||||
|
self.assertEqual(self.app.logik.schrittweite, 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
13
README.md
13
README.md
|
|
@ -25,12 +25,13 @@ Diese Übungsreihe ist didaktisch aufbereitet und **1:1 mit dem offiziellen Lehr
|
||||||
| Kapitel | Thema | Was du lernst |
|
| Kapitel | Thema | Was du lernst |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **[07. Referenzen, Speicher & Stammbäume](./07_referenzen_und_speicher/)** | **Speicher & RAM** | **Referenzen, Aliasing (`b = a`), Mutable vs. Immutable, `is` vs. `==` & Familienstammbaum** |
|
| **[07. Referenzen, Speicher & Stammbäume](./07_referenzen_und_speicher/)** | **Speicher & RAM** | **Referenzen, Aliasing (`b = a`), Mutable vs. Immutable, `is` vs. `==` & Familienstammbaum** |
|
||||||
| *08. Operator Overloading* | *Dunder-Methoden* | *`__add__`, `__eq__`, `__lt__` (2D-Vektoren & Kartenspiele)* |
|
| **[08. Operator Overloading](./08_operator_overloading_dunder/)** | **Dunder-Methoden** | **`__add__`, `__mul__`, `__eq__`, `__lt__` (2D-Vektoren & Wegstrecken)** |
|
||||||
| *09. Eigene Unit Tests & TDD* | *Softwarequalität* | *Testsuiten schreiben mit `unittest`, `assertEqual`, `assertTrue`, `assertRaises`* |
|
| **[09. Eigene Unit Tests & TDD](./09_eigene_unit_tests_schreiben/)** | **Softwarequalität** | **Testsuiten schreiben mit `unittest`, `assertEqual`, `assertTrue`, `assertRaises`** |
|
||||||
| *10. Vererbung & `super()`* | *Inheritance* | *Basisklassen, Kindklassen & `super().__init__()` (Fahrzeuge & RPG-Helden)* |
|
| **[10. Vererbung & `super()`](./10_vererbung_und_super/)** | **Inheritance** | **Basisklassen, Kindklassen, DRY-Prinzip & `super().__init__()` (Fahrzeugflotte)** |
|
||||||
| *11. Polymorphie & Interfaces* | *Polymorphism* | *Methoden überschreiben, Geometrie- & Grafik-Engine* |
|
| **[11. Polymorphie & Interfaces](./11_polymorphie_und_interfaces/)** | **Polymorphism** | **Vielgestaltigkeit, Duck Typing, abstrakte Basisklassen (`ABC`) & Geometrie-Engine** |
|
||||||
| *12. Exceptions & Fehler* | *Robustes Coding* | *`try/except/else/finally`, `raise`, eigene Exception-Klassen* |
|
| **[12. Exceptions & Fehler](./12_exceptions_und_fehlerbehandlung/)** | **Robustes Coding** | **`try/except/else/finally`, `raise`, eigene Exception-Klassen (Bankkonto)** |
|
||||||
| *13. JSON & CSV Speichern* | *Persistenz* | *Spielstände dauerhaft mit JSON/CSV auf der Festplatte sichern* |
|
| **[13. JSON & CSV Speichern](./13_persistenz_json_und_csv/)** | **Persistenz** | **Spielstände dauerhaft mit JSON/CSV auf der Festplatte sichern** |
|
||||||
|
| **[14. Desktop-GUIs mit Tkinter](./14_gui_mit_tkinter/)** | **Grafische Oberflächen** | **Fenster, Buttons, Labels, Grid-Layout & MVC-Architektur** |
|
||||||
|
|
||||||
*(Den vollständigen didaktischen Masterplan für alle Kapitel bis 16 findest du in [`LEHRPLAN.md`](./LEHRPLAN.md).)*
|
*(Den vollständigen didaktischen Masterplan für alle Kapitel bis 16 findest du in [`LEHRPLAN.md`](./LEHRPLAN.md).)*
|
||||||
|
|
||||||
|
|
|
||||||
80
index.html
80
index.html
|
|
@ -149,32 +149,82 @@
|
||||||
<div class="roadmap-footer">Kapitel 07 öffnen →</div>
|
<div class="roadmap-footer">Kapitel 07 öffnen →</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Vorschau Kapitel 8 -->
|
<!-- Kapitel 8 -->
|
||||||
<div class="roadmap-card" style="opacity: 0.85;">
|
<a href="08_operator_overloading_dunder/index.html" class="roadmap-card" style="border-left: 4px solid var(--secondary);">
|
||||||
<div>
|
<div>
|
||||||
<span class="roadmap-number">Kapitel 08 • In Vorbereitung</span>
|
<span class="roadmap-number" style="background: #e0f2fe; color: #0284c7;">Kapitel 08 • Neu</span>
|
||||||
<div class="roadmap-title">➕ Operator Overloading & Dunder</div>
|
<div class="roadmap-title">➕ Operator Overloading & Dunder</div>
|
||||||
<div class="roadmap-desc">Eigene Klassen rechen- und vergleichbar machen mit <code>__add__</code>, <code>__eq__</code>, <code>__lt__</code> für 2D-Vektoren & Kartenspiele.</div>
|
<div class="roadmap-desc">Eigene Klassen rechen- und vergleichbar machen mit <code>__add__</code>, <code>__mul__</code>, <code>__eq__</code>, <code>__lt__</code> für 2D-Vektoren & Wegstrecken.</div>
|
||||||
</div>
|
|
||||||
<div class="roadmap-footer" style="color: var(--text-muted);">Schulabgleich 28.0</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="roadmap-footer">Kapitel 08 öffnen →</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
<!-- Vorschau Kapitel 9 -->
|
<!-- Kapitel 9 -->
|
||||||
<div class="roadmap-card" style="opacity: 0.85; border-left: 4px solid var(--primary);">
|
<a href="09_eigene_unit_tests_schreiben/index.html" class="roadmap-card" style="border-left: 4px solid var(--primary);">
|
||||||
<div>
|
<div>
|
||||||
<span class="roadmap-number" style="background: var(--primary-light); color: var(--primary);">Kapitel 09 • Neu</span>
|
<span class="roadmap-number" style="background: var(--primary-light); color: var(--primary);">Kapitel 09 • Neu</span>
|
||||||
<div class="roadmap-title">🧪 Eigene Unit Tests schreiben (TDD)</div>
|
<div class="roadmap-title">🧪 Eigene Unit Tests & TDD</div>
|
||||||
<div class="roadmap-desc">Schreibe eigene Testsuiten mit <code>unittest</code>, <code>self.assertEqual()</code>, <code>assertRaises()</code> und lerne Test-Driven Development.</div>
|
<div class="roadmap-desc">Schreibe eigene Testsuiten mit <code>unittest</code>, <code>self.assertEqual()</code>, <code>assertRaises()</code> und lerne Test-Driven Development.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="roadmap-footer" style="color: var(--text-muted);">Softwarequalität • TDD</div>
|
<div class="roadmap-footer">Kapitel 09 öffnen →</div>
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
<!-- Vorschau Kapitel 10-13 -->
|
<!-- Kapitel 10 -->
|
||||||
|
<a href="10_vererbung_und_super/index.html" class="roadmap-card" style="border-left: 4px solid var(--primary);">
|
||||||
|
<div>
|
||||||
|
<span class="roadmap-number" style="background: var(--primary-light); color: var(--primary);">Kapitel 10 • Neu</span>
|
||||||
|
<div class="roadmap-title">🧬 Vererbung & super()</div>
|
||||||
|
<div class="roadmap-desc">DRY-Prinzip, Basis- & Kindklassen, <code>super().__init__()</code>, Methoden erweitern & Fahrzeugflotten-Hierarchie.</div>
|
||||||
|
</div>
|
||||||
|
<div class="roadmap-footer">Kapitel 10 öffnen →</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Kapitel 11 -->
|
||||||
|
<a href="11_polymorphie_und_interfaces/index.html" class="roadmap-card" style="border-left: 4px solid var(--secondary);">
|
||||||
|
<div>
|
||||||
|
<span class="roadmap-number" style="background: #e0f2fe; color: #0284c7;">Kapitel 11 • Neu</span>
|
||||||
|
<div class="roadmap-title">🎭 Polymorphie & Interfaces</div>
|
||||||
|
<div class="roadmap-desc">Vielgestaltigkeit, Duck Typing, abstrakte Basisklassen (<code>ABC</code>), Geometrie-Engine & Zeichenflächen-Manager.</div>
|
||||||
|
</div>
|
||||||
|
<div class="roadmap-footer">Kapitel 11 öffnen →</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Kapitel 12 -->
|
||||||
|
<a href="12_exceptions_und_fehlerbehandlung/index.html" class="roadmap-card" style="border-left: 4px solid var(--danger);">
|
||||||
|
<div>
|
||||||
|
<span class="roadmap-number" style="background: var(--danger-light); color: var(--danger);">Kapitel 12 • Neu</span>
|
||||||
|
<div class="roadmap-title">🛡️ Exceptions & Fehlerbehandlung</div>
|
||||||
|
<div class="roadmap-desc">Robuste Fehlerkontrolle mit <code>try-except-else-finally</code>, <code>raise</code> und eigene Exception-Klassen im Bankkonto-Simulator.</div>
|
||||||
|
</div>
|
||||||
|
<div class="roadmap-footer">Kapitel 12 öffnen →</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Kapitel 13 -->
|
||||||
|
<a href="13_persistenz_json_und_csv/index.html" class="roadmap-card" style="border-left: 4px solid var(--primary);">
|
||||||
|
<div>
|
||||||
|
<span class="roadmap-number" style="background: var(--primary-light); color: var(--primary);">Kapitel 13 • Neu</span>
|
||||||
|
<div class="roadmap-title">💾 Datei-Persistenz: JSON & CSV</div>
|
||||||
|
<div class="roadmap-desc">Dauerhaftes Speichern & Laden: Savegames mit <code>to_dict()</code>/<code>from_dict()</code>, <code>json.dump</code> und CSV-Tabellen-Export.</div>
|
||||||
|
</div>
|
||||||
|
<div class="roadmap-footer">Kapitel 13 öffnen →</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Kapitel 14 -->
|
||||||
|
<a href="14_gui_mit_tkinter/index.html" class="roadmap-card" style="border-left: 4px solid var(--purple);">
|
||||||
|
<div>
|
||||||
|
<span class="roadmap-number" style="background: var(--purple-light); color: var(--purple);">Kapitel 14 • Neu</span>
|
||||||
|
<div class="roadmap-title">🖥️ Desktop-GUIs mit Tkinter</div>
|
||||||
|
<div class="roadmap-desc">Fenster, Buttons, Labels, Grid-Layout, Event-Loop und MVC-Architektur im interaktiven Zähler & Rechner.</div>
|
||||||
|
</div>
|
||||||
|
<div class="roadmap-footer">Kapitel 14 öffnen →</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Vorschau Kapitel 8-11 & 15-16 -->
|
||||||
<div class="roadmap-card" style="opacity: 0.85;">
|
<div class="roadmap-card" style="opacity: 0.85;">
|
||||||
<div>
|
<div>
|
||||||
<span class="roadmap-number">Kapitel 10 - 13 • Geplant</span>
|
<span class="roadmap-number">Weitere Kapitel • Geplant</span>
|
||||||
<div class="roadmap-title">🧬 Vererbung, Exceptions & JSON</div>
|
<div class="roadmap-title">🧬 Vererbung, Polymorphie & Master-Projekt</div>
|
||||||
<div class="roadmap-desc">Vererbung & <code>super()</code> (10), Polymorphie (11), Eigene Fehlerklassen (12) und Savegames mit JSON/CSV sichern (13).</div>
|
<div class="roadmap-desc">Operator Overloading (08), Unit Testing (09), Vererbung (10), Polymorphie (11), Parameter (15) und Master-Projekt (16).</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="roadmap-footer" style="color: var(--text-muted);">Ausführlicher Plan in LEHRPLAN.md</div>
|
<div class="roadmap-footer" style="color: var(--text-muted);">Ausführlicher Plan in LEHRPLAN.md</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ KAPITEL = [
|
||||||
("Kapitel 05: Objekte kombinieren", "05_objekte_kombinieren"),
|
("Kapitel 05: Objekte kombinieren", "05_objekte_kombinieren"),
|
||||||
("Kapitel 06: Abschlussprojekt Tamagotchi", "06_abschlussprojekt_tamagotchi"),
|
("Kapitel 06: Abschlussprojekt Tamagotchi", "06_abschlussprojekt_tamagotchi"),
|
||||||
("Kapitel 07: Referenzen & Stammbäume", "07_referenzen_und_speicher"),
|
("Kapitel 07: Referenzen & Stammbäume", "07_referenzen_und_speicher"),
|
||||||
|
("Kapitel 08: Operator Overloading & Dunder", "08_operator_overloading_dunder"),
|
||||||
|
("Kapitel 09: Eigene Unit Tests & TDD", "09_eigene_unit_tests_schreiben"),
|
||||||
|
("Kapitel 10: Vererbung & super()", "10_vererbung_und_super"),
|
||||||
|
("Kapitel 11: Polymorphie & Interfaces", "11_polymorphie_und_interfaces"),
|
||||||
|
("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"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue