Level 04: Auch Funktionen freuen sich über Geschenke

Level 4: Auch Funktionen freuen sich über Geschenke #

Zu vorhersehbar… #

Henrik ist sehr glücklich – sein Programm läuft! Außerdem spart er sich viele Zeilen des immer wieder selben Codes, weil er auch noch eine werte_aus-Funktion geschrieben hat.

Sieh’ dir sein Programm an, überlege ob alles funktioniert, übernimm es in deine IDE und lass es laufen!

def hole_antwort():
    antwort = input('Gib ein: a, b, c oder d: ')
    while not antwort in ['a', 'b', 'c', 'd']:
        antwort = input('Nicht verstanden – a, b, c oder d? ')
    return antwort

def werte_aus():
    if antwort == 'd':
        print('Richtig!')
    else:
        print('Leider falsch!')

def trenner_ausgeben():
    print('-------------------------------')
    print('--Und nun zur nächsten Frage!--')
    print('-------------------------------')
    print()

print('Willkommen zum Flaggenquiz!')
print('Wo findest du die älteste, heute noch in Gebrauch befindliche Trikolore? ')
print('a) Deutschland b) Frankreich c) Italien d) Niederlande')
antwort = hole_antwort()
werte_aus()
trenner_ausgeben()
print('Off-Topic-Frage: Welcher Programmiersprachenentwickler kommt')
print('ebenfalls aus den Niederlanden?')
print('a) Dennis Ritchie b) Bjarne Stroustrup c) Graydon Hoare d) Guido van Rossum')
antwort = hole_antwort()
werte_aus()
trenner_ausgeben()
print('Die Flagge welchen Landes enthält gleichzeitig die Umrisse des Landes?')
print('a) Frankreich b) Niederlande c) Italien d) Zypern')
antwort = hole_antwort()
werte_aus()

Allerdings bereitet ihm etwas Ärger:

Henrik: Hast du mein Quiz einmal gespielt? Ich habe irgendwie das Gefühl, nachdem die Leute die ersten Fragen beantwortet haben, denken sie nicht mehr nach, und beantworten trotzdem alle Fragen richtig. Vielleicht bemerken sie, dass die richtige Antwort immer hinter »d« steckt? Ich möchte das gerne variieren, aber ich will auch nicht wieder jede Auswertung extra schreiben müssen… Kannst du mir helfen?

Mehr Varianz #

Zum Glück gibt es eine Möglichkeit! Du kannst richtige Antworten an unterschiedlicher Stelle haben, und kannst den Auswertungscode in eine einzige Funktion stecken!

Um das zu erreichen, teilen wir der Funktion mit, welche Antwort richtig ist. Schreibe dazu in die Klammern hinter dem Namen der Funktion, über was die Funktion Bescheid wissen soll:

def werte_aus(richtige_antwort):
    # …hier steht die Funktion…

Auf diese Weise erzeugst du eine Variable, die du in der Funktion nutzen kannst. richtige_antwort könnte hier z.B. 'a', 'b', 'c' oder 'd' sein, und die Funktion kann die tatsächlich gegebene Antwort mit der richtigen vergleichen. Das sieht dann so aus:

def werte_aus(richtige_antwort):
    if antwort == richtige_antwort:
        print('Richtig!')
    else:
        print('Leider falsch!')

Im unteren Teil des Programm kannst du jetzt die Reihenfolge der Antworten ändern, und trotzdem die Überprüfung durch die Funktion werte_aus vornehmen lassen:

print('Willkommen zum Flaggenquiz!')
print('Wo findest du die älteste, heute noch in Gebrauch befindliche Trikolore? ')
print('a) Deutschland b) Frankreich c) Italien d) Niederlande')
antwort = hole_antwort()
werte_aus('d')
trenner_ausgeben()
print('Off-Topic-Frage: Welcher Programmiersprachenentwickler kommt')
print('ebenfalls aus den Niederlanden?')
print('a) Dennis Ritchie b) Guido van Rossum c) Graydon Hoare d) Bjarne Stroustrup')
antwort = hole_antwort()
werte_aus('b')
trenner_ausgeben()
print('Die Flagge welchen Landes enthält gleichzeitig die Umrisse des Landes?')
print('a) Frankreich b) Niederlande c) Zypern d) Italien')
antwort = hole_antwort()
werte_aus('c')

Ein Python-Easter-Egg, und Namensräume

Hinweis zur Einordnung

Etwas Unerwartetes passiert, wenn du ein Programm, das nur aus dieser einen Zeile besteht, laufen lässt:

import this

Die Ausgabe umfasst 19 kurze Sätze, die auch das »Zen of Python« genannt werden – Richtlinien zum Entwurf der Sprache wie auch der Programme, die in Python geschrieben werden.

In unserem Kontext interessant ist die letzte Zeile:

Namespaces are one honking great idea – let’s do more of those!

