Implementiere Kapitel 08 bis 14 (Operator Overloading, Unit Testing, Vererbung, Polymorphie, Exceptions, Persistenz, Tkinter GUI)

This commit is contained in:
Alex Mueller 2026-08-29 14:26:27 +02:00
parent e7c64995aa
commit 0a32dadcc4
40 changed files with 7152 additions and 22 deletions

View file

@ -205,7 +205,7 @@
<div class="footer-nav">
<a href="../06_abschlussprojekt_tamagotchi/index.html" class="btn btn-secondary">&larr; Zurück zu Kapitel 06</a>
<a href="../index.html" class="btn">Zur Kursübersicht &rarr;</a>
<a href="../08_operator_overloading_dunder/index.html" class="btn">Weiter zu Kapitel 08 &rarr;</a>
</div>
</section>

View 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
```

View 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}")

View 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>&rsaquo;</span>
<a href="../07_referenzen_und_speicher/index.html">Kapitel 07</a>
<span>&rsaquo;</span>
<span style="color: white; font-weight: 600;">Kapitel 08</span>
</nav>
<div class="header-badge">Kapitel 08 &bull; Schulabgleich 28.0 Spezielle Methoden</div>
<h1>Operator Overloading &amp; 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" &amp; 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 &lt; 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 &lt;= 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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">&larr; Zurück zu Kapitel 07</a>
<a href="../09_eigene_unit_tests_schreiben/index.html" class="btn">Weiter zu Kapitel 09 &rarr;</a>
</div>
</section>
</main>
</body>
</html>

View 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

View 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)

View 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
```

View 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)

View 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>&rsaquo;</span>
<a href="../08_operator_overloading_dunder/index.html">Kapitel 08</a>
<span>&rsaquo;</span>
<span style="color: white; font-weight: 600;">Kapitel 09</span>
</nav>
<div class="header-badge">Kapitel 09 &bull; Softwarequalität &amp; TDD</div>
<h1>Eigene Unit Tests schreiben &amp; 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 &bull; Green &bull; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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">&larr; Zurück zu Kapitel 08</a>
<a href="../10_vererbung_und_super/index.html" class="btn">Weiter zu Kapitel 10 (Vererbung) &rarr;</a>
</div>
</section>
</main>
</body>
</html>

View 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)

View 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)

View 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)` &rarr; `True`
- `issubclass(ElektroAuto, Fahrzeug)` &rarr; `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
```

View 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")

View 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>&rsaquo;</span>
<a href="../09_eigene_unit_tests_schreiben/index.html">Kapitel 09 (Unit Tests)</a>
<span>&rsaquo;</span>
<span style="color: white; font-weight: 600;">Kapitel 10</span>
</nav>
<div class="header-badge">Kapitel 10 &bull; Schulabgleich 25.0</div>
<h1>Vererbung (Inheritance) &amp; <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- &amp; 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 &amp; 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> &lt;= <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">&larr; Zurück zu Kapitel 09</a>
<a href="../11_polymorphie_und_interfaces/index.html" class="btn">Weiter zu Kapitel 11 (Polymorphie) &rarr;</a>
</div>
</section>
</main>
</body>
</html>

View 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")

View 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)

View 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 &rarr; "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 &amp; 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 &amp; 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
```

View 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'}")

View 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>&rsaquo;</span>
<a href="../10_vererbung_und_super/index.html">Kapitel 10 (Vererbung)</a>
<span>&rsaquo;</span>
<span style="color: white; font-weight: 600;">Kapitel 11</span>
</nav>
<div class="header-badge">Kapitel 11 &bull; Schulabgleich 25.1</div>
<h1>Polymorphie &amp; 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> &nbsp;|&nbsp; <code>morphe</code> = <em>Gestalt, Form</em> &rarr; <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 &amp; 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">&laquo;abstract&raquo; 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 &nbsp;<i>(ruft form.flaeche() auf)</i><br>
+ gesamter_umfang() -> float &nbsp;<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>) &amp; 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">&larr; Zurück zu Kapitel 10</a>
<a href="../12_exceptions_und_fehlerbehandlung/index.html" class="btn">Weiter zu Kapitel 12 (Exceptions) &rarr;</a>
</div>
</section>
</main>
</body>
</html>

View 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'}")

View 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)

