Python Decorators verstehen und anwenden

Python Decorators sind ein Weg, Einfluss auf die grundlegende Funktionalität einer Funktion zu erweitern, ohne den ihr zugrundeliegenden Quellcode zu verändern.

Was sind Funktionsdekoratoren und wofür werden sie genutzt?

Die Nutzung von Python Decorators wird in vielen Python-Tutorials nicht näher erläutert. Der Grund hierfür: Um Funktionsdekoratoren zu verstehen, ist es nötig, sich zunächst mit Funktionen selbst gut auszukennen. Python Decorators erkennen Sie im Code durch einen eigenen Python-Operator: Das „@“-Zeichen gefolgt vom Namen des Funktionsdekorators.

Die grundlegende Syntax eines Python-Decorator-Aufrufs können Sie im folgenden Codebeispiel sehen, das allerdings keine Funktionalität implementiert:

@dekorator
def funktion():
	pass

Der in „dekorator“ hinterlegte Code würde in diesem Beispiel ausgeführt, wenn die Funktion namens „funktion“ aufgerufen wird.

Oft werden Funktionsdekoratoren auch in der objektorientierten Programmierung mit Python genutzt. So gibt es z. B. den Decorator Python Property. Dieser wird als Äquivalent zu Getter- und Setter-Methoden in anderen Programmiersprachen verwendet.

Tipp

Python ist nicht nur dank praktischen Konstrukten wie Funktionsdekoratoren eine ideale Programmiersprache für Webprojekte. Ebenfalls hervorragend geeignet für Webprojekte ist IONOS Deploy Now. Sie können Ihr Projekt mit Deploy Now einfach via GitHub bereitstellen und builden und behalten so jederzeit den Überblick.

Python Decorators im Einsatz

Kernfunktionalität mithilfe von Python Decorators erweitern

Python Decorators werden in den meisten Fällen genutzt, um die Kernfunktionalität einer Funktion zu erweitern. Dies ist praktisch, wenn Sie ein und dieselbe Grundfunktion in mehreren Usecases einsetzen und individuell erweitern möchten. Ein einfaches, aber bereits lauffähiges Codebeispiel verdeutlicht den Einsatz von Python Decorators zur Erweiterung von Funktionalität:

def dec(funktion):
	def foo(x):
		print("Vor dem Funktionsaufruf von " + funktion.__name__)
		funktion(x)
print("Nach dem Funktionsaufruf von " + funktion.__name__)
	return foo
@dec
def bar(y):
	print("Funktionsaufruf von bar mit dem Wert " + str(y))
bar("Test")

Zunächst wird im Codebeispiel der Decorator namens „dec“ erstellt, der wiederrum eine Funktion namens „foo“ beinhaltet. Wie Sie sehen, handelt es sich bei dem Funktionsdekorator im Grunde genommen um nichts anderes als eine eigenständige Wrapper-Funktion, die in unserem Fall eine andere Funktion namens „foo“ enthält. In „foo“ wird zunächst ausgegeben, dass wir uns vor dem Funktionsaufruf der dem Dekorator im Parameter „funktion“ übergebenen Funktion befinden. Anschließend wird die Funktion aus dem Parameter ausgeführt. Danach erfolgt erneut ein Python-print-Aufruf, der mitteilt, dass wir uns nach dem Funktionsaufruf der als Parameter übergebenen Funktion befinden.

Der zweite Teil des Codes besteht aus einer Funktionsdefinition der Funktion namens „bar“, die einen Übergabeparameter namens „y“ entgegennimmt. Die Funktionalität von bar ist leicht nachzuvollziehen: Sie gibt den Satz „Funktionsaufruf von bar mit dem Wert y“ auf dem Bildschirm aus, wobei für y der als Parameter übergebene Wert eingesetzt wird. Das Besondere an der bar-Funktion ist, dass sie dekoriert wurde. Dies erkennen Sie im Codebeispiel an der Zeile „@dec“ vor der Funktionsdefinition.

Doch was genau heißt es, dass die Funktion dekoriert wurde? Angenommen, wir hätten den Python Decorator, also die Codezeile „@dec“, weggelassen. Der Funktionsaufruf von „bar“, der unser Codebeispiel abschließt, würde dann folgenden Output liefern:

Funktionsaufruf von bar mit dem Wert Test

