Dekorateure


Einführung

decorators Dekorateure gehören vermutlich zu den Leistungsstärksten Design-Möglichkeiten von Python. Gleichzeitig ist es von den meisten als schwierig betrachtet, einen Einstieg zu finden. Um es ganauer 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 verscheidene Arten von Dekorateuren: 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 definitv auch das Kapitel Techniken der Bildverarbeitung anschauen. Es erklärt den gesamten Prozess des Making-Of unseres Dekorateurs und der Grafik.

Die ersten Schritte mit Dekorateuren

Aus Erfahrung können wir sagen, dass es für Anfänger beim Thema Dekorateure einige Schwierigkeiten gibt. Aus diesem Grund wiederholen wir an dieser Stelle einige wichtige Aspekte der Funktionen.

Funktions-Namen sind Referenzen auf Funktionen und wir können mehrere Funktions-Namen für ein und dieselbe Funktion vergeben:
>>> def succ(x):
...     return x + 1
... 
>>> successor = succ
>>> successor(10)
11
>>> succ(10)
11
Wir haben jetzt also zwei Namen (succ und successor) für ein und dieselbe Funktion.

Der nächste wichtige Punkt ist, dass wir "succ" oder "successor" löschen können ohne die eigentliche Funktion zu entfernen.
>>> del succ
>>> successor(10)
11

Funktionen in Funktionen

Das Konzept der Funktions-Definition innerhalb einer Funktion ist für C/C++-Programmierer völlig neu:
def f():
    
    def g():
        print("Hi, it's me 'g'")
        print("Thanks for calling me")
        
    print("This is the function 'f'")
    print("I am calling 'g' now:")
    g()

f()
Wenn wir den Code ausführen, erhalten wir die folgende Ausgabe:
This is the function 'f'
I am calling 'g' now:
Hi, it's me 'g'
Thanks for calling me
Ein weiteres Beispiel für "richtige" return-Anweisungen in den Funktionen:
def temperature(t):
    def celsius2fahrenheit(x):
        return 9 * x / 5

    result = "It's " + str(celsius2fahrenheit(t)) + " degrees!" 
    return result

print(temperature(20))
Die Ausgabe:
It's 36.0 degrees!

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("Hi, it's me 'g'")
    print("Thanks for calling me")
    
def f(func):
    print("Hi, it's me 'f'")
    print("I will call 'func' now")
    func()
          
f(g)
Die Ausgabe sieht wie folgt aus:
Hi, it's me 'f'
I will call 'func' now
Hi, it's me 'g'
Thanks for calling me
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("Hi, it's me 'g'")
    print("Thanks for calling me")
    
def f(func):
    print("Hi, it's me 'f'")
    print("I will call 'func' now")
    func()
    print("func's real name is " + func.__name__) 
          
f(g)
Einmal mehr zeigt die Ausgabe, was hier passiert:
Hi, it's me 'f'
I will call 'func' now
Hi, it's me 'g'
Thanks for calling me
func's real name is g
Noch ein Beispiel:
import math

def foo(func):
    print("The function " + func.__name__ + " was passed to foo")
    res = 0
    for x in [1, 2, 2.5]:
        res += func(x)
    return res

print(foo(math.sin))
print(foo(math.cos))
Die folgende Ausgabe liefert dieses Beispiel:
The function sin was passed to foo
2.3492405557375347
The function cos was passed to foo
-0.6769881462259364

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 vorige Beispiel liefert folgende Ausgabe:
5
7

Ein einfacher Dekorateur

Jetzt haben wir alles um unseren ersten einfachen Dekorateur zu bauen:
def our_decorator(func):
    def function_wrapper(x):
        print("Before calling " + func.__name__)
        func(x)
        print("After calling " + func.__name__)
    return function_wrapper

def foo(x):
    print("Hi, foo has been called with " + str(x))

print("We call foo before decoration:")
foo("Hi")
    
print("We now decorate foo with f:")
foo = our_decorator(foo)

