Dekorateure

Einführung

decorators

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)
Führt man obigen Code aus, erhält man folgendes Ergebnis:
11
nachfg(10)
Führt man obigen Code aus, erhält man Folgendes:
11

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)
Führt man obigen Code aus, erhält man folgendes Ergebnis:
11

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()
Dies ist die Funktion 'f'
'f' ruft nun 'g' auf!
Hallo, ich bin es, 'g'
Danke für's Aufrufen

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))
Es ist 68.0 grad!

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)
Hallo, ich bin es
Ich werde jetzt 'func' aufrufen
Hallo, ich bin es, 'g'
Danke für's Aufrufen

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)
Hallo, ich bin es​, 'f'
Ich werde jetzt 'func' aufrufen
Hallo, ich bin es​, 'g'
Danke für's Aufrufen
func's echter Name ist 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))
Die Funktion sin wurde an foo übergeben
2.3492405557375347
Die Funktion cos wurde an foo übergeben
-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))
5
7

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)
Wir rufen foo vor der Dekoration auf:
Hallo, foo wurde mit aufgerufen Hi
Wir dekorieren jetzt foo mit f:
Wir rufen foo nach der Dekoration auf:
Vor dem Aufruf foo
Hallo, foo wurde mit aufgerufen 42
Nach dem Aufruf foo

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_dekorateur
Diese 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 ")
Vor dem Aufruf foo
Hallo, foo wurde mit aufgerufen Hallo 
Nach dem Aufruf foo

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)
Vor dem Aufruf nachfg
11
Nach dem Aufruf nachfg

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)
Vor dem Aufruf sin
9.265358966049026e-05
Nach dem Aufruf sin
Vor dem Aufruf cos
-0.9999999957076562
Nach dem Aufruf 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 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])
Vor dem Aufruf random
0.5054712105583256
Nach dem Aufruf random
Vor dem Aufruf randint
5
Nach dem Aufruf randint
Vor dem Aufruf choice
4
Nach dem Aufruf choice

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))
1 1
2 2
3 6
4 24
5 120
6 720
7 5040
8 40320
9 362880
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-97-23965cb6b3f1> in <module>
     17         print(i, Fakultät(i))
     18 
---> 19 print(Fakultät(-1))
<ipython-input-97-23965cb6b3f1> in Helper(x)
      4             return f(x)
      5         else:
----> 6             raise Exception("Argument ist keine ganze Zahl")
      7     return Helper
      8 
Exception: Argument ist keine ganze Zahl

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)
0
1
2
3
4
5
6
7
8
9
10
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 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)
0
10
3

Dekorateure mit Parametern

Wir definieren im Folgenden zwei Dekorateure:

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")
Guten Abend, gibt foo zurück:
42

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")
καλημερα, foo gibt zurück:
42

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")
καλημερα, foo gibt zurück
42

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__)
Hi, f gibt zurück:
Funktionsname: Wrapper_Funktion
docstring:  Wrapper Funktion der Begrüßung 
Modulname: __main__

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)
Hi, f gibt zurück:
Der obige Code führt zu folgendem Ergebnis:
43

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)
Hallo , f gibt zurück:
Der obige Python-Code liefert folgendes Ergebnis:
43

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)
Eine Instanz von A wurde initialisiert
Rufen Sie jetzt die Instanz auf:
Argumente sind: (3, 4) {'x': 11, 'y': 10}
Rufen wir es noch einmal auf:
Argumente sind: (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=", ")
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 

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()
Dekorieren foo
innen 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()
Dekorieren foo
innen foo()

Beide Versionen liefern das gleiche Ergebnis.