Hier passiert genau das, was man vom Funktionsaufruf erwartet: Der für den Parameter y übergebene Python String „Test“ wird in das print-Statement eingesetzt. Entsprechend sieht die Ausgabe der Funktion aus.

Betrachten wir nun den Output desselben „bar“-Aufrufs, wobei die „bar“-Funktion dieses Mal mit unserem Python Decorator dekoriert wurde:

Vor dem Funktionsaufruf von bar
Funktionsaufruf von bar mit dem Wert Test
Nach dem Funktionsaufruf von bar

Was Sie sehen, überrascht Sie vielleicht: Nachdem unsere Funktion dekoriert wurde, gibt sie nicht mehr nur die Ausgabe des eigenen print-Statements auf dem Bildschirm aus. Vielmehr wurde die Ausgabe unseres Funktionsdekorators eingekapselt, sodass nun auch die beiden print-Statements aus der Hilfsfunktion „foo“ berücksichtigt werden. Die Kernfunktionalität von „bar“ wurde also erweitert, indem durch den Einsatz des Python Decorators zwei weitere print-Ausgaben hinzugenommen wurden.

Natürlich ist dieses Beispiel artifiziell und verfolgt keine tiefere Programmierlogik. Um sich einen Überblick über die Funktionsweise von Python Decorators zu verschaffen, reicht es allerdings aus. In der Dekorator-Funktion können Sie selbstverständlich beliebige Python-Funktionalität einbinden.

Wiederkehrende Bedingungen mit Python Decorators abfragen

Es kann sein, dass Sie die Ausführung bestimmter Funktionen an eine Bedingung knüpfen möchten. Hierfür sind Ihnen vermutlich bereits die Python-if-else-Statements bekannt. Wenn es sich bei diesen Bedingungen aber um solche handelt, die an verschiedener Stelle überprüft werden müssen, kann es für die Übersichtlichkeit Ihres Codes hilfreich sein, die Bedingung in einen Python Decorator auszulagern.

Auch hier hilft ein Codebeispiel, um sich die Anwendung des Decorators zu veranschaulichen. Manche mathematischen Operatoren sind nur auf den natürlichen Zahlen definiert. Hilfreich wäre es daher, einen Funktionsdekorator zu haben, der überprüft, ob der Übergabeparameter einer Funktion eine natürliche Zahl ist.

def natuerliche_zahl(funktion):
	def test(x):
		if type(x) == int and x > 0:
			return funktion(x)
		else:
			raise Exception("Das Argument ist keine natürliche Zahl")
@natuerliche_zahl
def fak(n):
	if n == 1:
		return 1
	else:
		return n * fak(n-1)
print(fak(5))
print(fak(-1))

Im obigen Code definieren wir zunächst unseren Python Decorator namens „natuerliche_zahl“, der testet, ob es sich bei dem Argument der ihm übergebenen Funktion namens „funktion“ um eine natürliche Zahl handelt. Hierzu wird in der if-Bedingung zunächst der Typ des Arguments überprüft. Außerdem wird getestet, ob es sich bei dem Argument um eine positive Zahl größer als 0 handelt. Wenn dies der Fall ist, wird die dem Dekorator als Parameter übergebene Funktion ausgeführt. Anderenfalls wird eine Exception geworfen, die mitteilt, dass es sich bei dem Argument der Funktion um keine natürliche Zahl handelt.

Die Funktionsweise unseres Python Decorators kann man sich in der Praxis verdeutlichen, indem man die mit ihm dekorierte Funktion namens „fac“ betrachtet. Diese wird im Code definiert und im Anschluss zuerst mit dem Wert „5“, dann mit dem Wert „-1“ aufgerufen. Der Output sieht dann folgendermaßen aus:

120
Traceback (most recent call last):
  File "<pyshell#17>", line 1, in <module>
    fak(-1)
  File "<pyshell#11>", line 6, in test
    raise Exception("Das Argument ist keine natürliche Zahl")
Exception: Das Argument ist keine natürliche Zahl

Zunächst sehen Sie die Zahl „120“, was der Fakultät von 5 entspricht. Für natürliche Zahlen funktioniert die Fakultätsfunktion also. Der Aufruf der Fakultätsfunktion mit einer negativen Zahl führt allerdings zu einem Fehler – und das liegt am Python Decorator! Da es sich bei einer negativen Zahl um keine natürliche Zahl handelt, soll die Fakultätsfunktion nicht ausgeführt werden.