print("We call foo after decoration:")
foo(42)
Wenn wir uns die folgende Ausgabe anschauen, dann sehen wir was passiert. Nach der Dekoration "foo = our_decorator(foo)" ist foo eine Referenz auf die Funktion "function_wrapper". foo wird innerhalb von "function_wrapper" aufgerufen, aber vorher und nacher wird noch etwas code ausgeführt. In diesem Beispiel zwei Print-Funktionen.
We call foo before decoration:
Hi, foo has been called with Hi
We now decorate foo with f:
We call foo after decoration:
Before calling foo
Hi, foo has been called with 42
After calling foo

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 = our_decorator(foo) ist einfach zu verstehen und einprägsam. Deshalb haben wir sie verwendet. Allerdings gibt es bei diesem Ansatz ein Design-Problem. "foo" gibt es in dem Programm in zwei Versionen...sowohl vor der Dekoration, als auch nach der Dekoration.

Die richtige Schreibweise der Dekoration zeigen wir ihnen jetzt. Die Dekoration passiert in der Zeile über dem Funktions-Kopf. Das Zeichen "@" gefolgt von dem Funktions-Namen des Dekorateurs.

Wir ersetzen diese Anweisung
foo = our_decorator(foo)
durch
@our_decorator
Diese Zeile muss direkt über der zu dekorierenden Funktion platziert werden. Das komplette Beispiel sieht folgendermaßen aus:
def our_decorator(func):
    def function_wrapper(x):
        print("Before calling " + func.__name__)
        func(x)
        print("After calling " + func.__name__)
    return function_wrapper

@our_decorator
def foo(x):
    print("Hi, foo has been called with " + str(x))

foo("Hi")
Wir können jede andere Funktion dekorieren, welche einen Parameter mit unserem Dekorateur "our_decorator" entgegennimmt. Im Folgenden werden wir dies demonstrieren. Wir haben den Wrapper etwas verändert, so das wir die Funktions-Aufrufe sehen können:
def our_decorator(func):
    def function_wrapper(x):
        print("Before calling " + func.__name__)
        res = func(x)
        print(res)
        print("After calling " + func.__name__)
    return function_wrapper

@our_decorator
def succ(n):
    return n + 1

succ(10)
Die Ausgabe des Beispiels:
Before calling succ
11
After calling succ
Es ist also möglich weitere Dritt-Anbieter-Funktionen zu dekorieren, z.B. welche die wir aus einem Modul importieren. Im folgenden Fall können wir die Python-Syntax mit dem "@"-Zeichen nicht verwenden.
from math import sin, cos

def our_decorator(func):
    def function_wrapper(x):
        print("Before calling " + func.__name__)
        res = func(x)
        print(res)
        print("After calling " + func.__name__)
    return function_wrapper

sin = our_decorator(sin)
cos = our_decorator(cos)

for f in [sin, cos]:
    f(3.1415)
Die Ausgabe:
Before calling sin
9.265358966049026e-05
After calling sin
Before calling cos
-0.9999999957076562
After calling cos
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 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

for f in [random, randint, choice]:
    f = our_decorator(f)

random = our_decorator(random) 
randint = our_decorator(randint)
choice = our_decorator(choice)
random()
randint(3, 8)
choice([4, 5, 6])
Die Ausgabe sieht aus wie erwartet:
Before calling random
0.16420183945821654
After calling random
Before calling randint
8
After calling randint
Before calling choice
5
After calling choice

Anwendungsfälle für Dekorateure

Überprüfung von Argumenten durch Dekorateure

In unserem Kapitel über rekursive Funktionen hatten wir die Fakultätsfuntion 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_natural_number(f):
    def helper(x):
        if type(x) == int and x > 0:
            return f(x)
        else:
            raise Exception("Argument is not an integer")
    return helper
    
@argument_test_natural_number
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)

for i in range(1,10):
	print(i, factorial(i))

print(factorial(-1))

Funktionsaufrufe mit Dekorateuren zählen

Im folgenden Beispiel zeigen wir, wie wir unter Benutzung eines Dekorateurs elegant die Aufrufe einer Funktion bzw. einer Methode zählen können. Wir können diesen Dekorateuer aber nur für Funktionen mit einem Parameter benutzen:
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):
    print(succ(i))
    
