Zum Inhalt springen

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:

  • json für Spielstände
  • pathlib für Pfade
  • logging für Logs
  • random fü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.
  • @decorator ist Kurzform für eine Zuweisung.
  • functools.wraps erhä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:

  • hp soll ein int sein.
  • menge soll ein int sein.
  • die Funktion soll ein int zurü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:

  1. Programm starten.
  2. Namen eingeben.
  3. Gold eingeben.
  4. nimm fackel eingeben.
  5. inventar prüfen.
  6. speichern testen.
  7. Programm neu starten.
  8. laden testen.

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:

async macht 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, pip und 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

  • random für Sicherheit verwenden: Für Spiele okay, für Tokens oder Passwörter secrets verwenden.

  • 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: @decorator ist nur eine Kurzform für eine Funktionsumwandlung.

  • functools.wraps vergessen: Ohne wraps heißen dekorierte Funktionen intern oft nur noch wrapper.

  • 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.

  • async als allgemeines Performance-Wunder sehen: async hilft 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)
Verwendung:
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))
Ausgabe:
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
Verwendung:
zahlen = zaehler(10)

print(next(zahlen))
print(next(zahlen))
print(next(zahlen))
Ausgabe:
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")
Ausgabe:
[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
Mit pytest ausführen:
python -m pytest
Oder mit uv:
uv run pytest

Vertiefungen zu diesem Ausblick

Jedes der angerissenen Themen verdient einen eigenen Artikel:

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

0 Kommentare

Noch keine Kommentare. Sei der/die Erste!