cat posts/python-lernen-10-module-und-projektstruktur.md
Python lernen (Teil 10): Module und Projektstruktur
Code auf mehrere Dateien aufteilen: eigene Module, import, der __main__-Block, klare Verantwortlichkeiten – plus ein erster Einstieg in venv und pip.
Unsere dungeon.py ist über die letzten Teile deutlich gewachsen.
Inzwischen enthält sie:
- Klassen für Spieler, Räume und Gegenstände,
- eigene Exceptions,
- die Weltdefinition,
- Eingabefunktionen,
- Befehlsverarbeitung,
- Speichern und Laden mit JSON,
- den eigentlichen Game-Loop.
Das funktioniert, wird aber zunehmend unübersichtlich. Wenn alles in einer Datei steht, musst Du beim Ändern ständig durch fremden Code scrollen. Außerdem ist nicht mehr sofort erkennbar, welche Teile fachlich zusammengehören.
Heute zerlegen wir den Dungeon in mehrere Dateien.
Dabei lernst Du:
- was ein Modul ist,
- wie
importfunktioniert, - warum Code beim Import ausgeführt wird,
- wofür
if __name__ == "__main__"gut ist, - wie eine kleine Projektstruktur aussehen kann,
- wie
venvundpipfür externe Pakete verwendet werden.
Ein Modul ist eine Python-Datei
Eine Datei mit der Endung .py kann als Modul verwendet werden.
Angenommen, neben Deinem Hauptskript liegt eine Datei werkzeuge.py:
# werkzeuge.py
import random
def wuerfel(seiten=6):
"""Liefert eine zufällige Zahl von 1 bis seiten."""
return random.randint(1, seiten)
Dann kannst Du dieses Modul in einer anderen Datei importieren:
# spiel.py
import werkzeuge
wurf = werkzeuge.wuerfel(20)
print(wurf)
Der Ausdruck
import werkzeuge
macht das Modul unter dem Namen werkzeuge verfügbar.
Die Funktion rufst Du anschließend über den Modulnamen auf:
werkzeuge.wuerfel(20)
Das hat einen Vorteil: Im Code bleibt sichtbar, woher wuerfel(...) stammt.
Einzelne Namen importieren
Du kannst auch gezielt einzelne Namen aus einem Modul importieren:
from werkzeuge import wuerfel
wurf = wuerfel(20)
print(wurf)
Das ist kürzer, weil der Modulname beim Aufruf entfällt.
Beide Formen sind üblich:
import werkzeuge
from werkzeuge import wuerfel
Als grobe Orientierung:
import modulist oft klarer, weil die Herkunft sichtbar bleibt.from modul import nameist praktisch bei häufig verwendeten Namen.from modul import *solltest Du vermeiden.
Diese Schreibweise:
from werkzeuge import *
importiert alle öffentlich sichtbaren Namen des Moduls in den aktuellen Namensraum. Dadurch wird schnell unklar, woher eine Funktion oder Variable stammt.
Die Standardbibliothek besteht ebenfalls aus Modulen
Du hast import bereits mehrfach verwendet:
import json
import logging
from pathlib import Path
Diese Module gehören zur Standardbibliothek. Sie werden mit Python mitgeliefert.
Selbst geschriebene Module und Module der Standardbibliothek werden grundsätzlich mit derselben Syntax importiert.
import json # Standardbibliothek
import werkzeuge # eigene Datei werkzeuge.py
Python sucht das angegebene Modul in bestimmten Verzeichnissen. Für den Einstieg ist vor allem wichtig:
Liegt eine Datei
werkzeuge.pyim selben Ordner wie Dein gestartetes Skript, kann sie mitimport werkzeugeimportiert werden.
Was beim Import passiert
Ein wichtiger Punkt: Beim ersten Import führt Python den Code eines Moduls aus.
Betrachten wir werkzeuge.py:
print("werkzeuge wird geladen")
def wuerfel(seiten=6):
return 4
Und spiel.py:
import werkzeuge
print("Spiel startet")
print(werkzeuge.wuerfel())
Beim Start von spiel.py erscheint:
werkzeuge wird geladen
Spiel startet
4
Das print(...) auf oberster Ebene von werkzeuge.py wird beim Import
ausgeführt.
Deshalb solltest Du Module so schreiben, dass beim Import möglichst wenig passiert. Funktions- und Klassendefinitionen sind unproblematisch. Ein automatisch startender Game-Loop wäre dagegen störend.
Ungünstig:
# welt.py
print("Spiel startet sofort")
main()
Besser:
# welt.py
def main():
...
if __name__ == "__main__":
main()
Dazu kommen wir gleich genauer.
Python importiert ein Modul nur einmal
Wird ein Modul mehrfach importiert, führt Python seinen Code normalerweise nur beim ersten Import aus.
import werkzeuge
import werkzeuge
Der zweite Import verwendet das bereits geladene Modul weiter.
Python merkt sich geladene Module intern in sys.modules. Für den Alltag musst
Du diese Details selten anfassen. Wichtig ist nur: Ein Import ist nicht einfach
ein textuelles Einfügen des Codes an dieser Stelle.
Das unterscheidet Python-Module von einem simplen Kopieren und Einfügen.
Namen sauber aufteilen
Je größer ein Programm wird, desto wichtiger wird die Frage:
Welche Datei ist wofür zuständig?
Eine sinnvolle Aufteilung entsteht meist entlang von Verantwortlichkeiten.
Für unseren Dungeon bietet sich diese Struktur an:
dungeon-projekt/
├── befehle.py # Befehle verarbeiten
├── eingabe.py # Eingabefunktionen
├── fehler.py # eigene Exceptions
├── modelle.py # Klassen: Gegenstand, Spieler, Raum
├── speicher.py # Spielstand speichern und laden
├── spiel.py # main() und Game-Loop
└── welt.py # Gegenstandsdaten und Räume erzeugen
Das ist noch kein installiertes Package mit pyproject.toml, sondern zunächst
nur ein Projektordner mit mehreren Python-Dateien.
Gestartet wird später:
python spiel.py
Wichtig ist: Starte den Befehl im Ordner dungeon-projekt/, also dort, wo
spiel.py und die anderen Dateien liegen.
Warum eine eigene fehler.py?
In Teil 9 hatten wir mehrere eigene Exceptions:
class DungeonError(Exception):
...
class UngueltigerBefehlError(DungeonError):
...
class UnmoeglicheAktionError(DungeonError):
...
class SpielstandError(DungeonError):
...
Diese Exceptions werden von verschiedenen Modulen gebraucht:
modelle.pybrauchtUnmoeglicheAktionError.speicher.pybrauchtSpielstandError.befehle.pybrauchtUngueltigerBefehlError.spiel.pyfängtDungeonError.
Würden wir sie irgendwo zwischen den anderen Modulen verstecken, entstehen leicht zirkuläre Importe.
Deshalb bekommen sie eine eigene Datei.
# fehler.py
class DungeonError(Exception):
"""Basisklasse für erwartbare Fehler im Dungeon."""
class UngueltigerBefehlError(DungeonError):
"""Der eingegebene Befehl ist unbekannt oder unvollständig."""
class UnmoeglicheAktionError(DungeonError):
"""Eine bekannte Aktion ist im aktuellen State nicht möglich."""
class SpielstandError(DungeonError):
"""Der Spielstand kann nicht gespeichert oder geladen werden."""
Dieses Modul enthält keine Spiellogik. Es stellt nur gemeinsame Fehlertypen zur Verfügung.
modelle.py: Klassen auslagern
Die Klassen wandern nach modelle.py.
# modelle.py
from fehler import UnmoeglicheAktionError
class Gegenstand:
"""Ein Gegenstand, der in einem Raum oder Inventar liegen kann."""
def __init__(self, kennung, name, beschreibung=""):
self.kennung = kennung
self.name = name
self.beschreibung = beschreibung
def __str__(self):
return self.name
def untersuche(self):
if self.beschreibung:
return self.beschreibung
return f"An {self.name} fällt Dir nichts Besonderes auf."
class Spieler:
"""Verwaltet den State und das Verhalten des Spielers."""
def __init__(self, name, max_hp=100, gold=0):
name = name.strip()
if not name:
raise ValueError("Der Name darf nicht leer sein.")
if max_hp <= 0:
raise ValueError("max_hp muss größer als 0 sein.")
if gold < 0:
raise ValueError("gold darf nicht negativ sein.")
self.name = name
self.hp = max_hp
self.max_hp = max_hp
self.gold = gold
self.inventar = []
def nimm(self, gegenstand):
self.inventar.append(gegenstand)
def erleide_schaden(self, menge):
if menge < 0:
raise ValueError("Schaden darf nicht negativ sein.")
self.hp = max(self.hp - menge, 0)
def heile(self, menge):
if menge < 0:
raise ValueError("Heilung darf nicht negativ sein.")
self.hp = min(self.hp + menge, self.max_hp)
def ist_besiegt(self):
return self.hp <= 0
def zeige_inventar(self):
if not self.inventar:
print("Dein Inventar ist leer.")
return
print("Inventar:")
for nummer, gegenstand in enumerate(self.inventar, start=1):
print(f" {nummer}) {gegenstand}")
def __str__(self):
return (
f"{self.name} | "
f"HP: {self.hp}/{self.max_hp} | "
f"Gold: {self.gold} | "
f"Inventar: {len(self.inventar)}"
)
class Raum:
"""Ein Raum mit Beschreibung, Ausgängen und Gegenständen."""
def __init__(
self,
name,
beschreibung,
ausgaenge=None,
gegenstaende=None,
):
self.name = name
self.beschreibung = beschreibung
self.ausgaenge = dict(ausgaenge) if ausgaenge is not None else {}
self.gegenstaende = (
list(gegenstaende)
if gegenstaende is not None
else []
)
def beschreibe(self):
print()
print(f"Ort: {self.name}")
print(self.beschreibung)
print("Ausgänge:")
for nummer, richtung in enumerate(self.ausgaenge, start=1):
print(f" {nummer}) {richtung}")
if self.gegenstaende:
print(
"Hier liegt:",
", ".join(str(g) for g in self.gegenstaende),
)
def zeige_gegenstaende(self):
if not self.gegenstaende:
print("Hier liegt nichts Brauchbares.")
return
print(
"Hier liegt:",
", ".join(str(g) for g in self.gegenstaende),
)
def nimm_gegenstand(self, name):
gesuchter_name = name.strip().lower()
for gegenstand in self.gegenstaende:
if gegenstand.name.lower() == gesuchter_name:
self.gegenstaende.remove(gegenstand)
return gegenstand
raise UnmoeglicheAktionError(
f"Hier liegt kein Gegenstand namens {name}."
)
def ziel(self, richtung):
try:
return self.ausgaenge[richtung]
except KeyError:
raise UnmoeglicheAktionError(
f"Du kannst nicht nach {richtung} gehen."
) from None
Dieses Modul kennt nur die Klassen und die Exception, die ein Raum auslösen kann. Es importiert weder den Game-Loop noch die Speicherlogik.
Das ist wichtig: Je weniger ein Modul wissen muss, desto leichter bleibt die Struktur.
welt.py: Räume und Gegenstände erzeugen
Die Weltdefinition kommt in welt.py.
# welt.py
from fehler import SpielstandError
from modelle import Gegenstand, Raum
GEGENSTANDS_DATEN = {
"fackel": {
"name": "Fackel",
"beschreibung": "Eine rußige, aber noch brauchbare Fackel.",
},
"schluessel": {
"name": "Schlüssel",
"beschreibung": "Ein schwerer Eisenschlüssel mit rostigen Zähnen.",
},
}
def erstelle_gegenstand(kennung):
"""Erstellt einen Gegenstand anhand seiner stabilen Kennung."""
try:
daten = GEGENSTANDS_DATEN[kennung]
except KeyError:
raise SpielstandError(
f"Unbekannter Gegenstand im Spielstand: {kennung}"
) from None
return Gegenstand(
kennung=kennung,
name=daten["name"],
beschreibung=daten["beschreibung"],
)
def erstelle_raeume():
"""Erstellt die Dungeon-Welt."""
return {
"halle": Raum(
name="Halle",
beschreibung=(
"Du stehst in einer dunklen Halle mit moderigem Geruch."
),
ausgaenge={
"norden": "bibliothek",
"osten": "krypta",
},
gegenstaende=[erstelle_gegenstand("fackel")],
),
"bibliothek": Raum(
name="Bibliothek",
beschreibung=(
"Staubige Regale voller zerfledderter Bücher umgeben Dich."
),
ausgaenge={
"sueden": "halle",
},
gegenstaende=[erstelle_gegenstand("schluessel")],
),
"krypta": Raum(
name="Krypta",
beschreibung=(
"Feuchtigkeit glänzt auf den Wänden der alten Krypta."
),
ausgaenge={
"westen": "halle",
},
),
}
Dieses Modul ist für den Aufbau der Spielwelt zuständig.
Es importiert:
from modelle import Gegenstand, Raum
weil es Objekte dieser Klassen erzeugt.
eingabe.py: Eingaben bündeln
Die Eingabefunktionen aus Teil 8 wandern nach eingabe.py.
# eingabe.py
from fehler import UngueltigerBefehlError
def frage_ganzzahl(text, minimum=None):
"""Fragt so lange nach einer Ganzzahl, bis die Eingabe gültig ist."""
while True:
eingabe = input(text)
try:
wert = int(eingabe)
except ValueError:
print("Bitte gib eine ganze Zahl ein.")
continue
if minimum is not None and wert < minimum:
print(f"Der Wert muss mindestens {minimum} betragen.")
continue
return wert
def frage_name(text):
"""Fragt so lange nach, bis ein nicht leerer Name eingegeben wurde."""
while True:
name = input(text).strip()
if name:
return name
print("Der Name darf nicht leer sein.")
def teile_befehl(eingabe):
"""Zerlegt den Input in ein Verb und ein optionales Argument."""
verb, _, argument = eingabe.strip().lower().partition(" ")
if not verb:
raise UngueltigerBefehlError(
"Bitte gib einen Befehl ein."
)
return verb, argument.strip()
Dieses Modul enthält keine Dungeon-Welt und keine Spielerklasse. Es kümmert sich nur um Benutzereingaben.
speicher.py: Speichern und Laden
Die Speicherlogik bekommt ein eigenes Modul.
# speicher.py
import json
from pathlib import Path
from fehler import SpielstandError
from welt import erstelle_gegenstand
SPEICHERDATEI = Path("spielstand.json")
def gegenstands_kennungen(gegenstaende):
"""Gibt die stabilen Kennungen mehrerer Gegenstände zurück."""
return [
gegenstand.kennung
for gegenstand in gegenstaende
]
def gegenstaende_aus_kennungen(kennungen):
"""Erstellt Gegenstände aus ihren gespeicherten Kennungen."""
return [
erstelle_gegenstand(kennung)
for kennung in kennungen
]
def erstelle_spielstand(spieler, ort, raeume, besuchte_raeume):
"""Wandelt den aktuellen State in JSON-kompatible Daten um."""
return {
"version": 1,
"ort": ort,
"besuchte_raeume": sorted(besuchte_raeume),
"spieler": {
"name": spieler.name,
"hp": spieler.hp,
"max_hp": spieler.max_hp,
"gold": spieler.gold,
"inventar": gegenstands_kennungen(spieler.inventar),
},
"raeume": {
raum_id: {
"gegenstaende": gegenstands_kennungen(
raum.gegenstaende
),
}
for raum_id, raum in raeume.items()
},
}
def wende_spielstand_an(daten, spieler, raeume):
"""Überträgt geladene Daten auf Spieler und Räume."""
if daten.get("version") != 1:
raise SpielstandError(
"Der Spielstand hat eine unbekannte Version."
)
spieler_daten = daten["spieler"]
spieler.name = spieler_daten["name"]
spieler.hp = spieler_daten["hp"]
spieler.max_hp = spieler_daten["max_hp"]
spieler.gold = spieler_daten["gold"]
spieler.inventar = gegenstaende_aus_kennungen(
spieler_daten["inventar"]
)
for raum_id, raum_daten in daten["raeume"].items():
if raum_id not in raeume:
raise SpielstandError(
f"Unbekannter Raum im Spielstand: {raum_id}"
)
raeume[raum_id].gegenstaende = gegenstaende_aus_kennungen(
raum_daten["gegenstaende"]
)
ort = daten["ort"]
if ort not in raeume:
raise SpielstandError(
f"Unbekannter aktueller Raum im Spielstand: {ort}"
)
return ort, set(daten.get("besuchte_raeume", []))
def speichere_spielstand(spieler, ort, raeume, besuchte_raeume):
"""Schreibt den aktuellen Spielstand als JSON-Datei."""
daten = erstelle_spielstand(
spieler=spieler,
ort=ort,
raeume=raeume,
besuchte_raeume=besuchte_raeume,
)
try:
with SPEICHERDATEI.open("w", encoding="utf-8") as datei:
json.dump(
daten,
datei,
ensure_ascii=False,
indent=2,
)
datei.write("\n")
except OSError as fehler:
raise SpielstandError(
f"Der Spielstand konnte nicht gespeichert werden: {fehler}"
) from fehler
def lade_spielstand(spieler, raeume):
"""Lädt den Spielstand und gibt Ort sowie besuchte Räume zurück."""
if not SPEICHERDATEI.exists():
raise SpielstandError("Kein Spielstand gefunden.")
try:
with SPEICHERDATEI.open(encoding="utf-8") as datei:
daten = json.load(datei)
except json.JSONDecodeError as fehler:
raise SpielstandError(
"Der Spielstand ist keine gültige JSON-Datei."
) from fehler
except OSError as fehler:
raise SpielstandError(
f"Der Spielstand konnte nicht gelesen werden: {fehler}"
) from fehler
try:
return wende_spielstand_an(
daten=daten,
spieler=spieler,
raeume=raeume,
)
except (KeyError, TypeError) as fehler:
raise SpielstandError(
"Der Spielstand hat ein unerwartetes Format."
) from fehler
Dieses Modul importiert aus welt.py die Funktion erstelle_gegenstand(...),
weil gespeicherte Gegenstandskennungen beim Laden wieder zu echten Objekten
werden müssen.
befehle.py: Befehlsverarbeitung auslagern
Die Befehlslogik kommt in befehle.py.
# befehle.py
from fehler import UngueltigerBefehlError
def finde_raeume_mit_loot(raeume):
"""Gibt die Namen aller Räume mit Gegenständen zurück."""
return [
raum.name
for raum in raeume.values()
if raum.gegenstaende
]
def verarbeite_befehl(
verb,
argument,
spieler,
raum,
raeume,
besuchte_raeume,
):
"""
Verarbeitet einen Befehl.
Gibt die ID eines neuen Raums oder None zurück.
"""
if verb == "umsehen":
raum.zeige_gegenstaende()
return None
if verb == "nimm":
if not argument:
raise UngueltigerBefehlError(
"Was möchtest Du nehmen?"
)
gegenstand = raum.nimm_gegenstand(argument)
spieler.nimm(gegenstand)
print(f"Du nimmst {gegenstand}.")
print(gegenstand.untersuche())
return None
if verb == "inventar":
spieler.zeige_inventar()
return None
if verb == "status":
print(spieler)
print(f"Besuchte Räume: {len(besuchte_raeume)}")
return None
if verb == "loot":
raeume_mit_loot = finde_raeume_mit_loot(raeume)
text = (
", ".join(raeume_mit_loot)
if raeume_mit_loot
else "(keine)"
)
print("Räume mit Loot:", text)
return None
if verb == "geh":
if not argument:
raise UngueltigerBefehlError(
"In welche Richtung möchtest Du gehen?"
)
return raum.ziel(argument)
if not argument:
try:
return raum.ausgaenge[verb]
except KeyError:
pass
raise UngueltigerBefehlError(
f"Den Befehl '{verb}' verstehe ich nicht."
)
Diese Datei kennt die Regeln für Befehle. Sie muss aber nicht wissen, wie der Spielstand gespeichert oder wie die Welt aufgebaut wird.
spiel.py: das Hauptprogramm
spiel.py führt alles zusammen.
# spiel.py
from befehle import verarbeite_befehl
from eingabe import frage_ganzzahl, frage_name, teile_befehl
from fehler import DungeonError
from modelle import Spieler
from speicher import lade_spielstand, speichere_spielstand
from welt import erstelle_raeume
def main():
"""Startet den Dungeon und führt den Game-Loop aus."""
raeume = erstelle_raeume()
print("Du stehst vor dem rostigen Tor eines vergessenen Verlieses.")
print("Ein kalter Luftzug weht Dir entgegen.")
name = frage_name("Wie heißt Du, Abenteurer? ")
gold = frage_ganzzahl(
"Wie viele Goldmünzen bringst Du mit? ",
minimum=0,
)
spieler = Spieler(
name=name,
max_hp=100,
gold=gold,
)
ort = "halle"
besuchte_raeume = set()
raum_anzeigen = True
print()
print(f"Willkommen, {spieler.name}!")
print(
"Befehle: umsehen, nimm <Gegenstand>, inventar, status, "
"loot, speichern, laden, geh <Richtung>, ende"
)
while True:
raum = raeume[ort]
if raum_anzeigen:
besuchte_raeume.add(ort)
raum.beschreibe()
raum_anzeigen = False
eingabe = input("\n> ")
try:
verb, argument = teile_befehl(eingabe)
if verb == "ende":
print("Du verlässt das Verlies. Bis zum nächsten Mal.")
break
if verb == "speichern":
speichere_spielstand(
spieler=spieler,
ort=ort,
raeume=raeume,
besuchte_raeume=besuchte_raeume,
)
print("Spielstand gespeichert.")
continue
if verb == "laden":
ort, besuchte_raeume = lade_spielstand(
spieler=spieler,
raeume=raeume,
)
raum_anzeigen = True
print("Spielstand geladen.")
continue
neuer_ort = verarbeite_befehl(
verb=verb,
argument=argument,
spieler=spieler,
raum=raum,
raeume=raeume,
besuchte_raeume=besuchte_raeume,
)
except DungeonError as fehler:
print(fehler)
else:
if neuer_ort is not None:
ort = neuer_ort
raum_anzeigen = True
if __name__ == "__main__":
main()
Jetzt ist spiel.py vor allem Koordination:
- Welt erstellen,
- Spieler anlegen,
- Eingaben lesen,
- Befehle weiterreichen,
- Speichern und Laden auslösen.
Die Details liegen in den jeweiligen Modulen.
Das Projekt starten
Wechsle in den Projektordner:
cd dungeon-projekt
Starte dann:
python spiel.py
Wichtig: Starte nicht aus einem beliebigen anderen Verzeichnis, solange wir noch keine richtige Package-Struktur verwenden.
Diese Struktur:
dungeon-projekt/
├── befehle.py
├── eingabe.py
├── fehler.py
├── modelle.py
├── speicher.py
├── spiel.py
└── welt.py
ist für den Anfang bewusst einfach gehalten.
Ein installierbares Package mit src/, pyproject.toml und sauberem
Package-Namen ist ein späterer Schritt.
__name__ verstehen
Jedes Modul besitzt eine Variable namens __name__.
Lege eine Datei namenstest.py an:
print(__name__)
Startest Du sie direkt:
python namenstest.py
lautet die Ausgabe:
__main__
Wird dieselbe Datei dagegen importiert:
import namenstest
lautet __name__ im Modul:
namenstest
Python unterscheidet also:
- Datei wird direkt gestartet:
__name__ == "__main__" - Datei wird importiert:
__name__ == "modulname"
Genau darauf basiert die bekannte Schreibweise:
if __name__ == "__main__":
main()
Warum der __main__-Block wichtig ist
Ohne Schutz würde ausführbarer Code beim Import direkt laufen.
Ungünstig:
# spiel.py
def main():
print("Spiel startet")
main()
Wenn ein anderes Modul nun spiel.py importiert, startet das Spiel sofort:
import spiel
Das ist meistens unerwünscht.
Besser:
# spiel.py
def main():
print("Spiel startet")
if __name__ == "__main__":
main()
Jetzt gilt:
python spiel.py
startet das Spiel.
Aber:
import spiel
definiert nur die Funktion main(), ohne sie auszuführen.
Das macht Module wiederverwendbar und testbarer.
Ausführbarer Code gehört in main()
Schlecht:
name = input("Wie heißt Du? ")
spieler = Spieler(name)
Dieser Code läuft sofort beim Import.
Besser:
def main():
name = input("Wie heißt Du? ")
spieler = Spieler(name)
if __name__ == "__main__":
main()
Die Faustregel:
Auf oberster Ebene eines Moduls stehen Imports, Konstanten, Klassen und Funktionen. Der eigentliche Programmstart gehört in
main().
Das verhindert überraschende Seiteneffekte beim Import.
Import-Stile
Es gibt mehrere Arten, Namen zu importieren.
Ganzes Modul importieren
import speicher
speicher.speichere_spielstand(...)
Vorteil: Die Herkunft bleibt sichtbar.
Einzelne Namen importieren
from speicher import speichere_spielstand
speichere_spielstand(...)
Vorteil: Kürzer bei häufig verwendeten Funktionen.
Alias verwenden
import pathlib as pl
Das ist erlaubt, aber nicht immer sinnvoll. Aliase sind besonders bei sehr etablierten Konventionen üblich, zum Beispiel:
import numpy as np
Für eigene kleine Module solltest Du Namen meistens nicht unnötig abkürzen.
Star Imports vermeiden
from speicher import *
Diese Form solltest Du in normalem Anwendungscode vermeiden.
Sie macht unklar:
- welche Namen importiert wurden,
- aus welchem Modul ein Name stammt,
- ob lokale Namen überschrieben werden.
Explizite Imports sind fast immer besser.
Absolute Imports im einfachen Projekt
In unserer aktuellen Struktur verwenden wir Imports wie:
from modelle import Spieler
from welt import erstelle_raeume
from speicher import lade_spielstand
Das funktioniert, weil alle Dateien im selben Projektordner liegen und
spiel.py von dort gestartet wird.
Diese Imports sind für den aktuellen Lernstand in Ordnung.
Später, wenn aus dem Dungeon ein richtiges Package wird, sehen Imports eher so aus:
from dungeon.modelle import Spieler
from dungeon.welt import erstelle_raeume
Oder innerhalb eines Packages relativ:
from .modelle import Spieler
Das behandeln wir hier noch nicht vollständig. Für heute reicht:
Mehrere
.py-Dateien im selben Ordner können einander über ihren Modulnamen importieren.
Zirkuläre Importe
Ein häufiger Stolperstein sind zirkuläre Importe.
Beispiel:
# a.py
from b import funktion_b
def funktion_a():
...
# b.py
from a import funktion_a
def funktion_b():
...
a importiert b, während b wieder a importiert.
Solche Abhängigkeiten führen schnell zu Fehlern wie:
ImportError: cannot import name 'funktion_a' from partially initialized module 'a'
Das bedeutet meist: Python hat ein Modul noch nicht fertig geladen, aber ein anderes Modul versucht bereits, daraus Namen zu importieren.
Zirkuläre Importe sind oft ein Zeichen dafür, dass Verantwortlichkeiten nicht sauber getrennt sind.
Typische Lösungen:
- gemeinsame Dinge in ein drittes Modul auslagern,
- Imports anders herum vermeiden,
- Funktionen so umbauen, dass Werte als Argumente übergeben werden,
- Code mit Seiteneffekten aus der Modulebene entfernen.
Unsere fehler.py ist genau so ein drittes gemeinsames Modul. Mehrere Dateien
dürfen daraus Exceptions importieren, ohne einander direkt importieren zu
müssen.
__pycache__
Nach dem Ausführen Deines Projekts siehst Du vielleicht einen Ordner:
__pycache__/
Darin speichert Python kompilierte Bytecode-Dateien. Sie beschleunigen spätere Starts und Imports.
Du musst diesen Ordner nicht bearbeiten.
In Git sollte er normalerweise ignoriert werden:
__pycache__/
Wenn Du den Ordner löschst, ist das nicht schlimm. Python erzeugt ihn bei Bedarf neu.
Fremder Code: Pakete installieren
Bisher verwendet der Dungeon nur die Standardbibliothek:
jsonpathlib- später vielleicht
random logging
Dafür musst Du nichts installieren.
Früher oder später möchtest Du aber ein Paket nutzen, das nicht mit Python mitgeliefert wird.
Beispiele:
richfür schönere Terminalausgabe,requestsfür HTTP-Anfragen,pytestfür Tests,rufffür Linting und Formatierung.
Solche Pakete kommen typischerweise aus dem Python Package Index, kurz PyPI, und
werden mit pip installiert.
Aber: Installiere Pakete für Projekte möglichst nicht einfach systemweit.
Dafür gibt es virtuelle Umgebungen.
Was ist eine virtuelle Umgebung?
Eine virtuelle Umgebung ist ein eigener kleiner Python-Bereich für ein Projekt.
Sie enthält:
- einen Python-Interpreter beziehungsweise Verweise darauf,
- eigene installierte Pakete,
- eigene Skripte wie
pip, - eigene Metadaten.
Dadurch können verschiedene Projekte unterschiedliche Paketversionen verwenden, ohne sich gegenseitig zu stören.
Beispiel:
projekt-a braucht rich==13.x
projekt-b braucht rich==14.x
Mit getrennten virtuellen Umgebungen ist das kein Problem.
Eine venv anlegen
Python bringt das Modul venv mit.
Im Projektordner:
python -m venv .venv
Dadurch entsteht ein Ordner .venv/.
Der Name .venv ist eine verbreitete Konvention. Du könntest die Umgebung auch
anders nennen, aber .venv wird von vielen Editoren und Tools automatisch
erkannt.
Die venv aktivieren
Unter macOS und Linux:
source .venv/bin/activate
Unter Windows PowerShell:
.venv\Scripts\Activate.ps1
Unter Windows cmd.exe:
.venv\Scripts\activate.bat
Nach der Aktivierung zeigt der Prompt häufig den Namen der Umgebung:
(.venv)
Nun verwenden python und pip normalerweise die Umgebung aus .venv.
Prüfen kannst Du das mit:
python --version
python -m pip --version
Warum python -m pip?
Du wirst oft sehen:
pip install rich
Das funktioniert häufig.
Robuster ist aber:
python -m pip install rich
Damit führst Du pip ausdrücklich mit genau dem Python aus, das gerade aktiv
ist.
Das verhindert typische Verwechslungen wie:
- Paket wurde für eine andere Python-Version installiert.
- Paket wurde außerhalb der venv installiert.
pipundpythonzeigen auf unterschiedliche Umgebungen.
Für Anfänger ist python -m pip ... deshalb eine gute Gewohnheit.
Ein Paket installieren
In der aktivierten venv:
python -m pip install rich
Danach kannst Du rich im Projekt verwenden:
from rich import print
print("[bold green]Willkommen im Dungeon![/bold green]")
Unser Dungeon braucht dieses Paket nicht. Es dient nur als Beispiel für ein externes Paket.
Installierte Pakete anzeigen:
python -m pip list
Informationen zu einem Paket:
python -m pip show rich
Ein Paket wieder entfernen:
python -m pip uninstall rich
Die venv verlassen
Mit:
deactivate
verlässt Du die virtuelle Umgebung.
Der (.venv)-Hinweis verschwindet.
Die installierten Pakete bleiben im .venv-Ordner erhalten und stehen wieder
zur Verfügung, sobald Du die Umgebung erneut aktivierst.
.venv nicht ins Git einchecken
Der Ordner .venv/ kann viele Dateien enthalten und ist nur eine lokale
Arbeitsumgebung.
Er gehört normalerweise nicht ins Git-Repository.
Ergänze Deine .gitignore:
.venv/
__pycache__/
*.pyc
Statt die komplette Umgebung zu speichern, dokumentierst Du, welche Pakete das Projekt benötigt.
requirements.txt
Eine einfache Möglichkeit ist eine requirements.txt.
Installierte Pakete in eine Datei schreiben:
python -m pip freeze > requirements.txt
Die Datei enthält dann Zeilen wie:
rich==14.0.0
Auf einem anderen Rechner kann die Umgebung so wiederhergestellt werden:
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
Unter Windows aktivierst Du die venv entsprechend mit dem passenden Befehl aus dem vorherigen Abschnitt.
requirements.txt ist ein einfacher Einstieg. Moderne Python-Projekte verwenden
häufig zusätzlich oder stattdessen eine pyproject.toml. Das wird relevant,
wenn ein Projekt installierbar werden oder seine Metadaten sauber beschreiben
soll.
Für diesen Teil reicht requirements.txt.
Projektstruktur mit venv
Ein kleiner Projektordner könnte jetzt so aussehen:
dungeon-projekt/
├── .venv/ # lokale virtuelle Umgebung, nicht in Git
├── befehle.py
├── eingabe.py
├── fehler.py
├── modelle.py
├── requirements.txt
├── speicher.py
├── spiel.py
└── welt.py
In .gitignore:
.venv/
__pycache__/
*.pyc
spielstand.json
Ob spielstand.json ins Git gehört, hängt vom Projekt ab. Für einen lokalen
Spielstand normalerweise nicht.
pip, venv und uv
venv und pip gehören zum klassischen Standard-Handwerkszeug:
python -m venv .venv
python -m pip install paketname
Das solltest Du kennen, weil es in vielen Projekten, Dokumentationen und Fehlermeldungen auftaucht.
Es gibt inzwischen modernere Werkzeuge, die vieles davon komfortabler und
schneller machen. Eines davon ist uv.
Mit uv lassen sich unter anderem virtuelle Umgebungen, Paketinstallation und
Lockfiles moderner verwalten. Das ist aber ein eigenes Thema.
Für heute ist wichtig:
venvisoliert die Umgebung.pipinstalliert Pakete in diese Umgebung.
Stolperfallen
-
Code auf Modulebene ausführen: Alles auf oberster Ebene eines Moduls läuft beim Import. Starte Programme über
main()und den__main__-Block. -
if __name__ == "__main__"falsch schreiben: Es sind jeweils zwei Unterstriche:__name__und"__main__". -
Aus dem falschen Ordner starten: Unsere einfachen Imports funktionieren nur zuverlässig, wenn Du
python spiel.pyim Projektordner ausführst. -
from modul import *verwenden: Star Imports machen unklar, woher Namen kommen, und können vorhandene Namen überschreiben. -
Zirkuläre Importe erzeugen: Wenn zwei Module einander importieren, ist die Aufteilung oft nicht sauber. Gemeinsame Dinge gehören in ein drittes Modul.
-
Module mit Paketnamen verwechseln: Eine Datei
json.pyin Deinem Projekt kann das Standardbibliotheksmoduljsonverdecken. Benenne eigene Module nicht wie bekannte Standardmodule oder installierte Pakete. -
Nur wegen der Dateianzahl aufteilen: Mehr Dateien sind nicht automatisch besser. Ein Modul sollte eine erkennbare Verantwortung haben.
-
Die venv vergessen zu aktivieren: Dann installierst Du Pakete womöglich in die falsche Umgebung.
-
pipundpythonaus verschiedenen Umgebungen verwenden: Nutzepython -m pip, damitpipzum aktiven Python gehört. -
.venv/ins Git einchecken: Die virtuelle Umgebung ist lokal und sollte ignoriert werden. -
pip freezeblind als Projektbeschreibung verstehen:requirements.txtist ein praktischer Einstieg, aber keine vollständige Projektmetadatei. -
__pycache__/bearbeiten: Das ist ein automatisch erzeugter Cache. In Git ignorieren, ansonsten in Ruhe lassen.
Übungen
1. Ein eigenes Modul anlegen
Lege eine Datei werkzeuge.py an:
# werkzeuge.py
import random
def wuerfel(seiten=6):
"""Liefert eine zufällige Zahl von 1 bis seiten."""
return random.randint(1, seiten)
Lege daneben eine Datei test_wuerfel.py an und importiere die Funktion.
Lösung
# test_wuerfel.py
from werkzeuge import wuerfel
print(wuerfel())
print(wuerfel(20))
python test_wuerfel.py
2. __name__ beobachten
Lege eine Datei namenstest.py an:
print(__name__)
Starte sie direkt und importiere sie anschließend aus einer zweiten Datei.
Lösung
Direkt starten:python namenstest.py
__main__
import namenstest
python import_test.py
namenstest
3. Einen main()-Block ergänzen
Schreibe eine Datei hallo.py, die beim direkten Start eine Funktion main()
ausführt, beim Import aber nichts ausgibt.
Lösung
def main():
print("Hallo aus main()")
if __name__ == "__main__":
main()
python hallo.py
import hallo
4. Eine venv anlegen und ein Paket installieren
Lege eine virtuelle Umgebung an, aktiviere sie und installiere rich.
Lösung
venv anlegen:python -m venv .venv
source .venv/bin/activate
.venv\Scripts\Activate.ps1
python -m pip install rich
from rich import print
print("[bold green]rich funktioniert[/bold green]")
5. Abhängigkeiten festhalten
Erzeuge eine requirements.txt aus der aktivierten venv.
Lösung
python -m pip freeze > requirements.txt
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
Vertiefung
Der schnelle, moderne Nachfolger beziehungsweise Ergänzer zu vielen
venv- und pip-Workflows:
uv: pip und venv in schnellgeplant
Wie aus einem Skript ein echtes Kommandozeilen-Tool wird:
Vom Skript zum echten CLIgeplant
Wie Ruff in ein Projekt eingebunden wird:
Ruff: Linter und Formatter in einem
Im nächsten Teil
Im letzten Teilgeplant bringen wir das Spiel mit weiteren Modulen aus der Standardbibliothek zum Funkeln.
Wir schauen auf random, collections, itertools und andere nützliche
Werkzeuge, streifen Generatoren und Decorators und ordnen ein, wohin die Reise
nach dieser Serie weitergehen kann: Type Hints, Tests, Packaging und async.
Weiterlesen
- Python lernen, Teil 9: Dateien und Speichern
- Ruff: Linter und Formatter in einem
- Python-Dokumentation: Module
- Python-Dokumentation:
__main__ - Python-Dokumentation:
venv - Python-Dokumentation: Virtuelle Umgebungen und Pakete
- Python Packaging User Guide: Installing packages with pip and virtual environments
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.