cat posts/python-lernen-06-pythonic-schreiben.md
Python lernen (Teil 6): Pythonic schreiben
Comprehensions, Slicing, enumerate, zip, Unpacking und Truthiness – mit diesen Idiomen wird unser Python-Code kompakter, klarer und leichter zu lesen.
Der Dungeon aus Teil 5 ist inzwischen sauber in Funktionen aufgeteilt. Der Code funktioniert, ist verständlich und lässt sich erweitern.
Heute kommen keine grundlegend neuen Fähigkeiten für das Spiel hinzu. Stattdessen geht es darum, typische Aufgaben so auszudrücken, wie es in Python üblich ist.
Dafür wird häufig der Begriff pythonic verwendet. Pythonic Code nutzt die Möglichkeiten und Idiome der Sprache, statt Schreibweisen aus anderen Programmiersprachen nachzubauen.
Dabei gilt:
Pythonic bedeutet nicht, möglichst wenige Zeichen zu schreiben. Entscheidend ist, dass die Absicht des Codes schnell erkennbar bleibt.
Wir beschäftigen uns unter anderem mit Comprehensions, Truthiness, Slicing,
enumerate, zip und Unpacking. Anschließend wenden wir diese Techniken auf
unseren Dungeon an.
Comprehensions
Angenommen, wir möchten die Namen mehrerer Räume in Großbuchstaben umwandeln.
Mit einer gewöhnlichen Schleife könnten wir eine neue Liste Schritt für Schritt aufbauen:
raum_ids = ["halle", "bibliothek", "krypta"]
namen = []
for raum_id in raum_ids:
namen.append(raum_id.upper())
print(namen)
Die Ausgabe lautet:
['HALLE', 'BIBLIOTHEK', 'KRYPTA']
Python bietet dafür eine kompaktere Schreibweise: die List Comprehension.
namen = [raum_id.upper() for raum_id in raum_ids]
Das Ergebnis ist dasselbe.
Die allgemeine Form lautet:
[ausdruck for element in collection]
Für jedes Element der Collection wird der Ausdruck ausgewertet. Die Ergebnisse landen in einer neuen Liste.
In unserem Beispiel:
[raum_id.upper() for raum_id in raum_ids]
bedeutet das:
- Nimm nacheinander jede
raum_id. - Rufe darauf
.upper()auf. - Speichere jedes Ergebnis in einer neuen Liste.
Elemente in einer Comprehension filtern
Mit einem angehängten if kannst Du nur bestimmte Elemente übernehmen:
gegenstaende = ["Brot", "Fackel", "Seil", "Schlüssel"]
lange_namen = [
gegenstand
for gegenstand in gegenstaende
if len(gegenstand) > 5
]
print(lange_namen)
Die Ausgabe lautet:
['Fackel', 'Schlüssel']
Die allgemeine Form lautet:
[ausdruck for element in collection if bedingung]
Die Bedingung entscheidet, welche Elemente berücksichtigt werden.
Für unsere Dungeon-Welt können wir beispielsweise alle Räume finden, in denen noch Gegenstände liegen:
raeume_mit_loot = [
raum["name"]
for raum in raeume.values()
if raum["gegenstaende"]
]
Hier verwenden wir gleichzeitig eine Comprehension und Truthiness: Eine nicht
leere Liste mit Gegenständen gilt als True, eine leere Liste als False.
List Comprehension oder normale Schleife?
Eine Comprehension eignet sich besonders, wenn Du:
- aus einer Collection eine neue Collection erzeugst,
- jedes Element auf dieselbe Weise verarbeitest,
- Elemente mit einer einfachen Bedingung filterst.
Dieses Beispiel ist übersichtlich:
quadrate = [zahl ** 2 for zahl in range(1, 6)]
Die Ausgabe wäre:
[1, 4, 9, 16, 25]
Eine Comprehension ist aber nicht automatisch besser, nur weil sie kürzer ist.
Dieses Beispiel wird bereits schwerer lesbar:
werte = [
zahl * 2
for gruppe in gruppen
for zahl in gruppe
if zahl > 0
if zahl % 2 == 0
]
In solchen Fällen kann eine normale Schleife klarer sein:
werte = []
for gruppe in gruppen:
for zahl in gruppe:
if zahl > 0 and zahl % 2 == 0:
werte.append(zahl * 2)
Pythonic Code bevorzugt Lesbarkeit gegenüber maximaler Kürze.
Dict Comprehensions
Neben Listen kannst Du auch Dictionaries mit einer Comprehension erzeugen.
Aus diesem Dictionary:
lebenspunkte = {
"Karl": 100,
"Goblin": 30,
"Drache": 999,
}
können wir ein neues Dictionary mit halbierten Lebenspunkten bauen:
halbe_lebenspunkte = {
name: hp // 2
for name, hp in lebenspunkte.items()
}
print(halbe_lebenspunkte)
Die Ausgabe lautet:
{'Karl': 50, 'Goblin': 15, 'Drache': 499}
Die allgemeine Form lautet:
{
schluessel: wert
for element in collection
}
Auch Dict Comprehensions können filtern:
starke_gegner = {
name: hp
for name, hp in lebenspunkte.items()
if hp >= 100
}
Das Ergebnis lautet:
{
"Karl": 100,
"Drache": 999,
}
Set Comprehensions
Auch Sets lassen sich mit einer Comprehension erzeugen:
richtungen = ["Norden", "Süden", "NORDEN", "Osten"]
normalisiert = {
richtung.lower()
for richtung in richtungen
}
print(normalisiert)
Die Ausgabe enthält jeden Wert nur einmal:
{'norden', 'süden', 'osten'}
Die Reihenfolge eines Sets solltest Du dabei nicht als Teil Deiner Programmlogik betrachten.
Die Schreibweise ähnelt einer Dict Comprehension, enthält aber keinen Doppelpunkt:
{ausdruck for element in collection}
Truthiness: Werte als Bedingung verwenden
Python kann viele Werte direkt in einer Bedingung auswerten.
Diese Werte gelten normalerweise als False:
FalseNone- numerische Nullwerte wie
0und0.0 - leere Strings wie
"" - leere Listen wie
[] - leere Tupel wie
() - leere Dictionaries wie
{} - leere Sets wie
set()
Nicht leere oder von null verschiedene Werte gelten normalerweise als True.
Du kannst das mit bool(...) überprüfen:
>>> bool([])
False
>>> bool(["Fackel"])
True
>>> bool("")
False
>>> bool("Dungeon")
True
>>> bool(0)
False
>>> bool(25)
True
>>> bool(None)
False
Eigene Klassen können dieses Verhalten über besondere Methoden beeinflussen. Für die eingebauten Datentypen reichen die genannten Regeln zunächst aus.
Collections ohne len(...) prüfen
Angenommen, Du möchtest prüfen, ob das Inventar mindestens einen Gegenstand enthält.
Diese Schreibweise funktioniert:
if len(inventar) > 0:
print("Du trägst etwas bei Dir.")
Pythonic ist jedoch:
if inventar:
print("Du trägst etwas bei Dir.")
Die zweite Variante drückt direkt aus, worum es geht:
Ist das Inventar nicht leer?
Auch der umgekehrte Fall lässt sich direkt formulieren:
if not inventar:
print("Dein Inventar ist leer.")
Statt:
if len(inventar) == 0:
print("Dein Inventar ist leer.")
Truthiness mit Bedacht verwenden
Truthiness ist praktisch, aber nicht immer eindeutig genug.
Angenommen, gold kann entweder eine Zahl oder None enthalten:
gold = None
Diese Bedingung unterscheidet nicht zwischen None und 0:
if not gold:
print("Kein Gold vorhanden.")
Sowohl None als auch 0 gelten als False.
Möchtest Du ausdrücklich auf None prüfen, verwendest Du:
if gold is None:
print("Der Goldwert wurde noch nicht gesetzt.")
Und für einen echten Zahlenwert von 0:
if gold == 0:
print("Du besitzt keine Goldmünzen.")
Die kürzere Truthiness-Prüfung ist nur dann besser, wenn alle falsy Werte dieselbe Bedeutung haben dürfen.
Bedingte Ausdrücke
Eine einfache Entscheidung lässt sich direkt innerhalb eines Ausdrucks formulieren:
status = "lebendig" if hp > 0 else "besiegt"
Diese Schreibweise heißt Conditional Expression.
Sie entspricht inhaltlich:
if hp > 0:
status = "lebendig"
else:
status = "besiegt"
Ein typisches Beispiel aus unserem Dungeon:
inventar_text = ", ".join(inventar) if inventar else "(leer)"
Diese Form eignet sich für kurze und leicht erkennbare Entscheidungen.
Zu viele verschachtelte Bedingungen werden dagegen schnell unlesbar:
status = "tot" if hp <= 0 else "kritisch" if hp <= 25 else "bereit"
Hier ist eine normale if-elif-else-Struktur klarer.
Slicing: Ausschnitte aus Sequenzen
Mit Slicing kannst Du Teile einer Liste, eines Tupels oder eines Strings auswählen.
Die Grundform lautet:
collection[start:stop]
start ist enthalten, stop dagegen nicht.
zahlen = [0, 1, 2, 3, 4, 5]
print(zahlen[1:4])
Die Ausgabe lautet:
[1, 2, 3]
Der Index 4 gehört nicht mehr zum Ergebnis.
Grenzen weglassen
Lässt Du start weg, beginnt der Slice am Anfang:
zahlen[:3]
Ergebnis:
[0, 1, 2]
Lässt Du stop weg, reicht der Slice bis zum Ende:
zahlen[3:]
Ergebnis:
[3, 4, 5]
Beide Grenzen können fehlen:
kopie = zahlen[:]
Dadurch entsteht eine neue Liste mit denselben Elementen.
Negative Indizes
Negative Indizes zählen vom Ende:
zahlen = [0, 1, 2, 3, 4, 5]
print(zahlen[-2:])
Ausgabe:
[4, 5]
Ein weiteres Beispiel:
inventar = ["Brot", "Fackel", "Seil", "Schlüssel"]
letzte_funde = inventar[-2:]
letzte_funde enthält:
["Seil", "Schlüssel"]
Die Schrittweite beim Slicing
Slicing kann einen dritten Wert für die Schrittweite enthalten:
collection[start:stop:schritt]
Jedes zweite Element:
zahlen = [0, 1, 2, 3, 4, 5]
print(zahlen[::2])
Ausgabe:
[0, 2, 4]
Mit einer negativen Schrittweite kannst Du die Reihenfolge umdrehen:
print(zahlen[::-1])
Ausgabe:
[5, 4, 3, 2, 1, 0]
Das funktioniert auch mit Strings:
wort = "Verlies"
print(wort[:3])
print(wort[::-1])
Ausgabe:
Ver
seilreV
Slicing erzeugt eine flache Kopie
Bei Listen erzeugt ein Slice eine neue Liste:
inventar = ["Fackel", "Brot"]
kopie = inventar[:]
kopie.append("Schlüssel")
print(inventar)
print(kopie)
Ausgabe:
['Fackel', 'Brot']
['Fackel', 'Brot', 'Schlüssel']
Das Hinzufügen des Schlüssels verändert nur die neue Liste.
Dabei handelt es sich allerdings um eine flache Kopie. Enthält die Liste selbst mutable Objekte, werden diese nicht ebenfalls kopiert:
raeume = [
{"name": "Halle"},
{"name": "Krypta"},
]
kopie = raeume[:]
kopie[0]["name"] = "Große Halle"
print(raeume[0]["name"])
Die Ausgabe lautet:
Große Halle
Beide Listen enthalten weiterhin eine Referenz auf dasselbe innere Dictionary.
enumerate: Index und Element gemeinsam
Manchmal benötigen wir beim Durchlaufen einer Collection zusätzlich eine laufende Nummer.
Eine mögliche Schreibweise wäre:
richtungen = ["norden", "osten", "westen"]
for index in range(len(richtungen)):
print(f"{index + 1}) {richtungen[index]}")
Das funktioniert, ist aber unnötig umständlich. Python bietet dafür
enumerate(...):
for nummer, richtung in enumerate(richtungen, start=1):
print(f"{nummer}) {richtung}")
Die Ausgabe lautet:
1) norden
2) osten
3) westen
enumerate(...) liefert bei jedem Durchlauf zwei Werte:
- die laufende Nummer,
- das aktuelle Element.
Durch Unpacking landen beide Werte direkt in den passenden Variablen:
nummer, richtung
Ohne start=1 beginnt enumerate(...) standardmäßig bei 0.
for index, richtung in enumerate(richtungen):
print(index, richtung)
Ausgabe:
0 norden
1 osten
2 westen
Verwende enumerate(...), wenn Du sowohl das Element als auch seine Position
benötigst. Brauchst Du nur das Element, bleibt die direkte Schleife klarer:
for richtung in richtungen:
print(richtung)
zip: Collections parallel durchlaufen
Mit zip(...) kannst Du mehrere Collections gleichzeitig durchlaufen:
namen = ["Goblin", "Ork", "Drache"]
lebenspunkte = [30, 50, 999]
for name, hp in zip(namen, lebenspunkte):
print(f"{name}: {hp} HP")
Die Ausgabe lautet:
Goblin: 30 HP
Ork: 50 HP
Drache: 999 HP
Bei jedem Durchlauf verbindet zip(...) die Elemente an derselben Position:
"Goblin" + 30
"Ork" + 50
"Drache" + 999
Auch hier werden die Paare direkt entpackt:
for name, hp in zip(namen, lebenspunkte):
zip endet bei der kürzesten Collection
Sind die Collections unterschiedlich lang, endet zip(...) standardmäßig bei
der kürzesten:
namen = ["Goblin", "Ork", "Drache"]
lebenspunkte = [30, 50]
for name, hp in zip(namen, lebenspunkte):
print(name, hp)
Der Drache wird nicht verarbeitet. Python erzeugt dabei standardmäßig keinen Fehler.
Seit Python 3.10 kannst Du mit strict=True verlangen, dass alle Collections
gleich lang sein müssen:
for name, hp in zip(namen, lebenspunkte, strict=True):
print(name, hp)
Sind die Längen unterschiedlich, entsteht ein ValueError.
strict=True ist sinnvoll, wenn unterschiedlich lange Daten auf einen Fehler
im Programm oder in den Eingangsdaten hindeuten.
Unpacking
Beim Unpacking verteilst Du die Elemente einer Collection auf mehrere Variablen:
position = (3, 7)
x, y = position
Danach gilt:
x == 3
y == 7
Die Anzahl der Variablen muss normalerweise zur Anzahl der Elemente passen:
x, y = (3, 7, 9)
Dieser Code führt zu einem ValueError, weil drei Werte auf nur zwei Variablen
verteilt werden sollen.
Werte mit Unpacking tauschen
In Python kannst Du zwei Werte ohne Hilfsvariable tauschen:
x = 3
y = 7
x, y = y, x
print(x, y)
Ausgabe:
7 3
Python wertet zuerst die rechte Seite aus und weist die Werte anschließend auf der linken Seite zu.
Mehrere Werte mit * sammeln
Ein Stern sammelt übrig bleibende Werte in einer Liste:
erster, *mitte, letzter = [1, 2, 3, 4, 5]
print(erster)
print(mitte)
print(letzter)
Ausgabe:
1
[2, 3, 4]
5
Ein weiteres Beispiel:
befehl, *argumente = "nimm alte fackel".split()
print(befehl)
print(argumente)
Ausgabe:
nimm
['alte', 'fackel']
Innerhalb einer solchen Zuweisung darf nur ein mit * markierter Name
vorkommen.
_ für nicht benötigte Werte
Der Unterstrich _ wird häufig als Name für einen Wert verwendet, den wir nicht
benötigen.
Die String-Methode partition(...) zerlegt einen String in drei Teile:
befehl = "nimm Fackel"
verb, trennzeichen, argument = befehl.partition(" ")
Danach gilt:
verb == "nimm"
trennzeichen == " "
argument == "Fackel"
Benötigen wir das Trennzeichen nicht, schreiben wir:
verb, _, argument = befehl.partition(" ")
Der Unterstrich besitzt dabei keine besondere technische Bedeutung. Er ist in einem normalen Skript lediglich ein gewöhnlicher Variablenname.
Die Schreibweise signalisiert anderen Entwicklern:
Diesen Wert benötigen wir nicht.
In der interaktiven REPL hat _ zusätzlich die besondere Bedeutung des zuletzt
ausgegebenen Ergebnisses. Diese Funktion gilt nicht allgemein für normale
Skripte.
Befehle mit partition(...) zerlegen
partition(" ") eignet sich gut für Befehle, die aus einem Verb und einem
optionalen Argument bestehen:
eingabe = "nimm alte Fackel"
verb, _, argument = eingabe.partition(" ")
print(verb)
print(argument)
Ausgabe:
nimm
alte Fackel
Im Gegensatz zu split() zerlegt partition(...) nur am ersten Vorkommen des
Trennzeichens. Der Rest bleibt als gemeinsamer String erhalten.
Bei einer Eingabe ohne Leerzeichen:
eingabe = "inventar"
verb, _, argument = eingabe.partition(" ")
erhalten wir:
verb == "inventar"
argument == ""
Da ein leerer String falsy ist, können wir direkt prüfen:
if not argument:
print("Es wurde kein Argument angegeben.")
any(...) und all(...)
Zwei weitere nützliche Built-ins sind any(...) und all(...).
any(...) gibt True zurück, wenn mindestens ein Element truthy ist:
tuer_offen = False
fenster_offen = True
geheimgang_offen = False
if any([tuer_offen, fenster_offen, geheimgang_offen]):
print("Es gibt einen offenen Ausgang.")
all(...) gibt nur dann True zurück, wenn alle Elemente truthy sind:
hat_schluessel = True
hat_fackel = True
hat_karte = False
if all([hat_schluessel, hat_fackel, hat_karte]):
print("Du bist vollständig ausgerüstet.")
In diesem Beispiel ist die Bedingung False, weil hat_karte den Wert False
besitzt.
Für wenige feste Bedingungen ist ein gewöhnliches and oder or oft klarer:
if hat_schluessel and hat_fackel:
print("Du kannst weitergehen.")
any(...) und all(...) sind besonders nützlich, wenn die Werte bereits in
einer Collection vorliegen oder dynamisch erzeugt werden.
Der Dungeon, Schritt 6
Wir wenden die neuen Idiome nun auf den Dungeon aus Teil 5 an.
Dabei ändern wir unter anderem:
- Ausgänge und Inventar werden mit
enumerate(...)nummeriert. - Leere Collections werden über Truthiness geprüft.
- Befehle werden mit
partition(...)in Verb und Argument zerlegt. - Gegenstände können nun gezielt mit
nimm Fackelaufgenommen werden. - Eine List Comprehension liefert Räume, in denen noch Loot liegt.
- Kurze Ausgaben verwenden Conditional Expressions.
Öffne dungeon.py und ersetze den bisherigen Inhalt durch folgenden Code:
# Dungeon – Teil 6: Pythonic schreiben
def erstelle_raeume():
"""Erstellt die Räume des Dungeons und gibt sie zurück."""
return {
"halle": {
"name": "Halle",
"beschreibung": (
"Du stehst in einer dunklen Halle mit moderigem Geruch."
),
"ausgaenge": {
"norden": "bibliothek",
"osten": "krypta",
},
"gegenstaende": ["Fackel"],
},
"bibliothek": {
"name": "Bibliothek",
"beschreibung": (
"Staubige Regale voller zerfledderter Bücher umgeben Dich."
),
"ausgaenge": {
"sueden": "halle",
},
"gegenstaende": ["Schlüssel"],
},
"krypta": {
"name": "Krypta",
"beschreibung": (
"Feuchtigkeit glänzt auf den Wänden der alten Krypta."
),
"ausgaenge": {
"westen": "halle",
},
"gegenstaende": [],
},
}
def zeige_raum(raum):
"""Zeigt den aktuellen Raum, seine Ausgänge und sichtbare Gegenstände."""
print()
print(f"Ort: {raum['name']}")
print(raum["beschreibung"])
print("Ausgänge:")
for nummer, richtung in enumerate(raum["ausgaenge"], start=1):
print(f" {nummer}) {richtung}")
if raum["gegenstaende"]:
print("Hier liegt:", ", ".join(raum["gegenstaende"]))
def zeige_gegenstaende(raum):
"""Zeigt die Gegenstände im aktuellen Raum."""
gegenstaende = raum["gegenstaende"]
text = ", ".join(gegenstaende) if gegenstaende else "nichts Brauchbares"
print(f"Hier liegt: {text}")
def zeige_inventar(inventar):
"""Zeigt das Inventar als nummerierte Liste."""
if not inventar:
print("Dein Inventar ist leer.")
return
print("Inventar:")
for nummer, gegenstand in enumerate(inventar, start=1):
print(f" {nummer}) {gegenstand}")
def nimm_gegenstand(raum, inventar, name):
"""Nimmt einen bestimmten Gegenstand auf und gibt seinen Namen zurück."""
gesuchter_name = name.strip().lower()
for gegenstand in raum["gegenstaende"]:
if gegenstand.lower() == gesuchter_name:
raum["gegenstaende"].remove(gegenstand)
inventar.append(gegenstand)
return gegenstand
return None
def erstelle_statuszeile(spieler):
"""Erstellt eine kompakte Statuszeile für den Spieler."""
return (
f"{spieler['name']} | "
f"HP: {spieler['hp']}/{spieler['max_hp']} | "
f"Gold: {spieler['gold']} | "
f"Inventar: {len(spieler['inventar'])}"
)
def zeige_status(spieler, besuchte_raeume):
"""Zeigt den Player State und einige Statistiken."""
print(erstelle_statuszeile(spieler))
print(f"Besuchte Räume: {len(besuchte_raeume)}")
def teile_befehl(eingabe):
"""Zerlegt den Input in ein Verb und ein optionales Argument."""
verb, _, argument = eingabe.strip().lower().partition(" ")
return verb, argument.strip()
def finde_raeume_mit_loot(raeume):
"""Gibt die Namen aller Räume zurück, in denen Gegenstände liegen."""
return [
raum["name"]
for raum in raeume.values()
if raum["gegenstaende"]
]
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.")
spieler = {
"name": input("Wie heißt Du, Abenteurer? ").strip(),
"hp": 100,
"max_hp": 100,
"gold": int(input("Wie viele Goldmünzen bringst Du mit? ")),
"inventar": [],
}
ort = "halle"
besuchte_raeume = set()
raum_anzeigen = True
print()
print(f"Willkommen, {spieler['name']}!")
print(
"Befehle: umsehen, nimm <Gegenstand>, inventar, status, "
"loot, geh <Richtung>, ende"
)
while True:
raum = raeume[ort]
if raum_anzeigen:
besuchte_raeume.add(ort)
zeige_raum(raum)
raum_anzeigen = False
verb, argument = teile_befehl(input("\n> "))
if verb == "ende":
print("Du verlässt das Verlies. Bis zum nächsten Mal.")
break
elif verb == "umsehen":
zeige_gegenstaende(raum)
elif verb == "nimm":
if not argument:
print("Was möchtest Du nehmen?")
continue
gegenstand = nimm_gegenstand(
raum,
spieler["inventar"],
argument,
)
if gegenstand:
print(f"Du nimmst {gegenstand}.")
else:
print(f"Hier liegt kein Gegenstand namens {argument}.")
elif verb == "inventar":
zeige_inventar(spieler["inventar"])
elif verb == "status":
zeige_status(spieler, besuchte_raeume)
elif 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)
elif verb == "geh":
if argument in raum["ausgaenge"]:
ort = raum["ausgaenge"][argument]
raum_anzeigen = True
else:
print(f"Du kannst nicht nach {argument or 'dort'} gehen.")
elif verb in raum["ausgaenge"] and not argument:
ort = raum["ausgaenge"][verb]
raum_anzeigen = True
else:
print("Das verstehe ich nicht.")
print("Mögliche Ausgänge:", ", ".join(raum["ausgaenge"]))
print(
"Weitere Befehle: umsehen, nimm <Gegenstand>, inventar, "
"status, loot, geh <Richtung>, ende"
)
main()
Führe das Programm aus:
python dungeon.py
Ein möglicher Durchlauf sieht so aus:
Du stehst vor dem rostigen Tor eines vergessenen Verlieses.
Ein kalter Luftzug weht Dir entgegen.
Wie heißt Du, Abenteurer? Karl
Wie viele Goldmünzen bringst Du mit? 50
Willkommen, Karl!
Befehle: umsehen, nimm <Gegenstand>, inventar, status, loot, geh <Richtung>, ende
Ort: Halle
Du stehst in einer dunklen Halle mit moderigem Geruch.
Ausgänge:
1) norden
2) osten
Hier liegt: Fackel
> nimm fackel
Du nimmst Fackel.
> inventar
Inventar:
1) Fackel
> geh norden
Ort: Bibliothek
Staubige Regale voller zerfledderter Bücher umgeben Dich.
Ausgänge:
1) sueden
Hier liegt: Schlüssel
> nimm schlüssel
Du nimmst Schlüssel.
> status
Karl | HP: 100/100 | Gold: 50 | Inventar: 2
Besuchte Räume: 2
> loot
Räume mit Loot: (keine)
> ende
Du verlässt das Verlies. Bis zum nächsten Mal.
Was wurde pythonic?
Schauen wir uns die wichtigsten Änderungen genauer an.
Nummerierte Ausgänge mit enumerate(...)
Statt selbst einen Zähler zu verwalten:
nummer = 1
for richtung in raum["ausgaenge"]:
print(f"{nummer}) {richtung}")
nummer = nummer + 1
verwenden wir:
for nummer, richtung in enumerate(raum["ausgaenge"], start=1):
print(f" {nummer}) {richtung}")
Der Code sagt direkt, dass wir jedes Element zusammen mit einer laufenden Nummer benötigen.
Truthiness für leere Collections
Das Inventar wird ohne len(...) geprüft:
if not inventar:
print("Dein Inventar ist leer.")
return
Auch die Gegenstände eines Raums werden direkt ausgewertet:
if raum["gegenstaende"]:
print("Hier liegt:", ", ".join(raum["gegenstaende"]))
Die Absicht ist dadurch sofort erkennbar.
Early Return
In zeige_inventar(...) beenden wir die Funktion frühzeitig, wenn das Inventar
leer ist:
if not inventar:
print("Dein Inventar ist leer.")
return
Der restliche Funktionskörper benötigt dadurch keinen zusätzlichen
else-Block.
Input mit Unpacking zerlegen
Die Funktion teile_befehl(...) verwendet partition(...) und Unpacking:
verb, _, argument = eingabe.strip().lower().partition(" ")
Aus
nimm alte fackel
werden:
verb == "nimm"
argument == "alte fackel"
Das Trennzeichen selbst wird an _ gebunden, weil wir es nicht benötigen.
Räume mit Loot über eine Comprehension finden
Die Funktion
def finde_raeume_mit_loot(raeume):
return [
raum["name"]
for raum in raeume.values()
if raum["gegenstaende"]
]
beschreibt direkt das gewünschte Ergebnis:
Erzeuge eine Liste mit den Namen aller Räume, deren Gegenstandsliste nicht leer ist.
Eine gewöhnliche Schleife wäre länger:
ergebnis = []
for raum in raeume.values():
if raum["gegenstaende"]:
ergebnis.append(raum["name"])
return ergebnis
Beide Varianten funktionieren. Die Comprehension ist hier kompakt und bleibt gut lesbar.
Conditional Expression für kurze Alternativen
Diese Zeile:
text = ", ".join(gegenstaende) if gegenstaende else "nichts Brauchbares"
ersetzt:
if gegenstaende:
text = ", ".join(gegenstaende)
else:
text = "nichts Brauchbares"
Da beide Alternativen kurz sind und nur einen Wert bestimmen, eignet sich die Conditional Expression hier gut.
Pythonic bedeutet nicht immer kürzer
Einige Stellen des Dungeon-Codes bleiben bewusst als normale Schleifen oder Bedingungen erhalten.
Die Funktion nimm_gegenstand(...) könnte beispielsweise mit einer
Comprehension oder einer Generator Expression noch kompakter geschrieben
werden. Die gewöhnliche Schleife macht den Ablauf aber sehr deutlich:
for gegenstand in raum["gegenstaende"]:
if gegenstand.lower() == gesuchter_name:
raum["gegenstaende"].remove(gegenstand)
inventar.append(gegenstand)
return gegenstand
Die Funktion:
- durchsucht die Gegenstände,
- entfernt den passenden Gegenstand,
- fügt ihn dem Inventar hinzu,
- gibt seinen Namen zurück.
Eine künstlich verkürzte Variante wäre nicht automatisch besser.
Pythonic Code verwendet Sprachmittel dort, wo sie die Absicht klarer machen. Er vermeidet sie, wenn sie den Code nur dichter und schwerer lesbar machen.
Stolperfallen
-
Comprehensions überladen: Mehrere verschachtelte
for- undif-Teile können eine Comprehension schwer verständlich machen. Verwende dann eine normale Schleife. -
Eine Comprehension nur wegen ihrer Kürze verwenden: Wenn der Ausdruck Seiteneffekte ausführt oder viele Schritte enthält, ist eine Schleife meistens klarer.
-
Truthiness zu allgemein einsetzen:
0,None,""und leere Collections sind alle falsy. Prüfe ausdrücklich mitis None, wenn DuNonevon anderen Werten unterscheiden musst. -
Slicing mit einer vollständigen Kopie verwechseln:
liste[:]erzeugt nur eine flache Kopie. Enthaltene mutable Objekte bleiben gemeinsam referenziert. -
Die exklusive Stop-Grenze vergessen: Bei
werte[1:4]sind die Indizes1,2und3enthalten, nicht aber4. -
enumerate(...)verwenden, obwohl der Index nicht benötigt wird: Wenn Du nur die Elemente brauchst, istfor element in collectionklarer. -
Von
zip(...)gleich lange Collections erwarten: Ohnestrict=Trueendetzip(...)bei der kürzesten Collection, ohne einen Fehler zu melden. -
_für besondere Syntax halten: In einem Skript ist_ein gewöhnlicher Variablenname und nur eine Konvention für nicht benötigte Werte. -
Bei
partition(...)drei Rückgabewerte vergessen: Die Methode liefert immer den Teil davor, das Trennzeichen und den Teil danach. -
Mehrere Wörter mit
split()verlieren:split()zerlegt jedes Wort einzeln.partition(" ")lässt den restlichen Text nach dem ersten Leerzeichen zusammen. -
Conditional Expressions verschachteln: Mehrere ineinander verschachtelte Bedingungen sind meist schlechter lesbar als ein normales
if-elif-else.
Übungen
1. Lange Gegenstandsnamen filtern
Erzeuge mit einer List Comprehension aus der folgenden Liste eine neue Liste, die nur Gegenstände mit mehr als fünf Zeichen enthält:
gegenstaende = ["Fackel", "Brot", "Seil", "Schlüssel"]
Lösung
lange_namen = [
gegenstand
for gegenstand in gegenstaende
if len(gegenstand) > 5
]
print(lange_namen)
['Fackel', 'Schlüssel']
2. Das Inventar nummeriert ausgeben
Gib das Inventar mit enumerate(...) nummeriert aus. Die Nummerierung soll bei
1 beginnen.
Lösung
inventar = ["Fackel", "Schlüssel", "Brot"]
for nummer, gegenstand in enumerate(inventar, start=1):
print(f"{nummer}. {gegenstand}")
1. Fackel
2. Schlüssel
3. Brot
3. Die letzten beiden Gegenstände anzeigen
Verwende Slicing, um nur die letzten beiden Gegenstände des Inventars auszugeben.
Lösung
inventar = ["Brot", "Seil", "Fackel", "Schlüssel"]
letzte_funde = inventar[-2:]
print(letzte_funde)
['Fackel', 'Schlüssel']
4. Namen und Lebenspunkte verbinden
Verbinde die folgenden Listen mit zip(...) und gib jeden Gegner zusammen mit
seinen Lebenspunkten aus:
namen = ["Goblin", "Ork", "Drache"]
lebenspunkte = [30, 50, 999]
Lösung
for name, hp in zip(namen, lebenspunkte, strict=True):
print(f"{name}: {hp} HP")
Goblin: 30 HP
Ork: 50 HP
Drache: 999 HP
Vertiefung
Pythonic Code lässt sich teilweise automatisch prüfen und formatieren:
ruff: Linter und Formatter in einemgeplant
Im nächsten Teil
Unsere Räume und der Player State bestehen weiterhin aus verschachtelten
Dictionaries. Das funktioniert, erlaubt aber beispielsweise Tippfehler in
Schlüsseln wie "max_hp" und verteilt das Verhalten auf mehrere Funktionen.
In Teil 7geplant bündeln wir Daten und Verhalten deshalb in Klassen. Aus dem Player State, den Räumen und den Gegenständen werden eigene Objekte mit passenden Attributen und Methoden.
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.