View 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
```

View 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)

View 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 &amp; 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>&rsaquo;</span>
<a href="../11_polymorphie_und_interfaces/index.html">Kapitel 11</a>
<span>&rsaquo;</span>
<span style="color: white; font-weight: 600;">Kapitel 12</span>
</nav>
<div class="header-badge">Kapitel 12 &bull; Schulabgleich 26.0</div>
<h1>Exceptions &amp; 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 &amp; 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 &amp; 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">&larr; Zurück zu Kapitel 11</a>
<a href="../13_persistenz_json_und_csv/index.html" class="btn">Weiter zu Kapitel 13 &rarr;</a>
</div>
</section>
</main>
</body>
</html>

View 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"))

View 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)

View 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
```

View 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}!")

View 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 &amp; 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>&rsaquo;</span>
<a href="../12_exceptions_und_fehlerbehandlung/index.html">Kapitel 12</a>
<span>&rsaquo;</span>
<span style="color: white; font-weight: 600;">Kapitel 13</span>
</nav>
<div class="header-badge">Kapitel 13 &bull; Schulabgleich 14.0</div>
<h1>Datei-Persistenz: JSON &amp; 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 &amp; 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">&larr; Zurück zu Kapitel 12</a>
<a href="../14_gui_mit_tkinter/index.html" class="btn">Weiter zu Kapitel 14 &rarr;</a>
</div>
</section>
</main>
</body>
</html>

View 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!")

View 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)

View 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 &rarr; Schritt 2 &rarr; `input()` &rarr; 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)*

View 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!")

View 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()

View 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>&rsaquo;</span>
<a href="../13_persistenz_json_und_csv/index.html">Kapitel 13</a>
<span>&rsaquo;</span>
<span style="color: white; font-weight: 600;">Kapitel 14</span>
</nav>
<div class="header-badge">Kapitel 14 &bull; Schulabgleich 27.0 Tkinter</div>
<h1>Desktop-GUIs mit Tkinter &amp; 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 &amp; 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 &amp; 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 &rarr; 🔢 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 &rarr; 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 &rarr; 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 &rarr; ↺ 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 &amp; Daten)</strong> strikt von der <strong>Präsentation (Fenster &amp; 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 &amp; 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 &gt; 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 &gt; 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 &amp; 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">&larr; Zurück zu Kapitel 13</a>
<a href="../index.html" class="btn">Zur Kursübersicht &rarr;</a>
</div>
</section>
</main>
</body>
</html>

View 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!")

View 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)

View file

@ -25,12 +25,13 @@ Diese Übungsreihe ist didaktisch aufbereitet und **1:1 mit dem offiziellen Lehr
| 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** |
| *08. Operator Overloading* | *Dunder-Methoden* | *`__add__`, `__eq__`, `__lt__` (2D-Vektoren & Kartenspiele)* |
| *09. Eigene Unit Tests & TDD* | *Softwarequalität* | *Testsuiten schreiben mit `unittest`, `assertEqual`, `assertTrue`, `assertRaises`* |
| *10. Vererbung & `super()`* | *Inheritance* | *Basisklassen, Kindklassen & `super().__init__()` (Fahrzeuge & RPG-Helden)* |
| *11. Polymorphie & Interfaces* | *Polymorphism* | *Methoden überschreiben, Geometrie- & Grafik-Engine* |
| *12. Exceptions & Fehler* | *Robustes Coding* | *`try/except/else/finally`, `raise`, eigene Exception-Klassen* |
| *13. JSON & CSV Speichern* | *Persistenz* | *Spielstände dauerhaft mit JSON/CSV auf der Festplatte sichern* |
| **[08. Operator Overloading](./08_operator_overloading_dunder/)** | **Dunder-Methoden** | **`__add__`, `__mul__`, `__eq__`, `__lt__` (2D-Vektoren & Wegstrecken)** |
| **[09. Eigene Unit Tests & TDD](./09_eigene_unit_tests_schreiben/)** | **Softwarequalität** | **Testsuiten schreiben mit `unittest`, `assertEqual`, `assertTrue`, `assertRaises`** |
| **[10. Vererbung & `super()`](./10_vererbung_und_super/)** | **Inheritance** | **Basisklassen, Kindklassen, DRY-Prinzip & `super().__init__()` (Fahrzeugflotte)** |
| **[11. Polymorphie & Interfaces](./11_polymorphie_und_interfaces/)** | **Polymorphism** | **Vielgestaltigkeit, Duck Typing, abstrakte Basisklassen (`ABC`) & Geometrie-Engine** |
| **[12. Exceptions & Fehler](./12_exceptions_und_fehlerbehandlung/)** | **Robustes Coding** | **`try/except/else/finally`, `raise`, eigene Exception-Klassen (Bankkonto)** |
| **[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).)*

