Letzte Woche haben wir folgende Themen behandelt:
Falls Sie Fragen zu diesen Themen oder zu den Übungsaufgaben haben, sprechen Sie uns bitte an!
Und so geht es weiter:
In VSCode können Sie eine Tastenkombination verwenden, um die Zeile, in der der Cursor sich befindet, sofort zu kommentieren. Sie können den Shortcut auch mit dem Multicursor oder einfach der Markierung mehrerer Zeilen kombinieren.
Probieren Sie es aus! Öffnen Sie eine Ihrer letzten Pythondateien und drücken Sie Ctrl + Shift + P
, um die Command Palette zu öffnen. Geben Sie den Befehl >toggle line comment
ein. Während Sie den Befehl eingeben, sehen Sie auf der rechten Seite einen Shortcut, vermutlich Ctrl + Shift + 7
. Sie können den Befehl entweder über die Command Palette ausführen oder sich den Shortcut merken und ab sofort Zeilen bequem kommentieren. Was passiert, wenn Sie den Shortcut zweimal nacheinander auf die gleiche Zeile anwenden?
Es gibt in Python einen speziellen Datentyp namens None
, der leere Objekte bezeichnet, die keinen anderen Datentyp haben.
print(type(None))
# Leere Listen, Strings usw. haben ihren erwarteten Datentyp:
print(type([]))
print(type(""))
Oft haben Methoden, die z.B. auf veränderlichen Objekten operieren, den Rückgabetyp None
- es muss immer einen Rückgabetyp geben, und None
bekommen wir, wenn nichts anderes zurückgegeben wird. Wenn Sie also eine Fehlermeldung wie die folgende sehen...
for i in [1,2,3].append(4):
print(i) # :(
... liegt das mit hoher Wahrscheinlichkeit daran, dass das Ergebnis der Methode append()
vom Typ None
ist und nicht - wie im Codebeispiel sinnvoll wäre - eine Liste.
Damit der Code ohne Fehler ausgeführt wird, können wir beispielsweise folgendes schreiben:
for i in [1,2,3] + [4]:
print(i) # :)
Hier übrigens der Grund, warum der erste Versuch schiefgegangen ist: Das Ergebnis der append()-Methode hat den Rückgabetyp None
.
print(type([1,2,3].append(4)))
None
kann auch als Boolean ausgewertet werden und hat dann den Wert False
. Das ist auch bei leeren Strings, leeren Listen und der Zahl 0
der Fall:
print(bool(None))
print(bool([]))
print(bool(""))
print(bool(0))
print("-----------")
print(bool([5,3,1]))
print(bool("String mit Inhalt"))
print(bool(-10))
Wenn Sie mitten in Ihrem Programm den Wert einer Variable ausgeben lassen und None
angezeigt wird, wissen Sie, dass etwas nicht geklappt hat. Prüfen Sie in so einem Fall alle Stellen, an denen der Wert der Variable verändert wurde, daraufhin, ob eventuell keine Rückgabe geliefert wurde und daher None
der Wert der Variable ist.
Inzwischen haben wir schon eine Menge Programmiergrundlagen behandelt und Sie sind bereit, komplexere Aufgaben zu bearbeiten. Funktionen bieten Ihnen die Möglichkeit, Sinnabschnitte Ihrer Programme vom Rest abzukapseln und so den Überblick zu bewahren.
Stellen Sie sich beispielsweise vor, dass Sie Quadratzahlen berechnen wollen. Bisher haben wir das etwa so gemacht:
input_number = 6 # Variable erzeugen und mit dem Wert 3 belegen
result = input_number * input_number # Quadratzahl berechnen
print(result) # Ergebnis ausgeben
Wenn wir genau eine Quadratzahl berechnen wollen, ist gegen den Code oben nichts einzuwenden. Wenn wir die Berechnung aber mehrmals für unterschiedliche Werte durchführen wollen, lohnt es sich, den Code dafür allgemeiner zu schreiben. Dann können wir die Funktionalität "Quadratzahl berechnen" vom restlichen Code abkapseln und jederzeit aufrufen, wenn wir sie brauchen.
Eine Funktion hat in Python die folgende Form:
def calculate_square(input_number): # Funktionsdefinition eröffnen
result = input_number * input_number # beliebig viele Befehle
return result # Funktionsdefinition beenden
Die Einrückung zeigt, welche Zeilen zur Funktionsdefinition gehören. Der Kopf der Funktion enthält den Namen (hier: calculate_square
) und eine Angabe aller Parameter, die wir in der Funktion brauchen. (Erinnern Sie sich an die Parameter in der Sitzung am 16.10., z.B. s.split(".", 2)
)
In der Beispielfunktion heißt der einzige Parameter input_number
.
Im Körper der Funktion folgen beliebig viele Befehle, in denen das Argument (der konkrete Wert, der als Parameter verwendet wird) verarbeitet wird. Beispielsweise berechnen wir oben das Quadrat der übergebenen Zahl. Das Ergebnis wird hier in der Variable result
gespeichert.
Schließlich gibt die Funktion mit return
einen Wert zurück. Im Beispiel ist das der Wert, der in der Variable result
gespeichert ist. Da return
die Funktion sofort beendet, werden keine eingerückten Zeilen unterhalb von return
mehr ausgeführt.
Wir können uns Funktionen wie eine Art Rezept vorstellen, nach dem bestimmte Aufgaben gelöst werden. Die Funktionsdefinition, die wir oben sehen, braucht noch einen konkreten Eingabewert (ein Argument für den Parameter input_number
), damit ein Ergebnis berechnet werden kann.
Erst, wenn die Funktion im Code mit einem konkreten Argument aufgerufen wird, werden die Befehle innerhalb des Funktionskörpers ausgeführt. Das Ergebnis - der Wert, der in der letzten Zeile der Funktion zurückgegeben wird - kann dann an der Stelle im Code, wo die Funktion aufgerufen wurde, verwendet werden.
Probieren Sie es aus: Führen Sie den Code in der nächsten Zelle aus und beachten Sie, in welcher Reihenfolge die print()
-Befehle ausgeführt werden.
print("1")
def calculate_square(input_number):
print("2")
result = input_number * input_number
return result
print("3")
my_square = calculate_square(4) # das Ergebnis des Funktionsaufrufs calculate_square mit dem Argument 4
# wird in der Variable my_square gespeichert
print("Ergebnis der Berechnung: " + str(my_square))
print("4")
Im Gegensatz zu z.B. Schleifen sind Variablen, die in Funktionskörpern definiert werden, außerhalb der Funktion nicht mehr verfügbar. Probieren Sie es aus: Ergänzen Sie im Beispiel unterhalb des bisherigen Codes einen print()
-Aufruf für result
oder input_number
. Das Programm stürzt ab.
Mithilfe von Funktionen können wir Programmcode, der vorher unübersichtlich war, auslagern. Wir geben Funktionen dafür sprechende Namen und lassen jede Funktion genau eine Aufgabe erfüllen.
Beachten Sie, dass der Interpreter eine Funktion gelesen haben muss, bevor sie ausgeführt werden kann. Eine Funktion muss also immer oberhalb ihres ersten Aufrufs im Hauptprogramm definiert werden.
Mit Funktionen können wir Redundanz (unnötige Wiederholungen) in unserem Code vermeiden. Jedesmal, wenn wir später im Programm eine Quadratzahl berechnen wollen, können wir die eben definierte Funktion verwenden, statt immer wieder die gleichen Codezeilen an verschiedenen Stellen einzufügen. Die Beispielfunktion ist sehr übersichtlich, aber wenn eine Funktion eine komplexere Aufgabe erfüllt - z.B. Daten nach bestimmten Kriterien zu filtern - , wollen wir vermeiden, Codezeilen mehrfach zu schreiben.
Codewiederholungen sind eine typische Fehlerquelle. Außerdem machen Sie unser Programm länger und dadurch unübersichtlicher. Mit Funktionen vermeiden wir solche Wiederholungen.
########### Funktionsdefinition ############
def filter_dictionary_only_keep_nouns(input_dict):
"""
Diese Funktion filtert das eingegebene Dictionary so, dass zum Schluss
ein Dictionary zurückgegeben wird, das nur die Nomen aus dem ursprünglichen
Dictionary enthält
"""
output_dict = {}
############# - Hier Code einfügen - ###############
# ...
####################################################
return output_dict
########### Hauptprogramm ############
words = {"Terminal": "N", "for-Schleife": "N", "einrücken": "V", "mutable": "A", "zurückgeben": "V", "redundant": "A", "Funktion": "N"}
filtered_words = {}
for w in words:
if words[w] == "N":
filtered_words[w] = "N"
print("gefiltertes Dictionary (nur Nomen): " + str(filtered_words))
Versuchen Sie ab jetzt, Übungsaufgaben durch den sinnvollen Einsatz von Funktionen zu lösen. Dadurch erhalten Sie Zwischenergebnisse, können Programmierfehler besser finden, und bei Änderungen im Code sehen Sie leichter, welche Funktionen angepasst werden müssen und welche gleich bleiben können.
return
-Statements schreiben, die in unterschiedlichen Fällen greifen. Achtung: Die Funktion soll in jeder möglichen Verwendung eine Rückgabe liefern.Ab jetzt empfehlen wir Ihnen, Sinnabschnitte unserer Python-Programme in Funktionen auszulagern, wann immer das sinnvoll ist.
Wenn Sie möchten, können Sie Ihre Funktionsdefinitionen sogar in eine separate Datei auslagern. Diese Datei können Sie dann - wie letzte Woche mit dem Modul random
- am Anfang Ihres Codes importieren. Dazu muss die Datei mit den Funktionen eine .py
-Dateiendung haben und im gleichen Verzeichnis liegen wie die andere Pythondatei.
# Datei utils.py
def calculate_square(input_number):
print("2")
result = input_number * input_number
return result
# Datei main.py
import utils
print(utils.calculate_square(3))
Ihre eigenen Module zu definieren kann sinnvoll sein, wenn Sie an größeren Projekten arbeiten. Ein wichtiger Teil der Philosophie von Python ist, dass Code für Menschen lesbar sein soll:
Readability counts!
Jedes Programm muss früher oder später gewartet werden, wenn sich Teile des Systems, auf dem das Programm ausgeführt wird, ändern. Die Wartbarkeit eines Programms steigt mit der Lesbarkeit: Wenn jede Teilaufgabe von einer Funktion erledigt wird, und wir auf den ersten Blick sehen, welche Funktion welche Teilaufgabe erfüllt, können wir viel besser mit dem vorhandenen Code arbeiten.
Ein Beispiel dafür, warum Lesbarkeit und Wartbarkeit nützlich sind:
Stellen Sie sich vor, Ihr Programm liest Informationen aus einer Datenbank. Nach drei Jahren stellen Sie fest, dass die Datenbank nicht groß genug ist, und ersetzen sie durch eine andere Datenbank, deren Struktur ganz anders aufgebaut ist.
Um das Pythonprogramm zu warten, bearbeiten Sie jetzt nur die Funktion, die für das Lesen der Informationen aus der Datenbank zuständig ist. Der Rückgabewert der Funktion, z.B. ein Dictionary, wird wie vorher an das restliche Programm weitergereicht; alle folgenden Funktionen können weiterarbeiten wie vorher. Dadurch, dass Sie die Funktionalität "Datenbank lesen" vom restlichen Code isoliert haben, müssen Sie nur einen Bruchteil des Programms ändern.
Dass Variablen, die in Funktionen definiert werden, nicht außerhalb der Funktion verfügbar sind, liegt daran, dass sie sich in einem anderen Namespace befinden. Ein Namespace ist die Zuordnung von Namen (z.B. Variablen) zu Werten, oder genauer: Zu Zellen im Arbeitsspeicher, in denen die Werte abgelegt wurden. Ein Pythonprogramm hat bis zu 3 Namespaces:
print, str(), for, if, def
etc.Informationen über Namespaces können wir erhalten, wenn wir uns das Ergebnis der Methode dir()
ausgeben lassen. dir()
enthält bei jedem Aufruf die Namen, die im aktuellen Namespace existieren. Das folgende Codebeispiel verdeutlicht den Unterschied: dir()
wird einmal außerhalb der Funktionsdefinition aufgerufen und einmal innerhalb der Funktion.
Achtung: Falls Sie das Programm hier im Jupyter Notebook mehrfach ausführen, merkt der Interpreter sich die Variablen, sodass die Ausgabe beim zweiten Mal anders aussieht als beim ersten Mal. Um alles zurückzusetzen, können Sie oben im Menü das Kommando Kernel -> Restart and Clear Output
ausführen. Oder Sie kopieren den Code in Ihren Editor und führen ihn dort aus.
print("Programmstart:")
print(dir())
print("-----------")
mein_name = "Esther Seyffarth"
print("nach Variablendefinition:")
print(dir())
print("-----------")
def calc_square(n):
result = n**2
print("in der Funktionsdefinition:")
print(dir())
print(mein_name)
print("-----------")
return n**2
print("Nach Funktionsdefinition:")
print(dir())
print("-----------")
print(calc_square(3))
print("Programmende:")
print(dir())
Der Aufruf in Zeile 1 zeigt uns den Namespace des Moduls. Darin enthalten sind zunächst nur die Namen, die zur Sprache Python gehören.
Als nächstes definieren wir eine eigene Variable und führen print(dir())
erneut aus. Wir sehen, dass unsere eigene Variable jetzt Teil des Modul-Namespace (globaler Namespace) geworden ist.
Die Funktionsdefinition sorgt dafür, dass der Name der Funktion ebenfalls im Namespace erzeugt wird. Das sehen wir an der Ausgabe von Zeile 21.
Nun folgt ein Aufruf unserer selbstgeschriebenen Funktion mit dem Argument 3 (Zeile 24). Der Funktionskörper wird ausgeführt, wobei der Parameter n
, eine lokale Variable im Funktions-Namespace, den Wert 3 hat.
Die Ausgabe in Zeile 16 zeigt uns, dass wir hier nicht mehr im globalen Namespace sind, sondern im lokalen Namespace der Funktionsdefinition. Nur zwei Variablen sind bekannt: n
, der Parameter der Funktion, und result
, eine Variable, die wir selbst in Zeile 14 innerhalb der Funktion erzeugt haben.
Am Ende des Programms wird schließlich noch einmal print(dir())
ausgeführt. Wir sehen, dass weder n
noch result
in diesem Namespace enthalten sind. Daran liegt es, dass weiter oben im Notebook unser Programm abgestürzt ist, als wir versucht haben, Variablen aus der Funktionsdefinition auszugeben.
mein_name
zugreifen? Warum/warum nicht? Prüfen Sie Ihre Antwort, indem Sie einen Aufruf von print(mein_name)
in der Funktion ergänzen.Wenn wir im Code auf Variablen oder Funktionen zugreifen, hält der Interpreter sich an eine feste Reihenfolge: Er sucht den angegebenen Namen erst im lokalen Namespace (falls vorhanden), dann im globalen Namespace und dann im Builtin-Namespace.
Beim Importieren von Modulen, z.B. import random
, werden Namen im globalen Namespace ergänzt:
print(dir())
print("-------------")
import random
print(dir())
Erinnern Sie sich, dass wir die Funktionen von random
verwendet haben, indem wir geschrieben haben:
zufallszahl = random.randint(1,5)
Dabei haben wir den Interpreter informiert, dass die Funktion randint()
im Namespace des Moduls random
zu finden ist.
Das war notwendig, weil Namenskonflikte immer möglich sind: Es kann sein, dass wir im eigenen Code Funktionen definiert haben, die genauso heißen wie die Funktionen in importierten Modulen.
Führen Sie das folgende Codebeispiel aus und beachten Sie den Unterschied zwischen den print()
-Anweisungen.
import random
def randint(min, max):
# soll aussehen wie die Zufallszahlenfunktion
result = "ätsch, das ist gar keine Zufallszahl"
return result
print(random.randint(1,5))
print(randint(1,5))
Wir können den Python-Interpreter richtig verwirren, indem wir Namenskonflikte herbeiführen:
zahl = 5
print(5)
print(str(5))
def str(irgendwas):
# ergänzt eine str()-Funktion im globalen Namespace dieser Pythondatei
return "Haha, reingelegt"
# Der Interpreter sucht erst im globalen Namespace; nur falls dort der Name str nicht gefunden wird,
# sucht er im Builtin-Namespace
print(str(5))
### ACHTUNG ###
# Nach dem Ausführen dieser Zelle sollte am besten
# der Kernel neugestartet werden - sonst können wir
# in diesem Notebook nicht mehr die echte str()-Funktion
# verwenden :(
Die Take-Home-Message hier lautet: Achten Sie auf Ihre Variablennamen! Vermeiden Sie außerdem, innerhalb von Funktionen auf Variablen außerhalb des lokalen Namespace zuzugreifen. Es ist zwar möglich (siehe Beispiel oben mit der Variable mein_name
), macht den Code aber schlecht nachvollziehbar und damit schwerer lesbar.
Stattdessen können Sie Variablen, die in der Funktion verfügbar sein sollen, als Parameter mit übergeben. Zum Schluss der Funktion geben Sie dann alle Werte, die weiter verwendet werden sollen, zurück - da wir Funktionen schreiben, um jeweils genau eine Aufgabe zu erfüllen, haben wir meist nur einen einzigen Rückgabewert. Haben wir einmal mehr, können wir z.B. ein Dictionary erstellen, das alle Rückgabewerte im gewünschten Format enthält und das dann nach dem Ausführen der Funktion "entpackt" werden kann.
for
-Schleifen und Namespaces¶Achtung! Weder Schleifen noch if
-Blöcke erstellen ihre eigenen Namespaces. Das wird problematisch, wenn Sie z.B. Schleifen verschachteln und Variablennamen dabei mehrfach belegen.
Verwenden Sie also immer eindeutige Variablen.
Inzwischen haben wir eine Reihe von Werkzeugen zur Verfügung, um eigenen Pythoncode zu schreiben. Sie können jederzeit in den Kursmaterialien nachschauen, wie bestimmte Befehle oder Konstrukte verwendet werden. Hier eine kleine Übersicht über die verschiedenen Schreibweisen und die Bedingungen, unter denen jede Schreibweise verwendet wird:
Anweisung | Bedeutung |
---|---|
<name> = <wert> |
Variablendefinition bzw. Überschreiben des Werts in bestehender Variable. |
for <element> in <sequenz>: <anweisung> |
Schleife, die pro Element in der Sequenz einmal ausgeführt wird. |
if <bedingung>: <anweisung> elif <bedingung2>: <anweisung2> else: <anweisung3> |
If-Block: Prüft, ob die erste Bedingung erfüllt ist. Falls nein, wird die zweite Bedingung geprüft. Falls alle alternativen Bedingungen nicht erfüllt sind, werden die Anweisungen im else -Block ausgeführt. |
def <funktion> (<parameter1>, <parameter2>): <anweisungen> return <rückgabewert> |
Funktionsdefinition: Beliebig viele Parameter, ein Rückgabewert. |
<name> = <funktion>(<argument>) |
Funktionsaufruf: Der Rückgabewert der Funktion (ausgeführt mit dem angegebenen Argument) wird als Wert in der Variable name gespeichert. |
<name> = <objekt>.<methode>(<argument>) |
Methodenaufruf: Eingebaute Operationen z.B. für Strings. Das objekt ist der String, auf den die methode angewendet wird. |
Sie haben heute gelernt,
################################################################
# Übungsaufgabe zu Funktionen #
################################################################
#
# Dieses Programm soll für beliebige User-Eingaben prüfen,
# ob sie im "Spongebob-Case" geschrieben sind. Das Ergebnis
# soll in Form eines Boolean (True oder False) zurückgegeben
# werden.
# Falls das Ergebnis "True" lautet, soll der ASCII-Spongebob
# ausgegeben werden. Andernfalls soll der Originaltext
# ausgegeben werden.
#
# Der Code funktioniert bereits. Können Sie die relevanten
# Zeilen so umbauen, dass eine Funktionsdefinition und ein
# Aufruf der Funktion entstehen? Die User-Eingabe soll außerhalb
# der Funktionsdefinition stehen.
#
# Infos zu "Mocking Spongebob": https://knowyourmeme.com/memes/mocking-spongebob
your_input = input("Please enter a word: ")
# Bis wir das Gegenteil feststellen, gehen wir davon aus,
# dass der Text im Spongebob-Case vorliegt:
sponge_case = True
last_character_is_upper = your_input[0].isupper()
# Schleife: Alle Zeichen ab Index 1 prüfen und mit dem
# jeweils vorigen Zeichen vergleichen
for c in your_input[1:]:
# SpongeCase liegt nur dann vor, wenn die Großschreibung
# jedes Zeichens sich von der des Zeichens davor unterscheidet!
if c.isupper() == last_character_is_upper:
# Zwei aufeinanderfolgende groß- bzw. kleingeschriebene
# Zeichen sind nicht erlaubt!
sponge_case = False
# die letzte beobachtete Großschreibung merken wir uns,
# um das nächste Zeichen damit zu vergleichen
last_character_is_upper = c.isupper()
if sponge_case:
print("""#> *
#> *
#> ----//-------
#> \..C/--..--/ \ `A
#> (@ ) ( @) \ \// |w
#> \ \ \---/
#> HGGGGGGG \ /`
#> V `---------`--'
#> << <<
#> ### ###""")
else:
print(your_input)
################################################################
# Übungsaufgabe zu Funktionen #
################################################################
#
# Schreiben Sie jetzt eine Funktion, die einen String als Argument
# bekommt und diesen String im Spongebob-Case zurückgibt.
# Sie können hier die Stringmethoden .upper() und .lower() verwenden.
##### hier Ihren Code einfügen! #####