print(succ.calls)
Die Ausgabe sieht wie folgt aus:
0
10
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)
Die Ausgabe sieht dann so aus:
0
10
3

Dekorateure mit Parametern

Wir definieren im Folgenden zwei Dekorateure:
def evening_greeting(func):
    def function_wrapper(x):
        print("Good evening, " + func.__name__ + " returns:")
        func(x)
    return function_wrapper

def morning_greeting(func):
    def function_wrapper(x):
        print("Good morning, " + func.__name__ + " returns:")
        func(x)
    return function_wrapper

@evening_greeting
def foo(x):
    print(42)

foo("Hi")
Diese beiden Dekorateure sind beinahe identisch, ausser 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 eine weitere Funktion um unseren Dekorateur wickeln. Jetzt können wir 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")
Die Ausgabe:
καλημερα, foo returns:
42
Wenn wir nicht die "@"-Schreibweise verwenden, sondern die Funktions-Aufrufe, dann sind zwei Schritte nötig:
def greeting(expr):
    def greeting_decorator(func):
        def function_wrapper(x):
            print(expr + ", " + func.__name__ + " returns:")
            func(x)
        return function_wrapper
    return greeting_decorator


def foo(x):
    print(42)

greeting2 = greeting("καλημερα")
foo = greeting2(foo)
foo("Hi")
Das Ergebnis ist identisch:
καλημερα, foo returns:
42
Natürlich ist die zusätzliche Definition von "greeting2" nicht nötig. Wir können die Rückgabe aus "greeting("καλημερα")" direkt an "foo" übergeben.
foo = greeting("καλημερα")(foo)

Nutzen von Wraps aus functools

Die Art und Weise wie wir bisher Dekorateure definiert haben, hat nicht berücksichtigt, dass die Attribute der originalen Funktionen nach der Dekoration verloren gehen.

Der folgende Dekorateur wird in der Datei greeting_decorator.py gespeichert:
def greeting(func):
    def function_wrapper(x):
        """ function_wrapper of greeting """
        print("Hi, " + func.__name__ + " returns:")
        return func(x)
    return function_wrapper
Im folgenden Programm rufen wir ihn auf:
from greeting_decorator import greeting

@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__)
Wir erhalten "ungewollte" Ergebnisse:
Hi, f returns:
function name: function_wrapper
docstring:  function_wrapper of greeting 
module name: greeting_decorator
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 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
In unserem Haupt-Programm müssen wir nichts weiter tun, als die import-Anweisung anzupassen. Wir schreiben jetzt:
from greeting_decorator_manually import greeting
Jetzt erhalten wir die richtigen Ergebnisse:
Hi, f returns:
function name: f
docstring:  just some silly function 
module name: __main__
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 greeting(func):
    @wraps(func)
    def function_wrapper(x):
        """ function_wrapper of greeting """
        print("Hi, " + func.__name__ + " returns:")
        return func(x)
    return function_wrapper

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("An instance of A was initialized")
    
    def __call__(self, *args, **kwargs):
        print("Arguments are:", args, kwargs)
              
x = A()
print("now calling the instance:")
x(3, 4, x=11, y=10)
print("Let's call it again:")
x(3, 4, x=11, y=10)
Wir erhalten folgende Ausgabe:
An instance of A was initialized
now calling the instance:
Arguments are: (3, 4) {'x': 11, 'y': 10}
Let's call it again:
Arguments are: (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=", ")
Die Ausgabe:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 

Eine Klasse als Dekorateur benutzen

Wir werden folgenden Dekoratuer als Klasse umschreiben:
def decorator1(f):
    def helper():
        print("Decorating", f.__name__)
        f()
    return helper

@decorator1
def foo():
    print("inside foo()")

foo()
Der folgende Dekoratuer, der als Klasse implementiert ist, erledigt die gleiche Aufgabe:
class decorator2(object):
    
    def __init__(self, f):
        self.f = f
        
    def __call__(self):
        print("Decorating", self.f.__name__)
        self.f()

@decorator2
def foo():
    print("inside foo()")

foo()
Beide Versionen liefern das gleiche Ergebnis:
Decorating foo
inside foo()