View file

@ -149,32 +149,82 @@
<div class="roadmap-footer">Kapitel 07 öffnen &rarr;</div>
</a>
<!-- Vorschau Kapitel 8 -->
<div class="roadmap-card" style="opacity: 0.85;">
<!-- Kapitel 8 -->
<a href="08_operator_overloading_dunder/index.html" class="roadmap-card" style="border-left: 4px solid var(--secondary);">
<div>
<span class="roadmap-number">Kapitel 08 &bull; In Vorbereitung</span>
<span class="roadmap-number" style="background: #e0f2fe; color: #0284c7;">Kapitel 08 &bull; Neu</span>
<div class="roadmap-title">➕ Operator Overloading &amp; 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 &amp; Kartenspiele.</div>
</div>
<div class="roadmap-footer" style="color: var(--text-muted);">Schulabgleich 28.0</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 &amp; Wegstrecken.</div>
</div>
<div class="roadmap-footer">Kapitel 08 öffnen &rarr;</div>
</a>
<!-- Vorschau Kapitel 9 -->
<div class="roadmap-card" style="opacity: 0.85; border-left: 4px solid var(--primary);">
<!-- Kapitel 9 -->
<a href="09_eigene_unit_tests_schreiben/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 09 &bull; Neu</span>
<div class="roadmap-title">🧪 Eigene Unit Tests schreiben (TDD)</div>
<div class="roadmap-title">🧪 Eigene Unit Tests &amp; 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>
<div class="roadmap-footer" style="color: var(--text-muted);">Softwarequalität &bull; TDD</div>
</div>
<div class="roadmap-footer">Kapitel 09 öffnen &rarr;</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 &bull; Neu</span>
<div class="roadmap-title">🧬 Vererbung &amp; super()</div>
<div class="roadmap-desc">DRY-Prinzip, Basis- &amp; Kindklassen, <code>super().__init__()</code>, Methoden erweitern &amp; Fahrzeugflotten-Hierarchie.</div>
</div>
<div class="roadmap-footer">Kapitel 10 öffnen &rarr;</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 &bull; Neu</span>
<div class="roadmap-title">🎭 Polymorphie &amp; Interfaces</div>
<div class="roadmap-desc">Vielgestaltigkeit, Duck Typing, abstrakte Basisklassen (<code>ABC</code>), Geometrie-Engine &amp; Zeichenflächen-Manager.</div>
</div>
<div class="roadmap-footer">Kapitel 11 öffnen &rarr;</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 &bull; Neu</span>
<div class="roadmap-title">🛡️ Exceptions &amp; 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 &rarr;</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 &bull; Neu</span>
<div class="roadmap-title">💾 Datei-Persistenz: JSON &amp; CSV</div>
<div class="roadmap-desc">Dauerhaftes Speichern &amp; 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 &rarr;</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 &bull; 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 &amp; Rechner.</div>
</div>
<div class="roadmap-footer">Kapitel 14 öffnen &rarr;</div>
</a>
<!-- Vorschau Kapitel 8-11 & 15-16 -->
<div class="roadmap-card" style="opacity: 0.85;">
<div>
<span class="roadmap-number">Kapitel 10 - 13 &bull; Geplant</span>
<div class="roadmap-title">🧬 Vererbung, Exceptions &amp; JSON</div>
<div class="roadmap-desc">Vererbung &amp; <code>super()</code> (10), Polymorphie (11), Eigene Fehlerklassen (12) und Savegames mit JSON/CSV sichern (13).</div>
<span class="roadmap-number">Weitere Kapitel &bull; Geplant</span>
<div class="roadmap-title">🧬 Vererbung, Polymorphie &amp; Master-Projekt</div>
<div class="roadmap-desc">Operator Overloading (08), Unit Testing (09), Vererbung (10), Polymorphie (11), Parameter (15) und Master-Projekt (16).</div>
</div>
<div class="roadmap-footer" style="color: var(--text-muted);">Ausführlicher Plan in LEHRPLAN.md</div>
</div>

View file

@ -19,6 +19,13 @@ KAPITEL = [
("Kapitel 05: Objekte kombinieren", "05_objekte_kombinieren"),
("Kapitel 06: Abschlussprojekt Tamagotchi", "06_abschlussprojekt_tamagotchi"),
("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():