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)
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))
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))
def f(x):
def g(y):
return y + x + 3
return g
nf1 = f(1)
nf2 = f(3)
print(nf1(1))
print(nf2(1))
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.
Anwendungsfälle für Dekorateure
Überprüfung von Argumenten durch Dekorateure
In unserem Kapitel über rekursive Funktionen hatten wir die Fakultätsfunktion eingeführt. Dabei wollten wir die Funktion so einfach wie möglich halten. Hätten wir die Argumente der Fakultätsfunktion auf Plausibilität geprüft, hätten wir die zugrunde liegende Idee verschleiert und der Kern des Algorithmus wäre nicht mehr so erkenntlich gewesen. So darf die Funktion keinesfalls mit negativen Werten oder Fließkommazahlen, also float-Werten, aufgerufen werden. In beiden Fällen kommt es zu einer endlosen Rekursion, die allerdings glücklicherweise durch den endlichen Rekursionsstack von Python mit einem RuntimeError abgebrochen wird:
RuntimeError: maximum recursion depth exceeded in comparison
Das folgende Programm benutzt eine Dekorateur-Funktion um sicherzustellen, dass es sich bei dem verwendeten Argument, um eine positive ganze Zahl handelt:
def Argument_Test_natürliche_Zahl(f):
def Helper(x):
if type(x) == int and x > 0:
return f(x)
else:
raise Exception("Argument ist keine ganze Zahl")
return Helper
@Argument_Test_natürliche_Zahl
def Fakultät(n):
if n == 1:
return 1
else:
return n * Fakultät(n-1)
for i in range(1,10):
print(i, Fakultät(i))
print(Fakultät(-1))
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 Aufruf_zähler(func):
def Helper(x):
Helper.calls += 1
return func(x)
Helper.calls = 0
return Helper
@Aufruf_zähler
def Nachfg(x):
return x + 1
print(Nachfg.calls)
for i in range(10):
print(Nachfg(i))
print(Nachfg.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 Aufruf_Zähler(func):
def Helper(*args, **kwargs):
Helper.calls += 1
return func(*args, **kwargs)
Helper.calls = 0
return Helper
@Aufruf_Zähler
def Nachfg(x):
return x + 1
@Aufruf_Zähler
def mul1(x, y=1):
return x*y + 1
print(Nachfg.calls)
for i in range(10):
Nachfg(i)
mul1(3, 4)
mul1(4)
mul1(y=3, x=2)
print(Nachfg.calls)
print(mul1.calls)
def Abendgruß(func):
def Wrapper_Funktion(x):
print("Guten Abend, gibt " + func.__name__ + " zurück:")
func(x)
return Wrapper_Funktion
def Morgengruß(func):
def Wrapper_Funktion(x):
print("Guten Morgen, gibt " + Funktions_Wrapper +" zurück:")
func(x)
return Wrapper_Funktion
@Abendgruß
def foo(x):
print(42)
foo("Hallo")
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 Gruß(expr):
def Grußdekoration(func):
def Wrapper_Funktion(x):
print(expr + ", " + func.__name__ + " gibt zurück:")
func(x)
return Wrapper_Funktion
return Grußdekoration
@Gruß("καλημερα")
def foo(x):
print(42)
foo("Hallo")
Wenn wir nicht die "@"-Schreibweise verwenden, sondern die Funktions-Aufrufe, dann können wir dies wie folgt tun:
def Gruß(expr):
def Grußdekoration(func):
def Wrapper_Funktion(x):
print(expr + ", " + func.__name__ + " gibt zurück")
func(x)
return Wrapper_Funktion
return Grußdekoration
def foo(x):
print(42)
besondere_Begrüßung = Gruß("καλημερα")
foo = besondere_Begrüßung(foo)
foo("Hallo")
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 Dekorateure 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) der originalen Funktionen nach der Dekoration verloren gehen.
def Gruß(func):
def Wrapper_Funktion(x):
""" Wrapper Funktion der Begrüßung """
print("Hi, " + func.__name__ + " gibt zurück:")
return func(x)
return Wrapper_Funktion
@Gruß
def f(x):
""" nur eine dumme Funktion """
return x + 4
f(10)
print("Funktionsname: " + f.__name__)
print("docstring: " + f.__doc__)
print("Modulname: " + f.__module__)
Wir erhalten "ungewollte" Ergebnisse.
Wir können die originalen Attribute der Funktion f speichern, indem wir sie innerhalb des Dekorateurs zuweisen. Wir passen den vorigen Dekorateur entsprechend an und speichern ihn unter greeting_decorator_manually.py:
def Gruß(func):
def Wrapper_Funktion(x):
""" Wrapper Funktion der Begrüßung """
print("Hi, " + func.__name__ + " gibt zurück:")
return func(x)
Wrapper_Funktion.__name__ = func.__name__
Wrapper_Funktion.__doc__ = func.__doc__
Wrapper_Funktion.__module__ = func.__module__
return Wrapper_Funktion
def f(x):
return x + 42
f = Gruß(f)
f(1)
Jetzt erhalten wir die richtigen Ergebnisse.
Wir müssen zum Glück nicht diesen ganzen Code zu unseren Dekorateuren hinzufügen, um diese Ergebnisse zu erhalten. Wir importieren einfach den Dekorateur "wraps" aus dem Modul "functools" und dekorieren unsere Funktion in dem Dekorateur damit:
from functools import wraps
def Gruß(func):
@wraps(func)
def Wrapper_Funktion(x):
""" Wrapper_Funktion von Gruß """
print("Hallo , " + func.__name__ + " gibt zurück:")
return func(x)
return Wrapper_Funktion
def f(x):
return x + 42
f = Gruß(f)
f(1)
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=", ")
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.