import numpy as np
import pandas as pd
Eigene Funktionen
Pythons vordefinierte Funktionen und importierte Funktionen z.B. aus numpy
und pandas
haben wir ja bereits kennengelernt. Es ist aber natürlich auch möglich sich selbst neue Funktionen zu definieren. Der Hauptgrund dafür ist eins der bekanntesten Prinzipien der Programmierung:
Don’t Repeat Yourself – Wiederhole dich nicht
Funktionen ermöglichen die Erstellung von übersichtlichen, wartbaren und wiederverwendbaren Programmen bzw. Workflows.
Syntax
Für die Definition einer Funktion muss der Funktionsname und die Funktionsparameter (“Argumente”) vorgegeben werden. Die grundlegende Syntax dafür sieht so aus:
# Definiere Funktion
def gruess(name):
print("Hallo " + name + "!")
# Rufe Funktion auf
"Lukas")
gruess("Mila")
gruess("Kerstin") gruess(
Hallo Lukas!
Hallo Mila!
Hallo Kerstin!
Das Argument name
ist in der Funktion also eine Variable, die mit dem Wert, den wir im Funktionsaufruf übergeben, definiert wird. Wie bei Schleifen und anderen logischen Blöcken wird der Kontext der Funktion über die Einrückung definiert.
Wir sparen es uns also, jedes mal die String-Verkettung und den print
-Befehl auszuschreiben, und man kann direkt im Code erkennen, dass drei Personen beim Namen gegrüßt werden sollen. Bei diesem einfachen Beispiel ist der Vorteil zwar noch gering, aber sobald komplexere Funktionen definiert werden und gar andere Personen den Code lesen und ändern sollen, werden sie bald unerlässlich.
Wir könnten die Funktion zum Beispiel so erweitern, dass sie in mehreren Sprachen grüßen kann, welche durch ein weiteres Argument definiert werden:
def localized_greeting(name, language):
if language == 'de':
print("Hallo " + name + "!")
elif language == 'en':
print("Hello " + name + "!")
elif language == 'es':
print("¡Hola " + name + "!")
else:
print("👋 " + name)
"Lukas", "fr")
localized_greeting("Mila", "es")
localized_greeting("Kerstin", "jp") localized_greeting(
👋 Lukas
¡Hola Mila!
👋 Kerstin
Um das Lesen von Python-Code zu vereinfachen, hat sich die Python-Community auf einige Konventionen für die Benennung von verschiedenen Dingen geeinigt. Die wichtigsten für uns sind:
- Variablen und Funktionen: im sogenannten Snake-Case, also kleingeschrieben und einzelne Wörter mit einem Unterstrich getrennt, z.B.
localized_greeting
- Konstanten, also Werte, die sich nicht ändern: in Großbuchstaben, z.B.
PI = 3.14159
Diese und weitere Richtlinien können in dem PEP 8 Style guide nachgelesen werden.
Standardargumente
Wir können ein Argument optional machen, indem wir einen Standardwert vorgeben, den es einnimmt, wenn kein anderer Wert beim Aufruf spezifiziert wird:
def localized_greeting(name, language = 'de'):
if language == 'de':
print("Hallo " + name + "!")
elif language == 'en':
print("Hello " + name + "!")
elif language == 'es':
print("¡Hola " + name + "!")
else:
print("👋 " + name)
"Brigitte") localized_greeting(
Hallo Brigitte!
Rückgabewerte
Eine weitere Eigenschaft von Funktionen ist das “Zurückgeben” von Werten, welche in der Funktion berechnet wurden. Das wird über das Schlüsselwort return
erreicht. return
beendet die Funktion und gibt den nachfolgenden Wert zurück.
= 3.14159
PI
def berechne_zylindervolumen(radius, hoehe):
= PI * radius ** 2
grundflaeche = grundflaeche * hoehe
volumen return volumen
= 5
r = 20
h = berechne_zylindervolumen(r, h)
vol print(f"Ein Zylinder mit Radius {r} und Höhe {h} hat ein Volumen von ungefähr {vol}.")
Ein Zylinder mit Radius 5 und Höhe 20 hat ein Volumen von ungefähr 1570.795.
Nur wenn eine Funktion explizit einen Wert zurückgibt, kann dieser auch außerhalb der Funktion verwendet, also z.B. in eine Variable gespeichert werden. Wenn keine return
-Anweisung vorhanden ist, gibt die Funktion None
zurück. So wäre es beispielsweise nicht möglich das Ergebnis der gruess()
-Funktion von vorhin in eine Variable zu speichern:
= gruess("Peter") eingruss
Hallo Peter!
print(eingruss)
None
Eine Funktion kann natürlich auch mehrere Werte bzw. “Dinge” gleichzeitig zurückgeben. Python ermöglicht es, diese direkt an mehrere Variablen zuzuweisen:
def berechne_kreis(radius):
= 2 * PI * radius
umfang = PI * radius ** 2
flaeche return umfang, flaeche
# Beide Rückgabewerte direkt verschiedenen Variablen zuweisen
= berechne_kreis(5)
u, a print(f"Umfang: {u:.2f}, Fläche: {a:.2f}")
Umfang: 31.42, Fläche: 78.54
Dieses “Auspacken” der Rückgabewerte macht den Code übersichtlicher und ist eine elegante Methode, um mit mehreren Ergebnissen zu arbeiten.
Scoping
Vielleicht ist euch im Beispiel oben etwas aufgefallen: Warum wird eine neue Variable vol
definiert, die den Rückgabewert der Funktion enthält, wo doch in der Funktion selbst eine Variable volumen
definiert wurde? Die Antwort darauf ist der Scope (dt. Nutzbarkeitsbereich/Sichtbarkeitsbereich) der Variablen.
Variablen, die in einer Funktion definierten werden, sind nur innerhalb dieser Funktion verwendbar. Das muss auch so sein, da man sich sonst aus Versehen durch die Verwendung einer Funktion eigene Variablen überschreiben könnte. Das bedeutet also auch, dass man Variablennamen, die in Funktionen verwendet werden, ohne weiteres wiederverwendet werden können.
In Funktionen können jedoch trotzdem Variablen (oder Konstanten) von außerhalb der Funktion verwendet werden, wie hier zum Beispiel PI
. Eine so verwendete Variable wird globale Variable genannt.
Die Verwendung von globalen Variablen sollte in den meisten Fällen vermieden werden. Sie verringern die Lesbarkeit des Codes, da nicht offensichtlich ist, wo die verwendeten Werte in einer Funktion herkommen. Außerdem können globale Variablen an verschiedenen Stellen modifiziert werden, was die eventuelle Fehlersuche erschwert.
Im folgenden Beispiel tritt ein Fehler auf, da die Variable volumen
außerhalb der Funktion nicht definiert ist. Stattdessen gibt es volumen
nur innerhalb der Funktion und das Ergebnis wird in vol
gespeichert. Die letzte Zeile hätte also print(vol)
lauten müssen:
= 3.14159
PI
def berechne_zylindervolumen(radius, hoehe):
= PI * radius ** 2
grundflaeche = grundflaeche * hoehe
volumen return volumen
= 5
r = 20
h = berechne_zylindervolumen(r, h)
vol
print(volumen)
NameError: name 'volumen' is not defined
Docstrings
Eine gute Praxis beim Schreiben von Funktionen ist die Verwendung von Docstrings. Ein Docstring ist ein mehrzeiliger String, der direkt unterhalb der Funktionsdefinition steht und die Funktion mit ihren Parametern und Rückgabewerten beschreibt. Im Endeffekt handelt es sich dabei um einen Kommentar, der die Funktion in keinster Weise beeinflusst, sondern nur dokumentiert. Dies erleichtert das Verständnis der Funktion für andere Menschen und für einen selbst, wenn man sich den Code später erneut ansieht.
Ein Docstring wird mit dreifachen Anführungszeichen (“““) oder (’’’) geschrieben und kann mehrere Zeilen umfassen. Hier ist ein Beispiel, das die berechne_zylindervolumen
-Funktion mit einem Docstring ergänzt:
= 3.14159
PI
def berechne_zylindervolumen(radius, hoehe):
"""
Berechnet das Volumen eines Zylinders.
Parameter:
radius: Der Radius der Zylinderbasis.
hoehe: Die Höhe des Zylinders.
Rückgabewert:
Das berechnete Volumen des Zylinders.
"""
= PI * radius ** 2
grundflaeche = grundflaeche * hoehe
volumen return volumen
Durch die Verwendung eines Docstrings wird klar definiert, welche Parameter die Funktion erwartet und welchen Wert sie zurückgibt. Dies verbessert die Lesbarkeit und Wartbarkeit des Codes erheblich. Viele Entwicklungsumgebungen zeigen euch auch automatisch den Docstring an, wenn ihr die Funktion benutzen wollt. Hier ein Beispiel aus VSCode, in welchem erste eine Funktion mit Docstring definiert wird und dann beim Hovern über die Funktion ein Tooltip1 mit dem Docstring erscheint:
Typ-Annotationen
Python ist eine dynamisch typisierte Sprache, was bedeutet, dass man nicht angeben muss, welchen Typen (float, int, bool, etc.) eine Variable haben soll. Allerdings helfen Typ-Annotationen dabei, Fehler frühzeitig zu erkennen und den Code verständlicher zu machen. Sie sind vor allem für größere Projekte nützlich.
Hier ist die berechne_zylindervolumen
-Funktion mit Typ-Annotationen:
float = 3.14159 # Auch bei der Erstellung einer Variable kann der Typ vorgegeben werden
PI:
def berechne_zylindervolumen(radius: float, hoehe: float) -> float:
"""
Berechnet das Volumen eines Zylinders mit Typ-Annotationen.
Parameter:
radius (float): Der Radius der Zylinderbasis.
hoehe (float): Die Höhe des Zylinders.
Rückgabewert:
float: Das berechnete Volumen des Zylinders.
"""
float = PI * radius ** 2
grundflaeche: float = grundflaeche * hoehe
volumen: return volumen
Die Syntax ist also <Variablenname>: <Typ>
, sowohl bei Variablendeklaration als auch bei der Angabe von Funktionsparametern. Mit -> <Typ>
Nach der Angabe der Parameter wird vorgegeben, welchen Typ der Rückgabewert der Funktion hat.
In statisch typisierten Sprachen wie C++ oder Java müssen die Typen von jeder Variable explizit angegeben werden, da der Compiler diese Information für die Generierung des ausführbaren Maschinencodes benötigt. Das mag zuerst wie ein eindeutiger Nachteil klingen, aber es ermöglicht die Erzeugung von etwas effizienteren Programmen und wirft viele Fehler schon frühzeitig auf.
Warum Typ-Annotationen hilfreich sind
Obwohl Python weiterhin dynamisch bleibt und die Typen nicht erzwungen werden, helfen sie beim Debuggen und in modernen Entwicklungsumgebungen, indem sie Fehler frühzeitig aufzeigen. So gut wie alle gängigen Python-Pakete sind nahezu vollständig mit Typ-Annotationen versehen.
Beispiel: Fehlererkennung durch Typ-Annotationen
Hier deifinieren wir eine Funktion, die einen Pandas DataFrame erwartet und ausgibt. Dann übergeben wir allerdings ein Numpy Array an die Funktion.
def process_dataframe(df: pd.DataFrame) -> None:
"""
Verarbeitet einen Pandas DataFrame.
"""
print(df.head())
= np.array([[1, 2, 3], [4, 5, 6]])
arr
process_dataframe(arr)
AttributeError: 'numpy.ndarray' object has no attribute 'head'
In einigen Entwicklungsumgebungen würde nun schon vor Ausführen des Codes eine Warnung ausgegeben werden. Beispielsweise würde in VSCode eine rote, gewellte Linie unter arr
erscheinen:
Falls ihr auch VSCode nutzt um Python Code auszuführen, aber keine solchen Warnungen erhaltet, liegt es vermutlich daran, dass ihr folgendes noch nicht getan habt:
- Das Pylance Plugin installieren
- TypeChecking aktivieren in den Einstellungen:
File > Preferences > Settings
Siehe z.B. auch diese Anleitung
Zusammenfassung
Python-Funktionen ermöglichen die modularisierung und wiederverwendung von Code, wobei richtige Dokumentation mit Docstrings und Typ-Annotationen die Codequalität und -verständlichkeit maßgeblich verbessern.
Übungen
Übung 1
Welcher Funktionsaufruf wird einen Fehler verursachen?
def function_1(arg1):
print(arg1)
def function_2(arg1, arg2, arg3: int = 5):
return arg1 + arg2 + arg3
def function_3():
print(arg3)
def function_4(arg1: str = 5):
print(arg1)
42)
function_1(5.0, arg2 = -15)
function_2(
function_3() function_4()
Übung 2
In dieser Übung sollst du eine Funktion erstellen, die den Mittelwert einer numerischen Spalte berechnet und das pro Gruppe in mit Daten aus einem DataFrame. Schreibe also eine Funktion mittelwert_pro_gruppe()
, die:
- Einen DataFrame, eine Gruppen-Spalte und eine Werte-Spalte als Eingabe nimmt
- Den Mittelwert der Werte-Spalte für jede Gruppe berechnet
- Das Ergebnis via
return
ausgibt.
import pandas as pd
# Beispieldatensatz
= pd.DataFrame({
verkaufsdaten 'Produkt': ['Laptop', 'Maus', 'Tastatur', 'Monitor', 'Laptop', 'Maus', 'Tastatur', 'Monitor'],
'Kategorie': ['Computer', 'Zubehör', 'Zubehör', 'Computer', 'Computer', 'Zubehör', 'Zubehör', 'Computer'],
'Verkäufer': ['Anna', 'Ben', 'Anna', 'Ben', 'Chris', 'Chris', 'Anna', 'Ben'],
'Preis': [1200, 25, 45, 150, 999, 20, 50, 180],
'Anzahl': [5, 10, 8, 3, 2, 15, 5, 4]
})
# Hier deine Funktion erstellen:
def mittelwert_pro_gruppe(df, gruppen_spalte, werte_spalte):
# Dein Code hier
pass
Übung 3
Überlege dir für folgende Funktion einen sinnvollen Funktionsnamen, Namen für die Argumente und ergänze Docstring und Typ-Annotationen:
import pandas as pd
def function(df, arg2, arg3, arg4):
= df[df[arg2] >= arg3]
df_filtered = df_filtered.sort_values(by=arg4)
df_sorted return df_sorted
# Beispielaufruf
= {
data "name": ["A", "B", "C", "D"],
"value": [5, 15, 25, 8],
"score": [50, 80, 90, 60]
}
= pd.DataFrame(data)
df "value", 10, "score") function(df,
Hinweis: Lösungen zu den meisten Übungsaufgaben findet ihr, indem ihr ganz oben rechts im Kapitel auf den </> Code
Button klickt und dann entsprechend nach ganz unten scrollt. Es ist Absicht, dass dies etwas umständlich ist.