Übersetzt etwa: »Namensräume sind eine großartige Idee, für die wir die Werbetrommel rühren sollten. Mehr davon!«

Funktionen haben ihren eigenen lokalen Namensraum – d.h. Variablen, die innerhalb von Funktionen mit einem Namen angelegt werden, sind unabhängig von gleich benannten Variablen, die außerhalb dieser Funktionen schon existieren. Es kann also zwei Variablen gleichen Namens geben, wenn eine in einem lokalen Namensraum (in einer Funktion) und die andere außerhalb (im globalen Namensraum) existiert.

Innerhalb einer Funktion kannst du auf Variablen aus dem globalen Namensraum zugreifen. Umgekehrt kannst du von außerhalb einer Funktion nicht auf Variablen aus dem lokalen Namensraum dieser Funktion zugreifen.

Oft ist es allerdings keine gute Idee, sich sehr auf globale Variablen zu verlassen – das wird von den meisten Pythoniker:innen auch als schlechter Stil angesehen. Als besser – weil übersichtlicher – angesehen werden sauber getrennte Namensräume ohne globale Variablen.

Noch Genaueres zu Namespaces findest du in diesem Realpython-Tutorial.

Die Funktion soll nicht von der globalen Variable abhängen! #

Wie sieht es mit unserer Funktion werte_aus und ihrer Abhängigkeit von globalen Variablen aus? Findest du eine Variable, die global ist?

def werte_aus(richtige_antwort):
    if antwort == richtige_antwort:
        print('Richtig!')
    else:
        print('Leider falsch!')

Verwendet werden zwei Variablen: richtige_antwort und antwort. richtige_antwort ist eine lokale Variable, die die richtige Antwort erhält. Die bekommt die Funktion beim Aufruf. Doch wie sieht es mit antwort aus?

Die Variable antwort wird vorausgesetzt. Sie wird nirgends in der Funktion erzeugt! Wenn die Funktion aufgerufen wird, sucht der Interpreter zunächst im lokalen Namensraum und wird dort nicht fündig. Also wird der nächste Namensraum durchsucht, und dort ist antwort vorhanden. Zum Glück! Wäre das nicht der Fall, würde das Programm mit einem Fehler beendet werden.

Diese Art zu programmieren macht das Programm allerdings schwer lesbar. Stell dir vor, du würdest den Programmcode zum ersten Mal lesen: du wüsstest nicht, woher genau die Variable antwort kommt, wenn du nur die Funktion ansiehst.

Besser ist es, der Funktion alle Werte zu übergeben, die sie braucht. Dann kannst du dich darauf verlassen, dass der Rückgabewert der Funktion auch nur von diesen Werten abhängt. Im Fehlerfall musst du dann nicht umfassend über das ganze Programm informieren, sondern kannst gezielt in der Funktion suchen. (Gleichzeitig ist dies ein wichtiges Prinzip der sogenannten funktionalen Programmierung.)

In Python (und in den meisten anderen Sprachen) lassen sich einer Funktion mehr als ein Argument übergeben (so werden die Werte genannt, die die Funktion bekommt). In unserem Fall sieht die verbesserte Version dann so aus:

def werte_aus(gegebene_antwort, richtige_antwort):
    if gegebene_antwort == richtige_antwort:
        print('Richtig!')
    else:
        print('Leider falsch!')

gegebene_antwort hätte auch einfach antwort bleiben können. Der lokale Namensraum hat Vorrang vor dem Globalen. Die Umbenennung macht allerdings klar, was der Inhalt der Variable ist, die die Funktion erwartet. Ein wertvoller Hinweis für Leser:innen des Programms!

Den Rest des Programms musst du nun ebenfalls leicht anpassen, indem du der werte_aus-Funktion die geholte Antwort übergibst. Übrigens: wo du jetzt schon über Namensräume Bescheid weisst, ist es an der Zeit, dir eine weitere übliche Vorgehensweise zu zeigen: in einer Python-Datei stehen häufig viele Funktionen. Manchmal möchte jemand nur eine Funktion nutzen, und den Rest des Programmes nicht. Um das Programm nur auszuführen, wenn es tatsächlich als solches gestartet wird (und nicht nur als Modul eingebunden), fügen Pythonist:innen üblicherweise noch die Zeile if __name__ == '__main__': hinzu. Das Programm stehe dann im nachfolgenden Block. (Manchmal steckt der Einstiegspunkt sogar in einer eigenen main-Funktion, und nach der Zeile wird nur diese mit main() aufgerufen.)

