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:

In [1]:
def nachfg(x):
     return x + 1

nachfolger = nachfg
nachfolger(10)
Out[1]:
11
In [2]:
nachfg(10)
Out[2]:
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.

In [3]:
del nachfg
nachfolger(10)
Out[3]:
11

Funktionen in Funktionen

Das Konzept von verschachtelten Funktionen, also Funktionsdefinitionen innerhalb einer Funktion, ist für C/C++-Programmierer völlig neu:

In [4]:
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:

In [5]:
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!

Im folgenden Beispiel geht es um die Funktion Fakultätsfunktion "factorial", die wir zuvor wie folgt definiert haben:

In [ ]:
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:

In [1]:
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:

In [2]:
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:

In [ ]:
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:

In [ ]:
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:

In [39]:
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:

In [40]:
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:

In [43]:
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.

In [44]:
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

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:

In [4]:
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
Günaydın Gülay!

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:

In [5]:
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:

In [6]:
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))
-2 14.5 -16.6
-1 5.5 -20.6
0 0.5 -20.0
1 -0.5 -14.8

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} $$
In [7]:
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))
-2 4 0 100 -7 591
-1 4 2 7 -2 -35
0 4 4 2 1 9
1 4 6 13 2 99

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:

In [8]:
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))
-2 4 0 100 -7 591
-1 4 2 7 -2 -35
0 4 4 2 1 9
1 4 6 13 2 99

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:

In [6]:
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:

In [7]:
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:

In [8]:
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.

In [9]:
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.

In [15]:
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.

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.

In [9]:
from math import sin, cos, pi
help(sin)
Help on built-in function sin in module math:

sin(x, /)
    Return the sine of x (measured in radians).

In [10]:
angle = 45   # Gradmaß
x = angle * pi / 180   # Wandlung in Bogenmaß
sin(x)
Out[10]:
0.7071067811865475

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.

In [12]:
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:

In [13]:
degrees = [40, 45, 70, 90]
for degree in degrees:
    print(sin(degree, mode='degrees'), cos(degree, mode='degrees'))
0.6427876096865393 0.766044443118978
0.7071067811865475 0.7071067811865476
0.9396926207859083 0.3420201433256688
1.0 6.123233995736766e-17

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:

In [14]:
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])
Before calling random
0.8915409386615734
After calling random
Before calling randint
3
After calling randint
Before calling choice
6
After calling choice

Benutzung mehrerer Dekorateure

Man kann auch mehrere Dekorateure auf eine Funkion anwenden, wie wir im folgenden Python-Programm sehen:

In [15]:
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
deco1 has been called
deco2 has been called
deco3 has been called

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:

In [16]:
foobar(42)
helper of deco3 has been called!
42
helper of deco2 has been called!
42
helper of deco1 has been called!
42
Out[16]:
48

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:

In [18]:
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!")
1 True
2 True
3 True
4 False
5 True
6 False
7 True
8 False
9 False
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.

In [15]:
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)
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.

In [19]:
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)
0
10
3

Dekorateure mit Parametern

Wir definieren im Folgenden zwei Dekorateure:

In [105]:
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")
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:

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

Wenn wir nicht die "@"-Schreibweise verwenden, sondern die Funktions-Aufrufe, dann können wir dies wie folgt tun:

In [2]:
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")
καλημερα, 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.

In [12]:
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:

In [20]:
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:

In [22]:
@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__) 
Hi, f returns:
function name: function_wrapper
docstring:  function_wrapper of greeting 
module name: __main__

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:

In [23]:
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 [24]:
@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__) 
Hi, f returns:
function name: f
docstring:  just some silly function 
module name: __main__

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:

In [26]:
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
In [27]:
@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__) 
Hi, f returns:
function name: f
docstring:  just some silly function 
module name: __main__

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.

In [1]:
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:

In [2]:
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:

In [3]:
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:

In [4]:
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.