Dekorateure¶
Einführung¶
Dekorateure gehören vermutlich zu den leistungsstärksten Design-Möglichkeiten von Python. Gleichzeitig wird es von vielen als schwierig betrachtet, einen Einstieg in die Thematik zu finden. Um es genauer zu sagen: Die Nutzung von Dekorateuren ist einfach. Aber das Schreiben von Dekorateuren kann sich als kompliziert erweisen, insbesondere dann, wenn man noch nicht sehr erfahren ist. In Python gibt es zwei verschiedene Arten von Dekorateuren:
- Funktions-Dekorateure
- Klassen-Dekorateure
Ein Dekorateur in Python ist ein beliebiges aufrufbares Python-Objekt, welches zur Modifikation einer Funktion oder einer Klasse genutzt wird. Eine Referenz zu einer Funktion "func" oder einer Klasse "C" wird an den Dekorateur übergeben und der Dekorateur liefert eine modifizierte Funktion oder Klasse zurück. Die modifizierten Funktionen oder Klassen rufen üblicherweise intern die Original-Funktion "func" oder -Klasse "C" auf.
Sie können ebenfalls das Kapitel zu Memoisation durcharbeiten.
Sollten Sie die Grafik auf der rechten Seite mögen und Sie auch noch Interesse an Bildbearbeitung mit Python, Numpy, Scipy und Matplotlib haben, sollten Sie definitiv auch das Kapitel Techniken der Bildverarbeitung anschauen. Dort wird der gesamte Prozess der Erzeugung unserer Grafik erklärt.
Vorübungen zu Dekorateuren¶
Aus Erfahrung können wir sagen, dass es für Anfänger beim Thema Dekorateure einige Schwierigkeiten mit der Benutzung von Funktionen gibt. Aus diesem Grund wiederholen wir an dieser Stelle einige wichtige Aspekte der Funktionen.
Funktionsnamen sind Referenzen auf Funktionen, und wir können mehrere Funktionsnamen für ein und dieselbe Funktion vergeben:
def nachfg(x):
return x + 1
nachfolger = nachfg
nachfolger(10)
nachfg(10)
Wir haben jetzt also zwei Namen (nachfg
und nachfolger
) für ein und dieselbe Funktion.
Der nächste wichtige Punkt ist, dass wir nachfg
oder nachfolger
löschen können, ohne die eigentliche Funktion zu entfernen.
del nachfg
nachfolger(10)
Funktionen in Funktionen¶
Das Konzept von verschachtelten Funktionen, also Funktionsdefinitionen innerhalb einer Funktion, ist für C/C++-Programmierer völlig neu:
def f():
def g():
print("Hallo, ich bin es, 'g'")
print("Danke für's Aufrufen")
print("Dies ist die Funktion 'f'")
print("'f' ruft nun 'g' auf!")
g()
f()
Es folgt nun ein Beispiel mit einer "richtigen" Funktion im Inneren mit einer return-Anweisungen:
def Temperatur(t):
def celsius2fahrenheit(x):
return 9 * x / 5 + 32
Ergebnis = "Es ist " + str(celsius2fahrenheit(t)) + " grad!"
return Ergebnis
print(Temperatur(20))
Im folgenden Beispiel geht es um die Funktion Fakultätsfunktion "factorial", die wir zuvor wie folgt definiert haben:
def factorial(n):
""" calculates the factorial of n,
n should be an integer and n <= 0 """
if n == 0:
return 1
else:
return n * factorial(n-1)
Was passiert, wenn jemand einen negativen Wert oder eine Fließkommazahl an diese Funktion übergibt? Sie wird niemals enden.
Man könnte auf die Idee kommen, das wie folgt zu überprüfen:
def factorial(n):
""" calculates the factorial of n,
n should be an integer and n <= 0 """
if type(n) == int and n >=0:
if n == 0:
return 1
else:
return n * factorial(n-1)
else:
raise TypeError("n has to be a positive integer or zero")
Ruft man diese Funktion zum Beispiel mit 4
auf, also factorial(4)
, so wird als erstes geprüft, ob es sich um eine positive ganze Zahl handelt. Im Prinzip ist das auch sinnvoll. Das "Problem" tritt nun im Rekursionsschritt auf. Nun wird factorial(3)
aufgerufen. Bei diesem und allen anderen Aufrufen wird ebenfalls geprüft, ob es sich um eine positive ganze Zahl handelt. Das ist aber unnötig: Wenn man von einer positiven ganzen Zahl den Wert 1
subtrahiert, erhält man wieder eine positive ganze Zahl oder 0
. Also beides wohldefinierte Argumentwerte für unsere Funktion.
Mit einer verschachtelten Funktion (lokale Funktion) kann man dieses Problem elegant lösen:
def factorial(n):
""" calculates the factorial of n,
n should be an integer and n <= 0 """
def inner_factorial(n):
if n == 0:
return 1
else:
return n * inner_factorial(n-1)
if type(n) == int and n >=0:
return inner_factorial(n)
else:
raise TypeError("n should be a positve int or 0")
Wir können den Bereich der möglichen Eingabewerte für unsere Funktion factorial
erweitern, indem wir Fließkommazahlen zulassen, die äquivalent zu ganzen Zahlen sind, d.h. die die Bedingung
int(x) == x
erfüllen.
Wenn wir wissen, dass eine Variable x auf einen Float-Wert verweist, können wir auch den Test x.is_integer()
verwenden.
Die folgende Implementierung von factorial folgt einer detaillierteren Fallanalyse des Arguments, wie zuvor besprochen:
def faktoriell(n):
""" Berechnet die Fakultät von n, wenn n entweder eine nicht negative
Ganzzahl oder eine Fließkommazahl ist, wobei x einer Ganzzahl entspricht, wie
4.0, 12.0, 8. d.h. keine Dezimalstellen nach dem Komma """
def inner_factorial(n):
if n == 0:
1 zurückgeben
else:
return n * inner_factorial(n-1)
if not isinstance(n, (int, float)):
raise ValueError("Wert ist weder ein Integer noch ein Float äquivalent zu int")
wenn isinstance(n, (int)) und n < 0:
raise ValueError('Sollte eine positive ganze Zahl oder 0 sein')
elif isinstance(n, (float)) und nicht n.is_integer():
raise ValueError('Wert ist ein Float, entspricht aber nicht einem Int')
sonst:
return inner_factorial(n)
Nun testen wir die vorige Funktion:
values = [0, 1, 5, 7.0, -4, 7.3, "7"]
for value in values:
try:
print(value, end=", ")
print(factorial(value))
except ValueError as e:
print(e)
Funktionen als Parameter¶
Wenn wir die Beispiele für sich betrachten, dann nützen sie nicht viel. Brauchbar werden sie erst in Kombination mit zwei weiteren starken Möglichkeiten der Python-Funktionen.
Jeder Parameter einer Funktion ist eine Referenz auf ein Objekt. Und Funktionen sind ebenfalls Objekte. Somit können wir Funktionen, oder besser "Referenzen auf Funktionen", ebenfalls als Argumente an Funktionen übergeben. Wir demonstrieren am folgenden Beispiel:
def g():
print("Hallo, ich bin es, 'g'")
print("Danke für's Aufrufen")
def f(func):
print("Hallo, ich bin es")
print("Ich werde jetzt 'func' aufrufen")
func()
f(g)
Möglicherweise sind Sie mit der Ausgabe nicht ganz zufrieden. 'f' sollte ausgeben, dass 'g' aufgerufen wird, und nicht 'func'. Dazu müssen wir den "wirklichen" Namen von 'func' wissen. In diesem Fall können wir das Attribut __name__
des Funktions-Objekts benutzen, welches den Namen der Funktion beinhaltet:
def g():
print("Hallo, ich bin es, 'g'")
print("Danke für's Aufrufen")
def f(func):
print("Hallo, ich bin es, 'f'")
print("Ich werde jetzt 'func' aufrufen")
func()
print("func's echter Name ist " + func.__name__)
f(g)
Einmal mehr zeigt die Ausgabe, was hier passiert.
Noch ein Beispiel:
import math
def foo(func):
print("Die Funktion " + func.__name__ + " wurde an foo übergeben")
res = 0
for x in [1, 2, 2.5]:
res += func(x)
return res
print(foo(math.sin))
print(foo(math.cos))
Funktionen geben Funktionen zurück¶
Die Rückgabe einer Funktion ist auch eine Referenz auf ein Objekt. Somit können auch Referenzen auf Funktionen zurückgegeben werden.
def f(x):
def g(y):
return y + x + 3
return g
nf1 = f(1)
nf2 = f(3)
print(nf1(1))
print(nf2(1))
Das vorherige Beispiel sieht sehr künstlich und absolut nutzlos aus. Wir werden nun ein anderes sprachorientiertes Beispiel vorstellen, das einen praktischeren Touch hat. Okay, immer noch keine Funktion, die wirklich nützlich wäre. Wir schreiben eine Funktion mit dem fast selbsterklärenden Namen greeting_func_gen
. Diese Funktion gibt also Funktionen zurück (oder generiert sie), mit denen man Personen in verschiedenen Sprachen erstellen kann, z.B. Deutsch, Französisch, Italienisch, Türkisch und Griechisch:
def greeting_func_gen(lang):
def customized_greeting(name):
if lang == "de": # German
phrase = "Guten Morgen "
elif lang == "fr": # French
phrase = "Bonjour "
elif lang == "it": # Italian
phrase = "Buongiorno "
elif lang == "tr": # Turkish
phrase = "Günaydın "
elif lang == "gr": # Greek
phrase = "Καλημερα "
else:
phrase = "Hi "
return phrase + name + "!"
return customized_greeting
say_hi = greeting_func_gen("tr")
print(say_hi("Gülay")) # this Turkish name means "rose moon" by the way
Ein nützlicheres Beispiel¶
Nützlicher und gleichzeitig mathematisch orientierter wird es im folgenden Beispiel. Nehmen wir an, wir müssen viele Polynome vom Grad 2 definieren. Das könnte so aussehen:
def p1(x):
return 2*x**2 - 3*x + 0.5
def p2(x):
return 2.3*x**2 + 2.9*x - 20
def p3(x):
return -2.3*x**2 + 4.9*x - 9
Dies kann durch die Implementierung einer Polynom-"Fabrik"-Funktion vereinfacht werden. Wir beginnen mit einer Version, die Polynome vom Grad 2 erzeugen kann.
$$p(x) = ax^2 + bx + c$$Die Python-Implementierung als Polynom-Fabrikfunktion kann wie folgt geschrieben werden:
def polynomial_creator(a, b, c):
def polynomial(x):
return a * x**2 + b * x + c
return polynomial
p1 = polynomial_creator(2, -3, 0.5)
p2 = polynomial_creator(2.3, 2.9, -20)
p3 = polynomial_creator(-2.3, 4.9, -9)
for x in range(-2, 2, 1):
print(x, p1(x), p2(x))
Wir können unsere Fabrikfunktion so verallgemeinern, dass sie für Polynome beliebigen Grades funktioniert:
$$\sum_{k=0}^{n} a_{k} \cdot x^{k} = a_{n} \cdot x^{n} + a_{n-1} \cdot x^{n-1} + ... + a_{2} \cdot x^{2} + a_{1} \cdot x + a_{0} $$def polynomial_creator(*coefficients):
""" coefficients are in the form a_n, ... a_1, a_0
"""
def polynomial(x):
res = 0
for index, coeff in enumerate(coefficients[::-1]):
res += coeff * x** index
return res
return polynomial
p1 = polynomial_creator(4)
p2 = polynomial_creator(2, 4)
p3 = polynomial_creator(1, 8, -1, 0, 3, 2)
p4 = polynomial_creator(-1, 2, 1)
p5 = polynomial_creator(4, 5, 7, 7, 9, 12, 3, 43, 9)
for x in range(-2, 2, 1):
print(x, p1(x), p2(x), p3(x), p4(x), p5(x))
Die Funktion p3 implementiert z. B. das folgende Polynom:
$$p_3(x) = x^{5} + 8 \cdot x^{4} - x^{3} + 3 \cdot x + 2 $$Die Polynomfunktion innerhalb unseres Dekorators polynomial_creator
kann effizienter implementiert werden. Wir können sie so faktorisieren, dass sie keine Potenzierung benötigt.
Faktorisierte Version eines allgemeinen Polynoms ohne Potenzierung:
$$res = (...(a_{n} \cdot x + a_{n-1}) \cdot x + ... + a_{1}) \cdot x + a_{0}$$Implementierung unseres Polynomschöpfer-Dekorators unter Vermeidung der Potenzierung:
def polynomial_creator(*coeffs):
""" coefficients are in the form a_n, a_n_1, ... a_1, a_0
"""
def polynomial(x):
res = coeffs[0]
for i in range(1, len(coeffs)):
res = res * x + coeffs[i]
return res
return polynomial
p1 = polynomial_creator(4)
p2 = polynomial_creator(2, 4)
p3 = polynomial_creator(1, 8, -1, 0, 3, 2)
p4 = polynomial_creator(-1, 2, 1)
p5 = polynomial_creator(4, 5, 7, 7, 9, 12, 3, 43, 9)
for x in range(-2, 2, 1):
print(x, p1(x), p2(x), p3(x), p4(x), p5(x))
Mehr über Polynome und die Erstellung einer Polynomklasse gibt es in unserem Kurs Polynome fortfahren.
Ein einfacher Dekorateur¶
Jetzt haben wir alles um unseren ersten einfachen Dekorateur zu bauen:
def ein_dekorateur(func):
def hilfsfunktion(x):
print("Vor dem Aufruf " + func.__name__)
func(x)
print("Nach dem Aufruf " + func.__name__)
return hilfsfunktion
def foo(x):
print("Hallo, foo wurde mit aufgerufen " + str(x))
print("Wir rufen foo vor der Dekoration auf:")
foo("Hi")
print("Wir dekorieren jetzt foo mit f:")
foo = ein_dekorateur(foo)
print("Wir rufen foo nach der Dekoration auf:")
foo(42)
Wenn wir uns die folgende Ausgabe anschauen, dann sehen wir was passiert. Nach der Dekoration foo = ein_dekorateur(foo)
ist foo eine Referenz auf die Funktion hilfsfunktion
. foo wird innerhalb von hilfsfunktion
aufgerufen, aber vorher und nachher wird noch etwas code ausgeführt. In diesem Beispiel zwei print
-Funktionen.
Die übliche Dekorateur-Syntax in Python¶
Die Dekoration in Python wird in der Regel nicht so gemacht, wie wir es im vorigen Beispiel gezeigt haben. Die Schreibweise foo = ein_dekorateur(foo)
ist zwar einfach zu verstehen und einprägsam, aber in dem Beispiel zeigt sich auch ein Problem. foo
wird in dem vorigen Programm in zwei Versionen benutzt, nämlich vor der Dekoration und nach der Dekoration.
Der übliche Weg eine selbst-definierte Funktion zu dekorieren, passiert in der Zeile über dem Funktions-Kopf. Dort schreibt man ein "@"-Zeichen gefolgt von dem Funktionsnamen des Dekorateurs.
Wir ersetzen diese Anweisung
foo = ein_dekorateur(foo)durch
@ein_dekorateurDiese Zeile muss direkt über der zu dekorierenden Funktion platziert werden. Das komplette Beispiel sieht nun folgendermaßen aus:
def ein_dekorateur(func):
def hilfsfunktion(x):
print("Vor dem Aufruf " + func.__name__)
func(x)
print("Nach dem Aufruf " + func.__name__)
return hilfsfunktion
@ein_dekorateur
def foo(x):
print("Hallo, foo wurde mit aufgerufen " + str(x))
foo("Hallo ")
Wir können auch andere Funktionen mit einem Parameter mit unserem Dekorateur dekorieren. Im Folgenden werden wir dies demonstrieren. Wir haben hilfsfunktion
etwas verändert, so das wir die Funktions-Aufrufe sehen können:
def ein_dekorateur(func):
def hilfsfunktion(x):
print("Vor dem Aufruf " + func.__name__)
res = func(x)
print(res)
print("Nach dem Aufruf " + func.__name__)
return hilfsfunktion
@ein_dekorateur
def nachfg(n):
return n + 1
nachfg(10)
Es ist ebenfalls möglich weitere Funktionen, die von anderen geschrieben worden sind, zu dekorieren. Also Funktionen, die wir beispielsweise aus einem Modul importieren. In solchen Fällen und in unserem nächsten Beispiel können wir die Dekorationsweise mit dem ,,@''-Zeichen nicht verwenden.
from math import sin, cos
def ein_dekorateur(func):
def hilfsfunktion(x):
print("Vor dem Aufruf " + func.__name__)
res = func(x)
print(res)
print("Nach dem Aufruf " + func.__name__)
return hilfsfunktion
sin = ein_dekorateur(sin)
cos = ein_dekorateur(cos)
for f in [sin, cos]:
f(3.1415)
Zusammenfassend halten wir folgendes fest. Ein Dekorateur ist ein aufrufbares Python-Objekt, welches zur Modifikation von Funktions-, Methoden- oder Klassen-Definitionen genutzt werden kann. Das Original-Objekt, welches also modifiziert werden soll, wird dem Dekorateur als Argument übergeben. Der Dekorateur liefert dann das modifizierte Objekt zurück.
from random import random, randint, choice
def ein_dekorateur(func):
def hilfsfunktion(*args, **kwargs):
print("Vor dem Aufruf " + func.__name__)
res = func(*args, **kwargs)
print(res)
print("Nach dem Aufruf " + func.__name__)
return hilfsfunktion
random = ein_dekorateur(random)
randint = ein_dekorateur(randint)
choice = ein_dekorateur(choice)
# Aufrufe unserer dekorierten Funktionen:
random()
randint(3, 8)
choice([4, 5, 6])
Die Ausgabe sieht aus wie erwartet.
Erweitern der trigonometrischen Funktionen¶
In unserem nächsten Beispiel zur Verwendung von Dekoratoren zeigen wir, dass wir Dekoratoren verwenden können, um die Möglichkeiten von Funktionen zu erweitern. In unserem Fall fügen wir einen zusätzlichen Parameter hinzu.
Wir erstellen einen Dekorator für die trigonometrischen Funktionen des Moduls math
. Wenn man sich die Hilfe von sin
, cos
oder den anderen trigonometrischen Funktionen des math-Moduls ansieht, erkennt man, dass die Argumente im Bogenmaß (rad
) angegeben werden müssen. Was ist, wenn man Gradmaße verwenden möchte? In diesem Fall muss man die Gradmaße in Bogenmaß umwandeln.
from math import sin, cos, pi
help(sin)
angle = 45 # Gradmaß
x = angle * pi / 180 # Wandlung in Bogenmaß
sin(x)
Wir könnten die trigonometrischen Funktionen auch mit einem Dekorator erweitern, der Gradangaben automatisch in Bogenmaße umwandelt. Wir fügen einen weiteren Parameter zu unserer helper
-Funktion hinzu.
from math import sin, cos, pi
def angle_deco(func):
def helper(x, mode="radians"):
if mode == "degrees":
x = x * pi / 180
return func(x)
return helper
sin = angle_deco(sin)
cos = angle_deco(cos)
Jetzt haben wir, was wir wollten: Wir können die Funktionen sin
und cos
sowohl mit Bogenmaß als auch mit Gradmaß aufrufen:
degrees = [40, 45, 70, 90]
for degree in degrees:
print(sin(degree, mode='degrees'), cos(degree, mode='degrees'))
Allgemeiner Dekorator¶
Alles in allem können wir sagen, dass ein Dekorator in Python ein aufrufbares Python-Objekt ist, das verwendet wird, um eine Funktion, Methode oder Klassendefinition zu ändern. Das ursprüngliche Objekt, das geändert werden soll, wird einem Dekorator als Argument übergeben. Der Dekorator gibt ein modifiziertes Objekt zurück, z. B. eine modifizierte Funktion, die an den in der Definition verwendeten Namen gebunden ist.
Der bisherige function_wrapper
(bzw. helper
) funktioniert nur für Funktionen mit genau einem Parameter. Wir stellen eine verallgemeinerte Version des function_wrapper zur Verfügung, die Funktionen mit beliebigen Parametern im folgenden Beispiel akzeptiert:
from random import random, randint, choice
def our_decorator(func):
def function_wrapper(*args, **kwargs):
print("Before calling " + func.__name__)
res = func(*args, **kwargs)
print(res)
print("After calling " + func.__name__)
return function_wrapper
random = our_decorator(random)
randint = our_decorator(randint)
choice = our_decorator(choice)
random()
randint(3, 8)
choice([4, 5, 6])
Benutzung mehrerer Dekorateure¶
Man kann auch mehrere Dekorateure auf eine Funkion anwenden, wie wir im folgenden Python-Programm sehen:
def deco1(func):
print('deco1 has been called')
def helper(x):
print('helper of deco1 has been called!')
print(x)
return func(x) + 3
return helper
def deco2(func):
print('deco2 has been called')
def helper(x):
print('helper of deco2 has been called!')
print(x)
return func(x) + 2
return helper
def deco3(func):
print('deco3 has been called')
def helper(x):
print('helper of deco3 has been called!')
print(x)
return func(x) + 1
return helper
@deco3
@deco2
@deco1
def foobar(x):
return 42
Die Ausgabe zeigt uns, dass die Funktion foobar
zuerst mit deco1
dekoriert wird, d.h. dem Dekorator direkt über der Funktionsdefinition. Danach wird sie mit deco2
und dann mit deco3
dekoriert.
Wenn wir die mehrfach dekorierte Funktion aufrufen, funktioniert es andersherum:
foobar(42)
Weitere Anwendungsfälle für Dekorateure¶
Überprüfung von Argumenten durch Dekorateure¶
Bisher haben wir viele Funktionen definiert, die ein Argument benötigen, und wir haben nicht geprüft, ob der Typ des Arguments korrekt ist. Der Hauptgrund dafür war, dass wir uns auf die grundlegenden Ideen von Funktionen wie fibonacci
und factorial
konzentrieren wollten. Wir haben zu Beginn dieses Kapitels eine Lösung für die factorial
Funktion mit einer verschachtelten Hilfsfunktion vorgestellt.
Wir wollen nun zeigen, wie wir einen Dekorator schreiben können, um zu prüfen, ob ein einzelnes Argument eine positive ganze Zahl ist. In unserem Kurs über rekursive Funktionen haben wir die faktorielle Funktion eingeführt. Wir wollten die Funktion so einfach wie möglich halten und die zugrundeliegende Idee nicht verschleiern, also haben wir keine Argumentprüfungen eingebaut. Wenn also jemand unsere Funktionen mit einem negativen Argument oder mit einem Float-Argument aufruft, würde unsere Funktionen in eine Endlosschleife geraten.
Das folgende Programm verwendet eine Dekorfunktion, um sicherzustellen, dass das an die Funktion factorial übergebene Argument eine positive ganze Zahl ist:
def argument_test_natural_number(f):
def helper(x):
if type(x) == int and x > 0:
return f(x)
else:
raise ValueError("Argument ist keine ganze Zahl")
return helper
@argument_test_natural_number
def is_prime(n):
return all(n % i for i in range(2, n))
for i in range(1,10):
print(i, is_prime(i))
try:
print(is_prime(-1))
except ValueError:
print("Argument ist keine ganze Zahl!")
Dieses Programm demonstriert erneut, wie Funktionsdekoratoren verwendet werden können, um zusätzliche Funktionen oder Validierungsprüfungen zu bestehenden Funktionen hinzuzufügen, ohne deren ursprünglichen Code zu ändern. In diesem Fall stellt der Dekorator sicher, dass das an die Funktion is_prime
übergebene Argument eine positive ganze Zahl ist.
Funktionsaufrufe mit Dekorateuren zählen¶
Unsere bisherigen Beispiele für Dekorateure waren nur für Funktionen mit einem Parameter geeignet. Wir zeigen nun, wie wir mittels *args und **kwargs beliebige Dekorateure schreiben können, d.h. solche, die mit einer beliebigen Anzahl an Positions- und Keywordparametern umgehen können.
def call_counter(func):
def helper(x):
helper.calls += 1
return func(x)
helper.calls = 0
return helper
@call_counter
def succ(x):
return x + 1
print(succ.calls)
for i in range(10):
succ(i)
print(succ.calls)
Wir haben gesagt, dass der Dekorateur nur für Funktionen mit einem Parameter geeignet ist. Jetzt benutzen wir die Schreibweise *args und **kwargs um Dekorateure zu schreiben, die mit einer beliebigen Anzahl an Positions- und Keyword-Parametern umgehen können.
def call_counter(func):
def helper(*args, **kwargs):
helper.calls += 1
return func(*args, **kwargs)
helper.calls = 0
return helper
@call_counter
def succ(x):
return x + 1
@call_counter
def mul1(x, y=1):
return x*y + 1
print(succ.calls)
for i in range(10):
succ(i)
mul1(3, 4)
mul1(4)
mul1(y=3, x=2)
print(succ.calls)
print(mul1.calls)
Dekorateure mit Parametern¶
Wir definieren im Folgenden zwei Dekorateure:
def evening_greeting(func):
def function_wrapper(x):
print("Good evening, " + func.__name__ + " returns:")
return func(x)
return function_wrapper
def morning_greeting(func):
def function_wrapper(x):
print("Good morning, " + func.__name__ + " returns:")
return func(x)
return function_wrapper
@evening_greeting
def foo(x):
print(42)
foo("Hi")
Diese beiden Dekorateure sind beinahe identisch, außer die Begrüßung. Wir wollen dem Dekorateur nun einen Parameter verpassen, damit der Gruß bei der Dekoration verändert werden kann. Damit das klappt müssen wir unsere Dekoratorfunktion in eine Wrapper-Funktion einpacken. Jetzt können wir beliebig grüßen, also zum Beispiel auch auf griechisch "Guten Morgen" sagen:
def greeting(expr):
def greeting_decorator(func):
def function_wrapper(x):
print(expr + ", " + func.__name__ + " returns:")
func(x)
return function_wrapper
return greeting_decorator
@greeting("καλημερα")
def foo(x):
print(42)
foo("Hi")
Wenn wir nicht die "@"-Schreibweise verwenden, sondern die Funktions-Aufrufe, dann können wir dies wie folgt tun:
def greeting(expr):
def greeting_decorator(func):
def function_wrapper(x):
print(expr + ", " + func.__name__ + " returns:")
return func(x)
return function_wrapper
return greeting_decorator
def foo(x):
print(42)
greeting2 = greeting("καλημερα")
foo = greeting2(foo)
foo("Hi")
Das Ergebnis ist identisch.
Natürlich ist die zusätzliche Definition von "special_greeting" nicht nötig. Wir können die Rückgabe aus "greeting("καλημερα")" direkt an "foo" übergeben.
foo = greeting("καλημερα")(foo)
Wraps-Dekorateur von functools¶
Die Art und Weise, wie wir bisher Dekoratoren definiert haben, hat nicht berücksichtigt, dass die Attribute
__name__
(Name der Funktion),__doc__
(der Docstring) und__module__
(Das Modul, in dem die Funktion definiert ist)
nach der Dekoration der ursprünglichen Funktionen verloren gegangen sind.
Wir demonstrieren dies im Folgenden:
def greeting(func):
def function_wrapper(x):
""" function_wrapper of greeting """
print("Hi, " + func.__name__ + " returns:")
return func(x)
return function_wrapper
Wir rufen diese Funktion nun mit folgendem Python-Code auf:
@greeting
def f(x):
""" just some silly function """
return x + 4
f(10)
print("function name: " + f.__name__)
print("docstring: " + f.__doc__)
print("module name: " + f.__module__)
Die Ergebnisse, die wir erhalten, sind "unerwünscht".
Wir können die ursprünglichen Attribute der Funktion f speichern, wenn wir sie innerhalb des Dekorators zuweisen. Wir ändern unseren bisherigen Dekorator entsprechend:
def greeting(func):
def function_wrapper(x):
""" function_wrapper of greeting """
print("Hi, " + func.__name__ + " returns:")
return func(x)
function_wrapper.__name__ = func.__name__
function_wrapper.__doc__ = func.__doc__
function_wrapper.__module__ = func.__module__
return function_wrapper
@greeting
def f(x):
""" just some silly function """
return x + 4
f(10)
print("function name: " + f.__name__)
print("docstring: " + f.__doc__)
print("module name: " + f.__module__)
Glücklicherweise müssen wir nicht all diesen Code zu unseren Dekoratoren hinzufügen, um diese Ergebnisse zu erzielen. Wir können stattdessen den Dekorator "wraps" von functools importieren und unsere Funktion im Dekorator damit dekorieren:
from functools import wraps
def greeting(func):
@wraps(func)
def function_wrapper(x):
""" function_wrapper of greeting """
print("Hi, " + func.__name__ + " returns:")
return func(x)
return function_wrapper
@greeting
def f(x):
""" just some silly function """
return x + 4
f(10)
print("function name: " + f.__name__)
print("docstring: " + f.__doc__)
print("module name: " + f.__module__)
Klassen statt Funktionen¶
Die call Methode¶
Bis hierher haben wir Funktionen als Dekorateure verwendet. Bevor wir einen Dekorateur als Klasse definieren können, führen wir die __call__
Methode der Klassen ein. Wir hatten bereits erwähnt, dass ein Dekorateur einfach ein aufrufbares Objekt ist, welches eine Funktion als Parameter entgegennimmt. Was die meisten Programmierer aber nicht wissen ist, dass wir Klassen ebenfalls als solches definieren können. Die __call__
Methode wird aufgerufen, wenn die Instanz "wie eine Funktion" aufgerufen wird. Sprich, man benutzt Klammern.
class A:
def __init__(self):
print("Eine Instanz von A wurde initialisiert")
def __call__(self, *args, **kwargs):
print("Argumente sind:", args, kwargs)
x = A()
print("Rufen Sie jetzt die Instanz auf:")
x(3, 4, x=11, y=10)
print("Rufen wir es noch einmal auf:")
x(3, 4, x=11, y=10)
Wir können eine Klasse für die Fibonacci-Funktion schreiben, indem wir __call__
benutzen:
class Fibonacci:
def __init__(self):
self.cache = {}
def __call__(self, n):
if n not in self.cache:
if n == 0:
self.cache[0] = 0
elif n == 1:
self.cache[1] = 1
else:
self.cache[n] = self.__call__(n-1) + self.__call__(n-2)
return self.cache[n]
fib = Fibonacci()
for i in range(15):
print(fib(i), end=", ")
Eine Klasse als Dekorateur benutzen¶
Wir werden folgenden Dekorateur als Klasse umschreiben:
def decorator1(f):
def helper():
print("Dekorieren", f.__name__)
f()
return helper
@decorator1
def foo():
print("innen foo()")
foo()
Der folgende Dekorateur, der als Klasse implementiert ist, erledigt die gleiche Aufgabe:
class decorator2(object):
def __init__(self, f):
self.f = f
def __call__(self):
print("Dekorieren", self.f.__name__)
self.f()
@decorator2
def foo():
print("innen foo()")
foo()
Beide Versionen liefern das gleiche Ergebnis.