cat posts/python-lernen-11-werkzeugkasten-und-ausblick.md
Python lernen (Teil 11): Werkzeugkasten und Ausblick
Zum Abschluss: nützliche Module aus der Standardbibliothek, Generatoren, Decorators und der Ausblick auf Type Hints, Tests, Packaging und async.
In Teil 10 haben wir den
Dungeon in mehrere Module aufgeteilt. Aus einer immer länger werdenden
dungeon.py wurde ein kleines Projekt mit klaren Verantwortlichkeiten.
Damit ist die Grundlage der Serie erreicht:
- Du kannst Python-Code ausführen.
- Du kennst Variablen, Datentypen und Kontrollstrukturen.
- Du hast Funktionen geschrieben.
- Du hast mit Collections gearbeitet.
- Du hast Klassen und Objekte verwendet.
- Du hast Exceptions behandelt.
- Du hast Dateien gelesen und geschrieben.
- Du hast Module und virtuelle Umgebungen kennengelernt.
Im letzten Teil polieren wir den Dungeon noch mit ein paar Werkzeugen aus der
Standardbibliothek. Danach werfen wir einen Blick auf Themen, die nach den
Grundlagen sinnvoll sind: Generatoren, Decorators, Type Hints, Tests, Packaging
und async.
Dieser Teil ist bewusst ein Ausblick. Nicht jedes Thema wird bis ins letzte Detail erklärt. Du sollst vor allem erkennen, welche Werkzeuge es gibt und wann sie nützlich werden.
Die Standardbibliothek als Werkzeugkasten
Python bringt viele Module direkt mit.
Du musst sie nicht installieren, sondern nur importieren:
import random
import json
import logging
from pathlib import Path
from collections import Counter
Einige davon haben wir bereits benutzt:
jsonfür Spielständepathlibfür Pfadeloggingfür Logsrandomfür Zufall
Die Standardbibliothek ist einer der großen Vorteile von Python. Für viele Alltagsaufgaben brauchst Du kein externes Paket, sondern nur das passende Modul.
random: Zufall ins Spiel bringen
Ein Dungeon ohne Überraschungen ist vorhersehbar.
Das Modul random liefert Pseudozufall für Spiele, Simulationen und kleine
Zufallseffekte:
import random
ereignisse = [
"Eine Fledermaus flattert durch den Raum.",
"Irgendwo tropft Wasser.",
"Du hörst ein fernes Knurren.",
]
ereignis = random.choice(ereignisse)
print(ereignis)
random.choice(...) wählt ein zufälliges Element aus einer nicht leeren Sequenz.
Für zufällige Zahlen gibt es unter anderem randint(...):
gold = random.randint(1, 10)
print(f"Du findest {gold} Goldmünzen.")
Die Grenzen sind bei randint(1, 10) beide enthalten. Das Ergebnis kann also
1, 10 oder eine Zahl dazwischen sein.
Zufallsereignisse im Dungeon
Wir können eine kleine Funktion schreiben, die gelegentlich ein Ereignis ausgibt:
import random
EREIGNISSE = [
"Eine Fledermaus flattert durch den Raum.",
"Irgendwo tropft Wasser.",
"Du hörst ein fernes Knurren.",
"Ein kalter Windhauch streicht über den Boden.",
]
def vielleicht_ereignis():
"""Gibt manchmal ein zufälliges Ereignis zurück."""
if random.random() < 0.3:
return random.choice(EREIGNISSE)
return None
random.random() liefert eine Zahl von 0.0 bis kleiner als 1.0.
Die Bedingung
random.random() < 0.3
ist ungefähr in 30 Prozent der Fälle wahr.
Im Game-Loop könnten wir schreiben:
ereignis = vielleicht_ereignis()
if ereignis is not None:
print(ereignis)
Dadurch wirkt der Dungeon etwas lebendiger, ohne dass sich die eigentliche Spiellogik stark verändert.
Zufall ist nicht automatisch Sicherheit
random ist gut für Spiele, Simulationen und zufällige Auswahl.
Für sicherheitsrelevante Werte ist es nicht geeignet:
- Passwörter
- Tokens
- Reset-Links
- API-Schlüssel
- Session-IDs
Dafür gibt es das Modul secrets:
import secrets
token = secrets.token_urlsafe(32)
print(token)
Faustregel:
Für Spielzufall:
random.
Für Sicherheitswerte:secrets.
Ein zufälliger Würfelwurf im Dungeon darf vorhersehbar sein. Ein Passwort-Reset-Token nicht.
Reproduzierbarer Zufall mit seed
Manchmal möchtest Du Zufall reproduzierbar machen, zum Beispiel für Tests oder Beispiele.
Dafür kann ein eigener Zufallsgenerator mit festem Seed verwendet werden:
import random
zufall = random.Random(42)
print(zufall.randint(1, 10))
print(zufall.randint(1, 10))
print(zufall.randint(1, 10))
Bei demselben Seed entsteht dieselbe Folge von Pseudozufallszahlen.
Das ist nützlich, wenn ein Fehler nur bei bestimmten Zufallswerten auftritt.
Für normalen Spielbetrieb möchtest Du dagegen meistens keinen festen Seed setzen.
collections.Counter: Dinge zählen
Das Modul collections enthält spezialisierte Container.
Einer der nützlichsten ist Counter.
from collections import Counter
beute = [
"Gold",
"Trank",
"Gold",
"Gold",
"Schlüssel",
]
zaehlung = Counter(beute)
print(zaehlung)
Die Ausgabe lautet:
Counter({'Gold': 3, 'Trank': 1, 'Schlüssel': 1})
Ein Counter zählt automatisch, wie oft ein hashbarer Wert vorkommt.
print(zaehlung["Gold"])
print(zaehlung["Trank"])
print(zaehlung["Fackel"])
Ausgabe:
3
1
0
Bei einem fehlenden Schlüssel liefert Counter den Wert 0.
Das häufigste Element finden
Mit most_common(...) findest Du die häufigsten Werte:
haeufigste = zaehlung.most_common(1)
print(haeufigste)
Ausgabe:
[('Gold', 3)]
Das Ergebnis ist eine Liste von Paaren:
[("Gold", 3)]
Für die häufigsten drei Werte:
print(zaehlung.most_common(3))
Ein Counter eignet sich gut, wenn Du Fragen dieser Art beantworten willst:
- Welcher Gegenstand kommt am häufigsten vor?
- Wie viele Räume wurden wie oft besucht?
- Welche Befehle gibt der Spieler am häufigsten ein?
- Welche Fehler treten am häufigsten auf?
Befehle mit Counter zählen
Im Dungeon könnten wir zählen, welche Befehle eingegeben wurden:
from collections import Counter
befehls_zaehler = Counter()
Im Game-Loop:
verb, argument = teile_befehl(eingabe)
befehls_zaehler[verb] += 1
Beim Beenden:
print("Häufigste Befehle:")
for befehl, anzahl in befehls_zaehler.most_common():
print(f"{befehl}: {anzahl}")
So bekommst Du am Ende eine kleine Statistik über den Spieldurchlauf.
defaultdict: Defaults automatisch erzeugen
Ein normales Dictionary löst bei einem fehlenden Schlüssel einen KeyError aus:
zaehlung = {}
zaehlung["Gold"] += 1
Das funktioniert nicht, weil "Gold" noch nicht existiert.
Mit einem normalen Dictionary würdest Du vorher prüfen:
zaehlung = {}
if "Gold" not in zaehlung:
zaehlung["Gold"] = 0
zaehlung["Gold"] += 1
Oder kompakter mit .get(...):
zaehlung["Gold"] = zaehlung.get("Gold", 0) + 1
Ein defaultdict erzeugt fehlende Werte automatisch:
from collections import defaultdict
zaehlung = defaultdict(int)
zaehlung["Gold"] += 1
zaehlung["Gold"] += 1
print(zaehlung["Gold"])
Ausgabe:
2
int() liefert 0. Deshalb startet ein neuer Schlüssel automatisch bei 0.
Für viele Zählaufgaben ist Counter noch passender. defaultdict ist aber
allgemeiner.
Listen pro Schlüssel sammeln
Ein typischer Einsatz für defaultdict(list):
from collections import defaultdict
funde_nach_raum = defaultdict(list)
funde_nach_raum["halle"].append("Fackel")
funde_nach_raum["halle"].append("Brot")
funde_nach_raum["bibliothek"].append("Schlüssel")
print(funde_nach_raum["halle"])
Ausgabe:
['Fackel', 'Brot']
Wenn "halle" noch nicht existiert, erzeugt defaultdict(list) automatisch eine
neue leere Liste.
Das spart wiederholte Prüfungen wie:
if raum_id not in funde_nach_raum:
funde_nach_raum[raum_id] = []
deque: effiziente Warteschlangen
Noch ein nützlicher Container aus collections ist deque.
Eine normale Liste kann am Ende effizient Werte anhängen und entfernen:
werte.append(1)
werte.pop()
Am Anfang einer Liste ist das aber ungünstiger, weil Elemente verschoben werden müssen.
Für Warteschlangen eignet sich deque:
from collections import deque
meldungen = deque()
meldungen.append("Du hörst ein Knacken.")
meldungen.append("Eine Tür fällt ins Schloss.")
print(meldungen.popleft())
print(meldungen.popleft())
Ausgabe:
Du hörst ein Knacken.
Eine Tür fällt ins Schloss.
deque ist praktisch für:
- Warteschlangen,
- Ereignislisten,
- begrenzte Historien,
- zuletzt gesehene Werte.
Eine begrenzte Historie:
letzte_befehle = deque(maxlen=5)
letzte_befehle.append("umsehen")
letzte_befehle.append("nimm fackel")
letzte_befehle.append("norden")
Sobald mehr als fünf Werte eingefügt werden, fällt der älteste automatisch heraus.
itertools: Iteration kombinieren
Das Modul itertools enthält Werkzeuge für Iteration.
Ein einfaches Beispiel ist count(...):
from itertools import count
nummern = count(start=1)
print(next(nummern))
print(next(nummern))
print(next(nummern))
Ausgabe:
1
2
3
count(...) erzeugt eine endlose Folge.
Mit cycle(...) kannst Du eine Folge immer wieder wiederholen:
from itertools import cycle
wetter = cycle(["klar", "neblig", "regnerisch"])
print(next(wetter))
print(next(wetter))
print(next(wetter))
print(next(wetter))
Ausgabe:
klar
neblig
regnerisch
klar
Und mit islice(...) kannst Du aus einem Iterator einen begrenzten Ausschnitt
nehmen:
from itertools import count, islice
erste_fuenf = list(islice(count(start=1), 5))
print(erste_fuenf)
Ausgabe:
[1, 2, 3, 4, 5]
itertools wird besonders interessant, sobald Du Daten schrittweise
verarbeitest, statt alles sofort in Listen zu speichern.
Generatoren: Werte auf Abruf
Eine normale Funktion gibt mit return einen Wert zurück und ist danach beendet:
def quadrat(zahl):
return zahl * zahl
Eine Funktion mit yield erzeugt dagegen einen Generator:
def zaehle_bis_drei():
yield 1
yield 2
yield 3
Verwendung:
zahlen = zaehle_bis_drei()
print(next(zahlen))
print(next(zahlen))
print(next(zahlen))
Ausgabe:
1
2
3
Bei jedem yield gibt die Funktion einen Wert ab und pausiert. Beim nächsten
next(...) läuft sie genau an dieser Stelle weiter.
Nach dem letzten Wert löst der Generator intern StopIteration aus.
In einer for-Schleife musst Du Dich darum nicht kümmern:
for zahl in zaehle_bis_drei():
print(zahl)
Ein Generator für Ereignisse
Für den Dungeon könnten wir einen Generator bauen, der zufällige Ereignisse liefert:
import random
def ereignis_strom(ereignisse):
"""Liefert endlos zufällige Ereignisse."""
while True:
yield random.choice(ereignisse)
Verwendung:
ereignisse = [
"Eine Fledermaus flattert durch den Raum.",
"Irgendwo tropft Wasser.",
"Du hörst ein fernes Knurren.",
]
strom = ereignis_strom(ereignisse)
print(next(strom))
print(next(strom))
print(next(strom))
Der Generator endet nie von selbst, weil die Schleife while True endlos läuft.
Das ist nicht grundsätzlich schlecht. Du musst nur wissen, dass eine normale
for-Schleife darüber ebenfalls endlos wäre:
for ereignis in strom:
print(ereignis)
Für endlose Generatoren brauchst Du eine Abbruchbedingung oder ein Werkzeug wie
islice(...):
from itertools import islice
for ereignis in islice(strom, 3):
print(ereignis)
Dadurch werden nur drei Ereignisse ausgegeben.
Generator Expressions
Neben Generatorfunktionen gibt es Generator Expressions.
Sie sehen ähnlich aus wie List Comprehensions, verwenden aber runde Klammern:
quadrate = (zahl * zahl for zahl in range(1, 6))
Die Werte werden nicht sofort als Liste erzeugt. Sie entstehen erst beim Iterieren:
for quadrat in quadrate:
print(quadrat)
Ausgabe:
1
4
9
16
25
Zum Vergleich:
liste = [zahl * zahl for zahl in range(1, 6)]
generator = (zahl * zahl for zahl in range(1, 6))
Die Liste enthält alle Werte sofort.
Der Generator erzeugt sie auf Abruf.
Das spart Speicher, wenn viele Werte verarbeitet werden oder wenn Du vielleicht gar nicht alle Werte brauchst.
Generatoren sind einmalig
Ein Generator merkt sich seinen aktuellen Zustand.
Wenn er durchlaufen wurde, ist er verbraucht:
zahlen = (zahl for zahl in range(3))
print(list(zahlen))
print(list(zahlen))
Ausgabe:
[0, 1, 2]
[]
Der zweite Durchlauf liefert nichts mehr.
Möchtest Du erneut iterieren, erzeugst Du einen neuen Generator:
zahlen = (zahl for zahl in range(3))
print(list(zahlen))
zahlen = (zahl for zahl in range(3))
print(list(zahlen))
Bei Listen ist das anders. Eine Liste kannst Du beliebig oft durchlaufen.
Decorators: Funktionen einpacken
Ein Decorator ist eine Funktion, die eine andere Funktion entgegennimmt und eine neue Funktion zurückgibt.
Das klingt abstrakt, ist aber ein häufiges Python-Muster.
Ein einfacher Decorator:
def mit_protokoll(funktion):
def wrapper(*args, **kwargs):
print(f"[log] {funktion.__name__} wird aufgerufen")
return funktion(*args, **kwargs)
return wrapper
Anwendung:
@mit_protokoll
def speichern(name):
print(f"{name} gespeichert.")
Der Aufruf:
speichern("Karl")
ergibt:
[log] speichern wird aufgerufen
Karl gespeichert.
Die Schreibweise mit @ ist Kurzform für:
speichern = mit_protokoll(speichern)
Der Decorator ersetzt die ursprüngliche Funktion durch die verpackte Version.
functools.wraps: Metadaten erhalten
Der einfache Decorator funktioniert, hat aber einen Nachteil.
Nach dem Dekorieren heißt die Funktion intern wrapper:
print(speichern.__name__)
Ausgabe ohne weitere Maßnahmen:
wrapper
Das ist beim Debugging, Logging und in Dokumentation unpraktisch.
Deshalb verwendet man meist functools.wraps:
from functools import wraps
def mit_protokoll(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
print(f"[log] {funktion.__name__} wird aufgerufen")
return funktion(*args, **kwargs)
return wrapper
Wird speichern mit diesem Decorator neu definiert, bleiben die wichtigen
Metadaten der ursprünglichen Funktion erhalten:
@mit_protokoll
def speichern(name):
...
print(speichern.__name__)
Ausgabe:
speichern
Decorators sind überall in Python zu finden:
@dataclass
class Gegenstand:
...
@property
def name(self):
...
@pytest.fixture
def spieler():
...
Du musst sie am Anfang nicht alle selbst schreiben können. Wichtig ist zunächst, die Schreibweise zu erkennen:
Eine
@...-Zeile verändert oder ergänzt die Definition direkt darunter.
Decorators mit Argumenten
Manche Decorators stehen ohne Klammern:
@cache
def berechne_etwas():
...
Andere erhalten selbst Argumente:
@route("/status")
def status():
...
Ein Decorator mit Argumenten ist eine weitere Ebene verschachtelter Funktionen. Das ist für den Einstieg noch nicht nötig.
Für den Moment reicht:
- Decorators können Funktionen oder Klassen verändern.
@decoratorist Kurzform für eine Zuweisung.functools.wrapserhält Metadaten der Originalfunktion.- Decorators sind ein eigenes Vertiefungsthema.
Type Hints: Typen als Dokumentation für Werkzeuge
Python ist dynamisch typisiert. Eine Variable ist nicht fest an einen Typ gebunden:
wert = 10
wert = "zehn"
Trotzdem kannst Du Typen annotieren:
def erleide_schaden(hp: int, menge: int) -> int:
return max(hp - menge, 0)
Die Annotationen bedeuten:
hpsoll einintsein.mengesoll einintsein.- die Funktion soll ein
intzurückgeben.
Python selbst erzwingt diese Typen zur Laufzeit normalerweise nicht.
Dieser Aufruf wird nicht automatisch von Python verhindert:
erleide_schaden("viel", 3)
Je nach Code führt er später zu einem Laufzeitfehler.
Type Hints helfen vor allem:
- beim Lesen des Codes,
- im Editor,
- bei Autocompletion,
- bei Refactorings,
- statischen Type Checkern wie mypy, Pyright oder ty.
Type Hints im Dungeon
Einige Funktionen aus dem Dungeon könnten so annotiert werden:
def frage_ganzzahl(text: str, minimum: int | None = None) -> int:
...
def teile_befehl(eingabe: str) -> tuple[str, str]:
...
def finde_raeume_mit_loot(raeume: dict[str, Raum]) -> list[str]:
...
Auch Klassenattribute lassen sich annotieren:
class Spieler:
name: str
hp: int
max_hp: int
gold: int
inventar: list[Gegenstand]
In Verbindung mit Dataclasses hast Du solche Annotationen bereits gesehen:
from dataclasses import dataclass
@dataclass
class Gegenstand:
name: str
beschreibung: str = ""
Type Hints sind freiwillig. Du musst nicht ein ganzes Projekt auf einmal annotieren.
Ein guter Einstieg sind:
- Funktionsparameter,
- Rückgabewerte,
- zentrale Datenstrukturen,
- öffentliche APIs.
Annotationen ab Python 3.14
In modernen Python-Versionen hat sich die Behandlung von Annotationen weiterentwickelt.
Ab Python 3.14 werden Annotationen standardmäßig verzögert ausgewertet. Das bedeutet vereinfacht: Sie werden nicht mehr in jedem Fall sofort beim Definieren einer Funktion oder Klasse vollständig aufgelöst.
Für Anfänger ist die wichtigste praktische Konsequenz:
Type Hints bleiben in erster Linie Informationen für Menschen und Werkzeuge. Verlasse Dich nicht darauf, dass Python sie automatisch zur Laufzeit prüft.
Wenn Du Laufzeitvalidierung brauchst, musst Du sie weiterhin selbst schreiben oder ein passendes Werkzeug verwenden.
Tests: Vertrauen durch Wiederholung
Bisher testen wir den Dungeon vor allem von Hand:
- Programm starten.
- Namen eingeben.
- Gold eingeben.
nimm fackeleingeben.inventarprüfen.speicherntesten.- Programm neu starten.
ladentesten.
Das ist mühsam und fehleranfällig.
Automatische Tests prüfen bestimmte Erwartungen immer wieder.
Das verbreitete Werkzeug dafür ist pytest.
Ein sehr einfacher Test könnte so aussehen:
from modelle import Spieler
def test_schaden_nicht_unter_null():
spieler = Spieler("Test", max_hp=10)
spieler.erleide_schaden(99)
assert spieler.hp == 0
Die Funktion beginnt mit test_. pytest findet sie dadurch automatisch.
Der assert prüft eine Erwartung. Ist sie falsch, schlägt der Test fehl.
pytest ausführen
pytest ist kein Teil der Standardbibliothek. Du installierst es als Development Dependency.
Mit uv:
uv add --dev pytest
Ausführen:
uv run pytest
Oder klassisch in einer aktivierten venv:
python -m pip install pytest
python -m pytest
pytest sucht standardmäßig nach Testdateien und Testfunktionen, die bestimmten Namenskonventionen folgen, zum Beispiel:
test_*.py
*_test.py
und darin nach Funktionen wie:
def test_irgendwas():
...
Ein Test ersetzt nicht das Denken. Er sorgt aber dafür, dass eine einmal gefundene Erwartung später automatisch überprüft wird.
Was man im Dungeon testen könnte
Gute erste Tests wären:
def test_heilung_nicht_ueber_max_hp():
spieler = Spieler("Test", max_hp=100)
spieler.erleide_schaden(50)
spieler.heile(999)
assert spieler.hp == 100
Oder:
def test_raum_ziel_fuer_gueltige_richtung():
raum = Raum(
name="Halle",
beschreibung="Eine dunkle Halle.",
ausgaenge={"norden": "bibliothek"},
)
assert raum.ziel("norden") == "bibliothek"
Oder für die Eingabezerlegung:
def test_teile_befehl_mit_argument():
verb, argument = teile_befehl("nimm fackel")
assert verb == "nimm"
assert argument == "fackel"
Tests sind besonders wertvoll für Code, der ohne direkte Benutzereingabe funktioniert. Darum war die Aufteilung in Module und Funktionen in Teil 10 so wichtig.
Gut strukturierter Code lässt sich leichter testen.
async: wenn Programme viel warten
Normale Funktionen laufen Schritt für Schritt:
def lade_welt():
return "Welt geladen"
Asynchrone Funktionen werden mit async def definiert:
import asyncio
async def lade_welt():
await asyncio.sleep(1)
return "Welt geladen"
ergebnis = asyncio.run(lade_welt())
print(ergebnis)
await bedeutet: Diese Funktion wartet auf ein Ergebnis und gibt währenddessen
die Kontrolle an den Event Loop zurück.
Das ist nützlich, wenn ein Programm viel auf langsame Dinge wartet:
- Netzwerkantworten
- APIs
- Datenbanken
- viele gleichzeitige Verbindungen
- Webserver
- Bots
Für unseren rundenbasierten Terminal-Dungeon brauchst Du async nicht.
Der Spieler gibt einen Befehl ein, das Programm reagiert, dann wartet es wieder auf den nächsten Befehl. Da passiert nichts parallel.
Wichtig ist deshalb:
asyncmacht Code nicht automatisch schneller. Es hilft vor allem bei vielen gleichzeitigen Wartezeiten.
Packaging: aus Code wird ein installierbares Projekt
In Teil 10 haben wir mehrere .py-Dateien in einen Ordner gelegt.
Das ist für den Einstieg gut.
Ein nächster Schritt wäre ein richtiges Package:
dungeon-projekt/
├── pyproject.toml
├── src/
│ └── dungeon/
│ ├── __init__.py
│ ├── befehle.py
│ ├── eingabe.py
│ ├── fehler.py
│ ├── modelle.py
│ ├── speicher.py
│ ├── spiel.py
│ └── welt.py
└── tests/
└── test_modelle.py
Damit wird das Projekt sauber installierbar und testbar.
Später könnte daraus sogar ein echtes Kommando entstehen:
dungeon
statt:
python spiel.py
Dafür definiert man in der pyproject.toml sogenannte Scripts beziehungsweise
Entry Points.
Das ist ein eigenes Thema, aber die Richtung ist wichtig: Ab einer gewissen Größe wird aus „ein paar Dateien“ ein richtiges Python-Package.
Was Du jetzt solide können solltest
Nach dieser Serie solltest Du nicht alles auswendig können. Das ist auch nicht das Ziel.
Wichtig ist, dass Du die Grundideen wiedererkennst.
Du solltest verstehen:
- wie Python-Code ausgeführt wird,
- wie Werte in Variablen gespeichert werden,
- wie Bedingungen und Schleifen funktionieren,
- wie Listen, Dictionaries, Sets und Tupel eingesetzt werden,
- wie Funktionen Code strukturieren,
- was Scope bedeutet,
- wie Klassen State und Verhalten bündeln,
- wie Exceptions funktionieren,
- wie Dateien gelesen und geschrieben werden,
- wie JSON als Speicherformat genutzt werden kann,
- wie Module ein Projekt aufteilen,
- wofür
venv,pipund uv gedacht sind, - wann Logging besser ist als
print(...), - wie ein Debugger bei der Fehlersuche hilft.
Das ist ein ordentliches Fundament.
Alles Weitere baut darauf auf.
Was Du nicht sofort lernen musst
Python ist groß. Du musst nicht direkt alles beherrschen.
Diese Themen können warten, bis Du sie wirklich brauchst:
- Metaclasses
- eigene Descriptor-Protokolle
- komplexe Decorators mit Argumenten
- asynchrone Generatoren
- Multiprocessing
- C-Erweiterungen
- Packaging für PyPI
- fortgeschrittene Typfeatures
- eigene Import-Hooks
Es ist völlig normal, diese Begriffe irgendwann zu sehen und erst später wirklich zu verstehen.
Lernen funktioniert besser, wenn ein konkretes Problem dahintersteht.
Sinnvolle nächste Schritte
Wenn Du nach dieser Serie weiterlernen möchtest, sind diese Wege besonders lohnend.
1. Den Dungeon erweitern
Baue weiter am Spiel:
- verschlossene Türen,
- Gegner,
- Kampf,
- Heiltränke,
- mehrere Spielstände,
- eine Karte,
- zufällige Ereignisse,
- einen Shop,
- Tests für wichtige Regeln.
Das hält den Kontext vertraut, während neue Konzepte dazukommen.
2. Type Hints ergänzen
Annotiere Schritt für Schritt die vorhandenen Funktionen und Klassen.
Starte mit einfachen Stellen:
def teile_befehl(eingabe: str) -> tuple[str, str]:
...
Lass danach einen Type Checker laufen.
3. Tests schreiben
Beginne mit Code, der keinen input(...) und kein print(...) braucht:
Spieler.erleide_schaden(...)Spieler.heile(...)Raum.ziel(...)teile_befehl(...)- Speicherfunktionen mit temporären Dateien
Tests machen Refactoring viel entspannter.
4. Ein kleines echtes Tool bauen
Schreibe ein Skript, das Dir wirklich hilft:
- Dateien umbenennen,
- Markdown prüfen,
- JSON formatieren,
- Logs auswerten,
- kleine Reports erzeugen,
- wiederkehrende Aufgaben automatisieren.
Ein echtes eigenes Problem motiviert mehr als künstliche Übungen.
5. Ein Framework ausprobieren
Wenn Du Webentwicklung spannend findest, schau Dir später Flask, FastAPI oder Django an.
Für Datenanalyse wären Pandas und Jupyter typische nächste Schritte.
Für Kommandozeilen-Tools sind Typer, Click oder argparse interessant.
Wichtig: Erst die Grundlagen, dann Frameworks. Frameworks sind leichter, wenn Python selbst nicht mehr ständig im Weg steht.
Stolperfallen
-
randomfür Sicherheit verwenden: Für Spiele okay, für Tokens oder Passwörtersecretsverwenden. -
Generatoren mehrfach verwenden wollen: Ein Generator ist nach dem Durchlaufen verbraucht. Erzeuge bei Bedarf einen neuen.
-
Eine endlose Folge versehentlich komplett in eine Liste umwandeln:
python
list(count())
Dieser Code endet nicht sinnvoll.
-
Decorators als Magie behandeln:
@decoratorist nur eine Kurzform für eine Funktionsumwandlung. -
functools.wrapsvergessen: Ohnewrapsheißen dekorierte Funktionen intern oft nur nochwrapper. -
Type Hints als Laufzeitprüfung verstehen: Python erzwingt sie normalerweise nicht automatisch.
-
Zu viel auf einmal annotieren: Type Hints lassen sich schrittweise einführen.
-
Tests nur über die Oberfläche schreiben: Kleine Funktionen und Methoden sind leichter zu testen als komplette interaktive Programme.
-
asyncals allgemeines Performance-Wunder sehen:asynchilft vor allem bei vielen gleichzeitigen Wartezeiten, nicht bei CPU-lastigem Code. -
Nur Frameworks lernen: Wer nur ein Framework bedienen kann, aber Python selbst nicht versteht, stößt schnell an Grenzen.
-
Nie aufräumen: Wenn eine Datei zu groß wird oder Funktionen zu viel tun, ist Aufräumen Teil der Arbeit.
Übungen
1. Zufällige Beute erzeugen
Schreibe eine Funktion zufalls_beute(), die zufällig einen Gegenstand aus einer
Liste zurückgibt.
Lösung
import random
def zufalls_beute():
beute = [
"Gold",
"Trank",
"Fackel",
"Schlüssel",
]
return random.choice(beute)
print(zufalls_beute())
2. Beute zählen
Nutze Counter, um aus dieser Liste zu zählen, wie oft jeder Gegenstand
vorkommt:
beute = ["Gold", "Gold", "Trank", "Fackel", "Gold"]
Lösung
from collections import Counter
beute = ["Gold", "Gold", "Trank", "Fackel", "Gold"]
zaehlung = Counter(beute)
print(zaehlung)
print(zaehlung.most_common(1))
Counter({'Gold': 3, 'Trank': 1, 'Fackel': 1})
[('Gold', 3)]
3. Einen Generator schreiben
Schreibe einen Generator zaehler(start), der ab start endlos aufwärts zählt.
Lösung
def zaehler(start):
zahl = start
while True:
yield zahl
zahl += 1
zahlen = zaehler(10)
print(next(zahlen))
print(next(zahlen))
print(next(zahlen))
10
11
12
4. Einen Decorator schreiben
Schreibe einen Decorator mit_log(...), der vor jedem Funktionsaufruf den Namen
der Funktion ausgibt.
Lösung
from functools import wraps
def mit_log(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
print(f"[log] {funktion.__name__} wird aufgerufen")
return funktion(*args, **kwargs)
return wrapper
@mit_log
def begruesse(name):
print(f"Hallo, {name}!")
begruesse("Karl")
[log] begruesse wird aufgerufen
Hallo, Karl!
5. Eine Funktion typisieren
Ergänze Type Hints für diese Funktion:
def heile(hp, menge, max_hp):
return min(hp + menge, max_hp)
Lösung
def heile(hp: int, menge: int, max_hp: int) -> int:
return min(hp + menge, max_hp)
6. Einen ersten Test schreiben
Schreibe einen pytest-Test für diese Funktion:
def heile(hp: int, menge: int, max_hp: int) -> int:
return min(hp + menge, max_hp)
Lösung
def heile(hp: int, menge: int, max_hp: int) -> int:
return min(hp + menge, max_hp)
def test_heile_nicht_ueber_max_hp():
assert heile(90, 20, 100) == 100
python -m pytest
uv run pytest
Vertiefungen zu diesem Ausblick
Jedes der angerissenen Themen verdient einen eigenen Artikel:
- Type Hints in der Praxis: mypy, Pyright und Co.geplant
- pytest von Nullgeplant
- Generatoren und yield wirklich verstehengeplant
- Decorators entmystifiziertgeplant
- Vom Skript zum echten CLIgeplant
- uv: pip und venv in schnell
- Ruff: Linter und Formatter in einem
Geschafft
Das war die Python-Grundlagenserie.
Wir sind gestartet mit:
print("Hallo, Welt!")
Und gelandet bei einem kleinen, modular aufgebauten Textadventure mit:
- Spieler-State,
- Räumen,
- Gegenständen,
- Befehlsverarbeitung,
- Klassen,
- Fehlerbehandlung,
- JSON-Spielstand,
- mehreren Modulen,
- einem ersten Projekt-Setup,
- Debugging- und Logging-Werkzeugen.
Das ist mehr als ein Spielzeug. Es ist ein Fundament.
Von hier aus kannst Du in viele Richtungen weitergehen:
- ein größeres eigenes Projekt,
- Tests,
- Type Hints,
- Webentwicklung,
- Automatisierung,
- Datenanalyse,
- CLI-Tools,
- APIs,
- Bots.
Du musst nicht alles auf einmal lernen. Nimm das nächste konkrete Problem, baue eine kleine Lösung und erweitere Dein Werkzeug Stück für Stück.
Zurück zum Anfang der Serie.
Weiterlesen
- Python lernen, Teil 10: Module und Projektstruktur
- Logging statt print
- Debuggen mit breakpoint() und pdb
- Python-Dokumentation:
random - Python-Dokumentation:
secrets - Python-Dokumentation:
collections - Python-Dokumentation:
itertools - Python-Dokumentation: Generatoren
- Python-Dokumentation:
functools.wraps - Python-Dokumentation:
typing - pytest-Dokumentation: Getting started
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.