if __name__ == '__main__':
    print('Willkommen zum Flaggenquiz!')
    print('Wo findest du die älteste, heute noch in Gebrauch befindliche Trikolore? ')
    print('a) Deutschland b) Frankreich c) Italien d) Niederlande')
    antwort = hole_antwort()
    werte_aus(antwort, 'd')
    trenner_ausgeben()
    print('Off-Topic-Frage: Welcher Programmiersprachenentwickler kommt')
    print('ebenfalls aus den Niederlanden?')
    print('a) Dennis Ritchie b) Guido van Rossum c) Graydon Hoare d) Bjarne Stroustrup')
    antwort = hole_antwort()
    werte_aus(antwort, 'b')
    trenner_ausgeben()
    print('Die Flagge welchen Landes enthält gleichzeitig die Umrisse des Landes?')
    print('a) Frankreich b) Niederlande c) Zypern d) Italien')
    antwort = hole_antwort()
    werte_aus(antwort, 'c')

Super! Damit hast du die Namensräume nun viel ordentlicher als vorher, und das ganze Programm ist übersichtlicher geworden!

Type-Hints bieten noch mehr Orientierung

Hinweis zur Einordnung

Wenn du möchtest, kannst du den Leser:innen deiner Programme noch mehr Orientierung geben, indem du Type hints, also Hinweise über den erwarteten Typ der Variablen, die deine Funktion entgegennimmt und zurückgibt, mit aufschreibst.

So sieht das aus für die Eingangsparameter:

def werte_aus(gegebene_antwort: str, richtige_antwort: str):
    # hier steht die Funktion

Und so für den Rückgabewert:

def hole_antwort() -> str:
    # hier wird die Antwort eingesammelt
    return antwort

Beide Schreibweisen lassen sich auch kombinieren. So wird auf einen Blick sichtbar, welche Art von Daten die Funktion entgegennimmt, und was sie zurückgibt.

Übung: Punkte für richtige Antworten #

Henrik freut sich, dass sich das Programm so prächtig entwickelt hat! Er hat sich schon das nächste Feature ausgedacht:

Henrik: Das Flaggen-Quiz läuft super! Jeden Tag spielen es immer mehr Leute! Doch viele haben mich schon gefragt, ob es irgendeine bessere Auswertung geben könnte… Bekommst du das hin? Einen Punkt pro Antwort, und am Ende eine Ausgabe wie: »Du hast 3 Punkte erspielt.«? Ich frage mich, ob nicht die Funktion werte_aus einen oder keinen Punkt zurückgeben könnte – was meinst du? Oh mann, das Flaggen-Quiz wird sicher durch die Decke gehen!

Tipp 1

Für dieses Problem ist vielleicht eine globale Variable punkte (oder wie immer du sie sinnvollerweise nennen möchtest) angebracht.

Viele Programmierer:innen erzeugen solche Variablen ganz zu Beginn ihres Programms:

punkte = 0
print('Willkommen zum Flaggenquiz!')
# hier geht es weiter…
Tipp 2

Wenn du Henriks Vorschlag aufgreifen möchtest, kannst du der Funktion eine return-Anweisung hinzufügen, die entweder einen Punkt (1) oder keinen Punkt (0) zurückgibt.

def werte_aus(gegebene_antwort: str, richtige_antwort: str) -> int:
    # hier wird die Antwort verglichen
    return punkt
Lösungsvorschlag
def hole_antwort() -> str:
    antwort = input('Gib ein: a, b, c oder d: ')
    while not antwort in ['a', 'b', 'c', 'd']:
        antwort = input('Nicht verstanden – a, b, c oder d? ')
    return antwort

def trenner_ausgeben():
    print('-------------------------------')
    print('--Und nun zur nächsten Frage!--')
    print('-------------------------------')
    print()

def werte_aus(gegebene_antwort: str, richtige_antwort: str) -> int:
    uebereinstimmung = gegebene_antwort == richtige_antwort
    punkt = 0
    if uebereinstimmung:
        print('Richtig!')
        punkt = 1
    else:
        print('Leider falsch!')
    return punkt

if __name__ == '__main__':
    punkte = 0
    print('Willkommen zum Flaggenquiz!')
    print('Wo findest du die älteste, heute noch in Gebrauch befindliche Trikolore? ')
    print('a) Deutschland b) Frankreich c) Italien d) Niederlande')
    punkte += werte_aus(hole_antwort(), 'd')
    trenner_ausgeben()
    print('Off-Topic-Frage: Welcher Programmiersprachenentwickler kommt')
    print('ebenfalls aus den Niederlanden?')
    print('a) Dennis Ritchie b) Guido van Rossum c) Graydon Hoare d) Bjarne Stroustrup')
    punkte += werte_aus(hole_antwort(), 'b')
    trenner_ausgeben()
    print('Die Flagge welchen Landes enthält gleichzeitig die Umrisse des Landes?')
    print('a) Frankreich b) Niederlande c) Zypern d) Italien')
    punkte += werte_aus(hole_antwort(), 'c')
    print('Du hast ' + str(punkte) + ' Punkt(e) erspielt.')

Zurück Weiter