cat posts/python-lernen-05-funktionen.md
Python lernen (Teil 5): Funktionen
Funktionen zerlegen Code in benannte, wiederverwendbare Blöcke. Mit Parametern, Rückgabewerten und Scope wird unser Game-Loop wieder übersichtlich.
In Teil 4 haben wir die Räume unseres Dungeons als Daten beschrieben und den Player State in einem Dictionary zusammengefasst. Dadurch müssen wir nicht mehr für jeden Raum eigenen Code schreiben.
Unser Game-Loop funktioniert, enthält inzwischen aber viele unterschiedliche Aufgaben: Er zeigt Räume an, verwaltet das Inventar, gibt den Player State aus und verarbeitet die Befehle des Spielers.
Heute zerlegen wir diese Logik in Funktionen. Eine Funktion bündelt zusammengehörigen Code unter einem Namen. Dadurch können wir ihn gezielt aufrufen, wiederverwenden und unabhängig vom restlichen Programm weiterentwickeln.
Eine Funktion definieren
Eine Funktion wird mit dem Keyword def definiert:
def begruesse():
print("Willkommen im Verlies!")
Nach def folgen:
- der Name der Funktion,
- runde Klammern,
- ein Doppelpunkt,
- der eingerückte Funktionskörper.
Die Definition allein führt den Funktionskörper noch nicht aus:
def begruesse():
print("Willkommen im Verlies!")
Erst der Aufruf mit runden Klammern startet die Funktion:
begruesse()
Die Ausgabe lautet:
Willkommen im Verlies!
Du kannst die Funktion beliebig oft aufrufen:
begruesse()
begruesse()
begruesse()
Dabei wird ihr Funktionskörper bei jedem Aufruf erneut ausgeführt.
Funktionen sinnvoll benennen
Der Name einer Funktion sollte möglichst deutlich machen, was sie tut.
Gute Funktionsnamen beginnen häufig mit einem Verb:
zeige_raum()
berechne_schaden()
nimm_gegenstaende()
erstelle_dungeon()
Wenig aussagekräftig wären dagegen Namen wie:
mach()
funktion1()
daten()
Für Funktionsnamen gelten dieselben grundlegenden Regeln wie für
Variablennamen. Mehrere Wörter werden üblicherweise in snake_case
geschrieben:
zeige_player_state()
berechne_verbleibende_hp()
Parameter: Werte an eine Funktion übergeben
Die bisherige Funktion gibt immer denselben Text aus. Soll sie mit unterschiedlichen Werten arbeiten, erhält sie Parameter:
def begruesse(name):
print(f"Willkommen, {name}!")
Beim Aufruf übergeben wir einen konkreten Wert:
begruesse("Karl")
begruesse("Lara")
Die Ausgabe lautet:
Willkommen, Karl!
Willkommen, Lara!
Innerhalb der Funktion verhält sich name wie eine lokale Variable. Bei jedem
Aufruf erhält sie den jeweils übergebenen Wert.
Genau genommen unterscheiden wir zwischen Parameter und Argument:
nameist der Parameter in der Funktionsdefinition."Karl"ist das Argument beim Funktionsaufruf.
Im Alltag werden beide Begriffe allerdings häufig nicht streng voneinander getrennt.
Mehrere Parameter
Eine Funktion kann mehrere Parameter besitzen:
def zeige_status(name, hp, max_hp):
print(f"{name} | HP: {hp}/{max_hp}")
Beim Aufruf werden die Argumente in derselben Reihenfolge zugeordnet:
zeige_status("Karl", 75, 100)
Die Ausgabe lautet:
Karl | HP: 75/100
Die Zuordnung sieht dabei so aus:
| Parameter | Argument |
|---|---|
name |
"Karl" |
hp |
75 |
max_hp |
100 |
Diese Form wird als Übergabe über die Position bezeichnet.
Keyword Arguments
Du kannst Argumente beim Aufruf auch ausdrücklich über den Namen des Parameters zuordnen:
zeige_status(name="Karl", hp=75, max_hp=100)
Solche Argumente heißen Keyword Arguments.
Bei ihnen spielt die Reihenfolge keine Rolle:
zeige_status(max_hp=100, name="Karl", hp=75)
Das Ergebnis ist dasselbe.
Keyword Arguments können längere Aufrufe leichter lesbar machen:
zeige_status(
name="Karl",
hp=75,
max_hp=100,
)
Du kannst Positionsargumente und Keyword Arguments kombinieren. Dabei müssen die Positionsargumente zuerst stehen:
zeige_status("Karl", hp=75, max_hp=100)
Diese Schreibweise wäre dagegen ungültig:
zeige_status(name="Karl", 75, 100)
Nach einem Keyword Argument dürfen keine einfachen Positionsargumente mehr folgen.
Default-Werte
Parameter können einen Default-Wert besitzen. Dieser wird verwendet, wenn beim Aufruf kein eigenes Argument angegeben wird:
def verursache_schaden(menge=10):
print(f"Du verlierst {menge} Lebenspunkte.")
Ohne Argument verwendet die Funktion den Default-Wert:
verursache_schaden()
Ausgabe:
Du verlierst 10 Lebenspunkte.
Du kannst ihn beim Aufruf überschreiben:
verursache_schaden(25)
Ausgabe:
Du verlierst 25 Lebenspunkte.
Auch ein Keyword Argument ist möglich:
verursache_schaden(menge=25)
Parameter ohne Default-Wert müssen vor Parametern mit Default-Wert stehen:
def zeige_status(name, hp, max_hp=100):
print(f"{name} | HP: {hp}/{max_hp}")
Jetzt können wir max_hp beim Aufruf weglassen:
zeige_status("Karl", 75)
Python verwendet automatisch den Wert 100.
Ein Pflichtparameter darf nicht hinter einem Parameter mit Default-Wert stehen:
# Ungültig:
def zeige_status(max_hp=100, name):
print(f"{name} | HP: {max_hp}")
Python könnte sonst Positionsargumente nicht eindeutig zuordnen.
return: Ein Ergebnis zurückgeben
Eine Funktion kann nicht nur etwas ausgeben, sondern auch ein Ergebnis an die aufrufende Stelle zurückgeben.
Dafür verwenden wir return:
def erstelle_statuszeile(name, hp, max_hp=100):
return f"{name} | HP: {hp}/{max_hp}"
Beim Aufruf erhalten wir den zurückgegebenen String:
status = erstelle_statuszeile("Karl", 75)
print(status)
Die Ausgabe lautet:
Karl | HP: 75/100
Der wichtige Unterschied:
print(...)zeigt einen Wert im Terminal an.returngibt einen Wert an den aufrufenden Code zurück.
Mit einem Return Value kannst Du anschließend weiterarbeiten:
status = erstelle_statuszeile("Karl", 75)
laenge = len(status)
print(f"Die Statuszeile ist {laenge} Zeichen lang.")
Eine Funktion, die nur etwas ausgibt, stellt dem aufrufenden Code dagegen keinen solchen Wert zur weiteren Verarbeitung bereit.
return beendet die Funktion
Sobald Python ein return erreicht, wird die Funktion sofort beendet:
def pruefe_hp(hp):
if hp <= 0:
return "besiegt"
return "lebendig"
Bei hp <= 0 wird der erste Wert zurückgegeben. Das zweite return wird in
diesem Fall nicht mehr erreicht.
print(pruefe_hp(0))
print(pruefe_hp(75))
Ausgabe:
besiegt
lebendig
Dieses frühe Beenden einer Funktion wird häufig als Early Return bezeichnet.
Dadurch lassen sich unnötig tiefe Verschachtelungen vermeiden:
def pruefe_hp(hp):
if hp <= 0:
return "besiegt"
if hp <= 25:
return "kritisch"
return "bereit"
Python prüft die Bedingungen von oben nach unten. Sobald ein return ausgeführt
wird, endet die Funktion.
Funktionen ohne ausdrückliches return
Eine Funktion ohne return gibt automatisch den besonderen Wert None zurück:
def begruesse(name):
print(f"Willkommen, {name}!")
Speichern wir den Return Value, wird das sichtbar:
ergebnis = begruesse("Karl")
print(ergebnis)
Die Ausgabe lautet:
Willkommen, Karl!
None
Die Funktion hat zwar einen Text ausgegeben, aber keinen eigenen Wert
zurückgegeben. Deshalb enthält ergebnis den Wert None.
None bedeutet in Python, dass kein eigentlicher Wert vorhanden ist.
Docstrings
Direkt unter einer Funktionsdefinition kann ein Docstring stehen:
def erstelle_statuszeile(name, hp, max_hp=100):
"""Erstellt eine Statuszeile für den Spieler."""
return f"{name} | HP: {hp}/{max_hp}"
Ein Docstring beschreibt kurz, wofür eine Funktion gedacht ist.
Anders als ein Kommentar ist er Teil der Funktion und kann zur Laufzeit abgerufen werden:
help(erstelle_statuszeile)
Oder direkt über das Attribut __doc__:
print(erstelle_statuszeile.__doc__)
Für sehr einfache Funktionen ist ein Docstring nicht immer notwendig. Sobald eine Funktion jedoch Parameter, besondere Bedingungen oder ein nicht offensichtliches Verhalten besitzt, kann eine kurze Beschreibung sehr hilfreich sein.
Ein Docstring sollte vor allem erklären:
- was die Funktion tut,
- was sie zurückgibt,
- welche Besonderheiten zu beachten sind.
Scope: Wo Variablen sichtbar sind
Variablen besitzen einen bestimmten Scope, also einen Geltungsbereich.
Eine Variable, die innerhalb einer Funktion angelegt wird, ist normalerweise nur in dieser Funktion sichtbar:
def wuerfeln():
ergebnis = 6
return ergebnis
Innerhalb der Funktion können wir auf ergebnis zugreifen. Außerhalb existiert
dieser lokale Name nicht:
wuerfeln()
print(ergebnis)
Das führt zu einem NameError.
Um den Wert außerhalb zu verwenden, speichern wir den Return Value:
wurf = wuerfeln()
print(wurf)
Die Variable wurf gehört zum Scope außerhalb der Funktion. Die Variable
ergebnis bleibt lokal in wuerfeln().
Parameter sind ebenfalls lokal
Auch Parameter gehören zum lokalen Scope der Funktion:
def verdopple(zahl):
ergebnis = zahl * 2
return ergebnis
Sowohl zahl als auch ergebnis existieren nur innerhalb von verdopple().
Das Argument beim Aufruf kann aus einer anders benannten Variablen stammen:
hp = 50
neue_hp = verdopple(hp)
print(neue_hp)
Die Namen außerhalb und innerhalb der Funktion müssen nicht übereinstimmen.
Lokale und globale Variablen
Eine Variable, die außerhalb aller Funktionen angelegt wird, liegt im globalen Scope:
max_hp = 100
def zeige_max_hp():
print(max_hp)
Die Funktion kann den globalen Wert lesen:
zeige_max_hp()
Trotzdem sollten Funktionen nicht unnötig von globalen Variablen abhängen.
Diese Variante ist meist klarer:
def zeige_max_hp(max_hp):
print(max_hp)
max_hp = 100
zeige_max_hp(max_hp)
Der benötigte Wert wird ausdrücklich als Argument übergeben. Dadurch ist direkt erkennbar, welche Daten die Funktion verwendet.
Auch Tests und spätere Änderungen werden einfacher, wenn Funktionen ihre Abhängigkeiten über Parameter erhalten.
Was bei mutable Objekten passiert
Listen und Dictionaries sind mutable: Ihr Inhalt kann verändert werden.
Übergibst Du ein solches Objekt an eine Funktion, erhält der lokale Parameter Zugriff auf dasselbe Objekt:
def fuege_hinzu(inventar, gegenstand):
inventar.append(gegenstand)
spieler_inventar = ["Fackel"]
fuege_hinzu(spieler_inventar, "Schlüssel")
print(spieler_inventar)
Die Ausgabe lautet:
['Fackel', 'Schlüssel']
Die Funktion hat nicht den äußeren Variablennamen spieler_inventar verändert.
Sie hat die Liste verändert, auf die sowohl spieler_inventar als auch der
lokale Parameter inventar verweisen.
Das gilt genauso für Dictionaries:
def verletze(spieler, schaden):
spieler["hp"] = spieler["hp"] - schaden
spieler = {
"name": "Karl",
"hp": 100,
}
verletze(spieler, 25)
print(spieler["hp"])
Ausgabe:
75
Zuweisen ist nicht dasselbe wie Verändern
Eine neue Zuweisung an den lokalen Parameter ersetzt nur den lokalen Namen:
def leere_inventar(inventar):
inventar = []
Nach diesem Aufruf bleibt die ursprüngliche Liste unverändert:
spieler_inventar = ["Fackel", "Schlüssel"]
leere_inventar(spieler_inventar)
print(spieler_inventar)
Ausgabe:
['Fackel', 'Schlüssel']
Die Funktion hat lediglich ihren lokalen Namen inventar auf eine neue Liste
gesetzt.
Diese Funktion verändert dagegen die bestehende Liste:
def leere_inventar(inventar):
inventar.clear()
Nun ist die Änderung auch außerhalb sichtbar:
spieler_inventar = ["Fackel", "Schlüssel"]
leere_inventar(spieler_inventar)
print(spieler_inventar)
Ausgabe:
[]
Dieser Unterschied ist wichtig:
inventar = []weist dem lokalen Namen eine neue Liste zu.inventar.clear()verändert die bereits vorhandene Liste.
Immutable Werte und Rückgabewerte
Ganzzahlen, Strings, Booleans und Tupel sind immutable. Ihr bestehender Wert kann nicht direkt verändert werden.
Betrachten wir diese Funktion:
def heile(hp, menge):
hp = hp + menge
Der Aufruf verändert die äußere Variable nicht:
hp = 50
heile(hp, 20)
print(hp)
Ausgabe:
50
Innerhalb der Funktion wurde lediglich der lokale Name hp auf einen neuen
Integer gesetzt.
Deshalb sollte die Funktion den neuen Wert zurückgeben:
def heile(hp, menge, max_hp=100):
return min(hp + menge, max_hp)
Beim Aufruf speichern wir den Return Value:
hp = 50
hp = heile(hp, 20)
print(hp)
Ausgabe:
70
Diese Schreibweise macht ausdrücklich sichtbar, dass ein neuer Wert berechnet und anschließend im Player State gespeichert wird.
Kleine Funktionen mit einer klaren Aufgabe
Eine Funktion sollte möglichst eine klar erkennbare Aufgabe erfüllen.
Diese Funktion übernimmt gleich mehrere unterschiedliche Aufgaben:
def alles_machen():
# Raum anzeigen
# Input lesen
# Gegenstände aufnehmen
# Player State verändern
# Spiel beenden
...
Dadurch wäre zwar Code in einer Funktion untergebracht, aber kaum etwas gewonnen.
Besser sind mehrere gezielte Funktionen:
zeige_raum()
zeige_inventar()
nimm_alles()
erstelle_statuszeile()
Der Name beschreibt jeweils, wofür die Funktion zuständig ist.
Das bedeutet nicht, dass jede einzelne Codezeile eine eigene Funktion braucht. Eine gute Funktion fasst mehrere Anweisungen zusammen, die gemeinsam eine verständliche Aufgabe erledigen.
Der Dungeon, Schritt 5
Wir zerlegen nun den Dungeon aus Teil 4 in einzelne Funktionen.
Öffne dungeon.py und ersetze den bisherigen Inhalt durch folgenden Code:
# Dungeon – Teil 5: Funktionen
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 Namen, die Beschreibung und die Ausgänge eines Raums."""
print()
print(f"Ort: {raum['name']}")
print(raum["beschreibung"])
print("Ausgänge:", ", ".join(raum["ausgaenge"]))
def zeige_gegenstaende(raum):
"""Zeigt die Gegenstände im aktuellen Raum."""
if raum["gegenstaende"]:
print("Hier liegt:", ", ".join(raum["gegenstaende"]))
else:
print("Hier liegt nichts Brauchbares.")
def nimm_alles(raum, inventar):
"""Legt alle Gegenstände des Raums ins Inventar und gibt ihre Anzahl zurück."""
gegenstaende = raum["gegenstaende"]
if not gegenstaende:
return 0
anzahl = len(gegenstaende)
inventar.extend(gegenstaende)
gegenstaende.clear()
return anzahl
def zeige_inventar(inventar):
"""Zeigt den Inhalt des Inventars."""
if inventar:
print("Inventar:", ", ".join(inventar))
else:
print("Dein Inventar ist leer.")
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']}"
)
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)}")
print(f"Gegenstände: {len(spieler['inventar'])}")
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, inventar, status, ende")
while True:
raum = raeume[ort]
if raum_anzeigen:
besuchte_raeume.add(ort)
zeige_raum(raum)
raum_anzeigen = False
befehl = input("\n> ").strip().lower()
if befehl == "ende":
print("Du verlässt das Verlies. Bis zum nächsten Mal.")
break
elif befehl == "umsehen":
zeige_gegenstaende(raum)
elif befehl == "nimm":
anzahl = nimm_alles(raum, spieler["inventar"])
if anzahl > 0:
print(f"Du nimmst {anzahl} Gegenstand auf.")
else:
print("Hier gibt es nichts zu nehmen.")
elif befehl == "inventar":
zeige_inventar(spieler["inventar"])
elif befehl == "status":
zeige_status(spieler, besuchte_raeume)
elif befehl in raum["ausgaenge"]:
ort = raum["ausgaenge"][befehl]
raum_anzeigen = True
else:
print("Das geht hier nicht.")
print("Mögliche Ausgänge:", ", ".join(raum["ausgaenge"]))
print("Weitere Befehle: umsehen, nimm, inventar, status, 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, inventar, status, ende
Ort: Halle
Du stehst in einer dunklen Halle mit moderigem Geruch.
Ausgänge: norden, osten
> umsehen
Hier liegt: Fackel
> nimm
Du nimmst 1 Gegenstand auf.
> inventar
Inventar: Fackel
> norden
Ort: Bibliothek
Staubige Regale voller zerfledderter Bücher umgeben Dich.
Ausgänge: sueden
> status
Karl | HP: 100/100 | Gold: 50
Besuchte Räume: 2
Gegenstände: 1
> ende
Du verlässt das Verlies. Bis zum nächsten Mal.
Der Programmstart mit main()
Fast der gesamte eigentliche Programmablauf steckt nun in einer Funktion namens
main:
def main():
...
Am Ende der Datei wird sie aufgerufen:
main()
Python verarbeitet die Datei von oben nach unten. Dabei werden zunächst alle Funktionen definiert. Der jeweilige Funktionskörper wird noch nicht ausgeführt.
Erst beim abschließenden Aufruf von main() beginnt das eigentliche Spiel.
Der Name main besitzt in Python keine besondere technische Bedeutung. Er ist
eine verbreitete Konvention für die Funktion, die den Hauptablauf eines
Programms enthält.
Räume über eine Funktion erstellen
Die Funktion
def erstelle_raeume():
return {
...
}
erstellt das Dictionary mit der Dungeon-Welt und gibt es zurück.
In main() speichern wir diesen Return Value:
raeume = erstelle_raeume()
Dadurch ist die umfangreiche Raumdefinition vom eigentlichen Game-Loop getrennt.
Später können wir die Räume in eine eigene Datei auslagern oder aus einer Datei laden, ohne den restlichen Programmablauf grundlegend umzubauen.
Funktionen für Output
Mehrere Funktionen sind ausschließlich für bestimmte Ausgaben zuständig:
zeige_raum(raum)
zeige_gegenstaende(raum)
zeige_inventar(inventar)
zeige_status(spieler, besuchte_raeume)
Der Game-Loop muss dadurch nicht mehr wissen, wie jede einzelne Ausgabe zusammengesetzt wird.
Statt mehrerer print-Anweisungen steht dort nur noch:
zeige_raum(raum)
Der Funktionsname beschreibt bereits, was an dieser Stelle geschieht.
Einen String zurückgeben
erstelle_statuszeile(...) gibt einen String zurück, zeigt ihn aber nicht selbst
an:
def erstelle_statuszeile(spieler):
return (
f"{spieler['name']} | "
f"HP: {spieler['hp']}/{spieler['max_hp']} | "
f"Gold: {spieler['gold']}"
)
Die Funktion zeige_status(...) verwendet diesen Return Value:
print(erstelle_statuszeile(spieler))
Diese Trennung ist nützlich: Die Statuszeile könnte später nicht nur im Terminal angezeigt, sondern beispielsweise auch in einer Logdatei gespeichert oder über ein Netzwerk verschickt werden.
Eine Funktion, die einen String erzeugt, ist flexibler als eine Funktion, die
ihn ausschließlich mit print(...) ausgibt.
Den State über mutable Objekte verändern
Die Funktion nimm_alles(...) erhält zwei mutable Objekte:
def nimm_alles(raum, inventar):
raumverweist auf das Dictionary des aktuellen Raums.inventarverweist auf die Liste im Player State.
Die Funktion erweitert das Inventar:
inventar.extend(gegenstaende)
Anschließend leert sie die Liste im Raum:
gegenstaende.clear()
Beide Änderungen bleiben nach dem Funktionsaufruf erhalten, weil die vorhandenen Listen verändert wurden.
Zurückgegeben wird lediglich die Anzahl der aufgenommenen Gegenstände:
return anzahl
Der Game-Loop kann daraufhin eine passende Meldung ausgeben:
anzahl = nimm_alles(raum, spieler["inventar"])
if anzahl > 0:
print(f"Du nimmst {anzahl} Gegenstand auf.")
else:
print("Hier gibt es nichts zu nehmen.")
Die Funktion erledigt damit eine klar begrenzte Aufgabe:
- Gegenstände ins Inventar übertragen,
- den Raum leeren,
- die Anzahl der übertragenen Gegenstände zurückgeben.
Der Game-Loop wird lesbarer
Vergleichen wir einen Ausschnitt aus Teil 4:
elif befehl == "inventar":
if spieler["inventar"]:
print("Inventar:", ", ".join(spieler["inventar"]))
else:
print("Dein Inventar ist leer.")
Nach der Auslagerung in eine Funktion bleibt im Game-Loop:
elif befehl == "inventar":
zeige_inventar(spieler["inventar"])
Die Details befinden sich weiterhin im Programm, stehen aber an einer eindeutigen Stelle unter einem verständlichen Namen.
Der Game-Loop liest sich dadurch zunehmend wie eine Beschreibung des Spielablaufs:
if befehl == "ende":
...
elif befehl == "umsehen":
zeige_gegenstaende(raum)
elif befehl == "nimm":
...
elif befehl == "inventar":
zeige_inventar(spieler["inventar"])
elif befehl == "status":
zeige_status(spieler, besuchte_raeume)
Vorsicht bei mutablen Default-Werten
Listen und Dictionaries sollten normalerweise nicht direkt als Default-Wert verwendet werden.
Diese Funktion wirkt zunächst praktisch:
def fuege_gegenstand_hinzu(gegenstand, inventar=[]):
inventar.append(gegenstand)
return inventar
Der Default-Wert wird jedoch nur einmal beim Definieren der Funktion erzeugt. Dadurch verwenden mehrere Aufrufe dieselbe Liste:
print(fuege_gegenstand_hinzu("Fackel"))
print(fuege_gegenstand_hinzu("Schlüssel"))
Die Ausgabe lautet:
['Fackel']
['Fackel', 'Schlüssel']
Der zweite Aufruf beginnt nicht mit einer neuen leeren Liste.
Eine sichere Variante verwendet None:
def fuege_gegenstand_hinzu(gegenstand, inventar=None):
if inventar is None:
inventar = []
inventar.append(gegenstand)
return inventar
Nun wird bei jedem Aufruf ohne eigenes Inventar eine neue Liste erstellt.
Unveränderbare Default-Werte wie Zahlen, Strings, Booleans oder None sind
dagegen unproblematisch:
def heile(hp, menge=20, max_hp=100):
return min(hp + menge, max_hp)
Stolperfallen
-
Eine Funktion nur definieren: Ein
def-Block legt die Funktion an, führt sie aber nicht aus. Erst ein Aufruf wiezeige_raum(raum)startet ihren Funktionskörper. -
Die Klammern beim Aufruf vergessen:
zeige_raumbezeichnet die Funktion selbst.zeige_raum(raum)ruft sie auf. -
printundreturnverwechseln:print(...)erzeugt Output im Terminal.returngibt einen Wert an die aufrufende Stelle zurück. -
Einen Return Value nicht speichern: Gibt eine Funktion einen neuen Integer oder String zurück, musst Du diesen Wert verwenden:
python
hp = heile(hp, 20)
-
Code nach
returnerwarten: Sobaldreturnausgeführt wurde, endet die Funktion. Nachfolgende Anweisungen desselben Pfads werden nicht mehr erreicht. -
Auf lokale Variablen von außen zugreifen: Variablen, die innerhalb einer Funktion angelegt werden, gehören zu deren lokalem Scope.
-
Zuweisung mit Mutation verwechseln:
inventar = []ersetzt nur den lokalen Namen.inventar.clear()verändert die bestehende Liste. -
Mutable Default-Werte verwenden: Eine Liste oder ein Dictionary als Default-Wert wird zwischen mehreren Aufrufen geteilt.
-
Eine Funktion zu früh aufrufen: Das zugehörige
defmuss bereits ausgeführt worden sein, bevor die Funktion aufgerufen wird. Deshalb stehtmain()am Ende der Datei. -
Zu viele globale Variablen verwenden: Übergib benötigte Werte möglichst als Argumente und gib berechnete Ergebnisse mit
returnzurück.
Übungen
1. Prüfen, ob der Spieler besiegt ist
Schreibe eine Funktion ist_besiegt(hp). Sie soll True zurückgeben, wenn der
Spieler keine Lebenspunkte mehr besitzt, andernfalls False.
Lösung
def ist_besiegt(hp):
"""Prüft, ob der Spieler keine Lebenspunkte mehr besitzt."""
return hp <= 0
if ist_besiegt(spieler["hp"]):
print("Du bist besiegt.")
2. Eine Heilfunktion schreiben
Schreibe eine Funktion heile(...), die aktuelle Lebenspunkte, eine Heilmenge
und die maximalen Lebenspunkte erhält.
Die neuen Lebenspunkte dürfen max_hp nicht überschreiten.
Lösung
def heile(hp, menge, max_hp=100):
"""Berechnet die Lebenspunkte nach einer Heilung."""
return min(hp + menge, max_hp)
spieler["hp"] = heile(
spieler["hp"],
menge=20,
max_hp=spieler["max_hp"],
)
3. Die Statuszeile erweitern
Erweitere erstelle_statuszeile(...), sodass zusätzlich die Anzahl der
Gegenstände im Inventar angezeigt wird.
Die Ausgabe soll beispielsweise so aussehen:
Karl | HP: 75/100 | Gold: 50 | Inventar: 2
Lösung
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'])}"
)
Im nächsten Teil
Unser Code ist nun in überschaubare Funktionen aufgeteilt. Einige Stellen sind aber noch ausführlicher, als sie in Python sein müssten.
In Teil 6 geht es deshalb um
pythonic Code: Comprehensions, Slicing, enumerate, Unpacking und weitere
Idiome, mit denen sich typische Aufgaben klar und kompakt ausdrücken lassen.
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.