Ein Decorator ist ein Entwurfsmuster das eine Methode oder Klasse mit zusätzlicher Funktion anreichern kann.
Dies kann sehr nützlich sein, da du wiederkehrende Aufgaben in einen Decorator auslagern kannst und das Rad nicht in jeder Methode neu erfinden musst.

Decorator
Was ein Decorator ist und wie du einen selber in Python implementierst, zeige ich dir am Besten an einem einfach Beispiel.
Es kommt häufig vor, dass du die Laufzeit einer Methode messen willst. Das Vorgehen ist immer das gleiche. Zum Beginn der Methode holst du dir die aktuelle Zeit und am Ende erneut. Die Differenz zwischen Endzeit und Startzeit ergibt dann die Laufzeit. Dies machst du entweder in der Methode selbst oder beim Aufruf derselben. Das ist nichts Neues, ich habe in einem früheren Kapitel schon verwendet. Wenn du mehrere Methoden messen willst, ist es lästig und unübersichtlich, immer wieder den gleichen Code um die Methoden drum herum zu bauen. Ein passender Decorator kann dir das Leben sehr viel einfacher machen.
innere Funktionen
Zunächst müssen wir uns kurz mit eingebetteten oder inneren Funktionen beschäftigen.
Eine innere Funktion wird innerhalb einer anderen Funktion definiert und ist nur in der letzteren sichtbar.
#Datei: nested_function.py
def print_message(message):
print('Umgebende Funktion')
def inner_function():
print('Eingebettete Funktion')
print(message)
inner_function()
print_message("Irgendein Text")
Du siehst hier die äußere Funktion print_message()
. In dieser wird die Funktion inner_function()
definiert. print _message()
gibt zunächst den Text „Umgebende Funktion“ aus und ruft die inner_function()
nach deren Definition auf.
Mal sehen, was passiert, wenn wir das Programm starten.

Beim Einstieg in print_message() wird zunächst der String „Umgebende Funktion“ ausgegeben. Nach der Deklaration von inner_function() wird diese innerhalb von print_message()
aufgerufen. Diese gibt dann „Eingebettete Funktion“ aus. Erst danach wird der übergebende Text in message
„Irgendein Text“ gedruckt. inner_function()
kann nur innerhalb von print_message()
aufgerufen werden. Alles andere wird einen NameError auslösen.
Funktionen als Parameter
Ein weiterer wichtiger Mechanismus für einen Decorator ist die Übergabe einer Funktion als Parameter:
# Datei: func_param.py
def add(x, y):
return x + y
def mul(x,y):
return x * y
def calculate(func, x, y):
return func(x, y)
result = calculate(add, 4, 6) # Aufruf von calculate mit add Funktion als Parameter
print(result) # Ausgabe ist 10
result = calculate(mul, 4, 6) # Aufruf von calculate mit mul Funktion als Parameter
print(result) # Ausgabe ist 24
Beim Aufruf von calculate()
wird add
als Parameter übergeben. Dies ist nicht die Funktion selber sondern eher ein Zeiger auf die Funktion, die dann innerhalb von calculate()
aufgerufen werden kann. Dies wird mit dem zweiten Aufruf vermutlich etwas klarer. Wir rufen dieselbe Funktion auf, ändern aber die übergebene Funktion zu mul()
und erhalten dann als Ergebnis die Multiplikation der beiden Werte.

Der erste Decorator
Diese Konstrukte sind für einen eigenen Decorator unabdingbar. Ich zeige dir jetzt, wie du mit einem Decorator sehr einfach die Laufzeit einer Funktion messen kannst.
Ich zeig die einfach mal, wie das Ergebnis aussieht.
def timer(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args,**kwargs)
end_time = time.time()
print(f'Methode {func.__name__} - Laufzeit {end_time - start_time:.4f}s')
return result
return wrapper
Unser Decorator ist nichts anderes als eine Funktion, die die zu dekorierende Funktion in func
übergeben bekommt. Darin wird die innere Funktion wrapper()
definiert, die dann die dekorierte Funktion mit allen Parametern aufruft. Vor und nach dem Aufruf von func() werden die oben angesprochenen Zeitstempel ermittelt und zum Schluss die Laufzeit ausgegeben. wrapper()
gibt als Ergebnis den Rückgabewert von func()
zurück, timer
gibt wrapper
zurück.
Den Decorator verwenden wir jetzt in einem kleinen Programm, für das ich die Fibonacci-Funktion wieder ausgegraben habe. 👀
# Datei: timer.py
from fib import fib
import time
import sys
def timer(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args,**kwargs)
end_time = time.time()
print(f'Methode {func.__name__} - Laufzeit {end_time - start_time:.4f}s')
return result
return wrapper
@timer
def summe(n):
return f"Summe: {sum(range(n))}"
@timer
def calc_fib(n):
return fib(n)
print(summe(1000000))
print(calc_fib(int(sys.argv[1])))
#Datei: fib.py
import functools
import sys
#@functools.cache
def fib(n):
if n in [0,1]:
return n
else:
return fib(n-1) + fib(n-2)
Du siehst, ich habe mich hier gegen die Generatorversion von fib()
entschieden. Den Grund erläutere ich dir gleich.
Zunächst mal erkennst du, dass die Funktionen summe()
und calc_fib()
mit @timer
dekoriert sind. Mehr braucht es nicht, um unseren Decorator zu verwenden. Die Syntax ist immer die gleiche: Einfach ein @ vor den Namen der Decorator Funktion stellen. Python kümmert sich automatisch darum, dass die dekorierte Funktion als Parameter in den Decorator übermittelt wird.
Wenn wir das Programm starten, sehen wir, wie der Decorator für beiden Funktionen die Laufzeit ausgibt.

Du erkennst hier den großen Vorteil eines Decorator, ohne viel Aufwand kannst du jetzt eine Laufzeitmessung bei jeder beliebiger Funktion durchführen ohne die Funktion selber zu verändern.
Klassen dekorieren
Einen Decorator kannst du auch für eine Klasse definieren. In diesem Beispiel definieren wir eine universelle __repr__() Methode, die per Decorator an die Klasse angefügt wird.
# Datei: classdecor.py
def addrepr(cls):
# Universelle __repr__ Methode
def __repr__(self):
return f"{cls.__name__}({self.__dict__})"
print('addrepr Decorator')
print('__repr__ wird hinzugefügt.')
cls.__repr__ = __repr__
return cls
@addrepr
class Fahrzeug():
def __init__(self,farbe,typ):
self.typ = typ
self.farbe = farbe
f1 = Fahrzeug('grau','VW')
f2 = Fahrzeug('rot','Ferrari')
print(f'{f1}')
print(f'{f2}')
Der Decorator addrepr
besitzt eine universelle __repr__
Methode, die neben dem Klassennamen auch das Dictionary aller Attribute einer Instanz der Klasse ausgibt. Beachte: Da die Klasse kein self besitzt, erhält der Decorator in cls
die Klasse als Parameter.

builtin Decorators
Python liefert einige Decorator bereits mit. Ich möchte sie hier nicht alle aufzählen. Der interessanteste ist für mich der functools.cache Decorator, er kann das Ergebnis einer bereits durchlaufenden Funktion zwischenspeichern und es bei einem erneuten Aufruf mit den selben Parametern zurückgeben, ohne die Funktion nochmal durchlaufen zu müssen. Gerade bei der fib() Funktion mit ihrer doppelten Rekursion ist der sehr praktisch.
#Datei: fib.py
import functools
import sys
@functools.cache
def fib(n):
if n in [0,1]:
return n
else:
return fib(n-1) + fib(n-2)
Wenn die Funktion derart dekoriert ist, benötigt sie nur noch 0,0001s statt 0,6453s wie ohne.
Ein anderer manchmal hilfreicher Decorator ist @staticmethod
, der eine Methode einer Klasse zu einer statischen Methode macht. Statische Methoden benötigen keine Instanz, um aufgerufen zu werden. Python hat kein static Schlüsselwort, das es in anderen Sprachen gibt, um statische (oder Klassenmethoden) zu deklarieren.
# Datei static.py
class Math():
@staticmethod
def add(x,y):
return x+y
@staticmethod
def sub(x,y):
return x-y
@staticmethod
def mul(x,y):
return x*y
print(f'Add: {Math.add(3,2)}')
print(f'Sub: {Math.sub(3,2)}')
print(f'Mul: {Math.mul(3,2)}')
Du siehst, die statischen Methoden sind nur über den Klassennamen zu referenzieren und sie besitzen kein self Attribut (logisch ohne Instanz).
Funktionsaufrufe zählen
Manchmal ist es hilfreich zu wissen, wie oft eine Funktion aufgerufen wird. Das demonstriert der Counter Decorator.
#Datei: counter.py
def counter(func):
func.count = 0
def wrapper(*args, **kwargs):
func.count = func.count +1
print(f'{func.__name__} wurde {func.count}-mal aufgerufen.')
result = func(*args,**kwargs)
return result
wrapper.count = 0
return wrapper
Neu ist hier, dass in der dekorierten Funktion die Variable count
angelegt wird, die im wrapper()
bei jedem Aufruf hochgezählt wird.
Ich entferne den functools.cache Decorator wieder. Und lasse die Aufrufe zählen:

Durch die zweifache Rekursion kommen eine Menge Aufrufe zustande. Das print() im Decorator sorgt für eine signifikant höhere Laufzeit. Das Wissen um die Aufrufhäufigkeit ist nützlich, das Laufzeitverhalten eines Programms zu optimieren.
schädliches Dekorieren
Es gibt Fälle in denen das Dekorieren, eher schadet als zu helfen. So solltest du das, was die Aufgabe der Funktion ist, nicht in einen Decorator auslagern! Das erschwert nur die Fehlersuche.
Außerdem solltest du aufpassen, dass ein importierter Decorator aus einer sauberen Quelle stammt, denn ein Decorator kann u.U. das Ergebnis deiner Funktion massiv zu seinem Gunsten verändern. Auch dazu habe ich ein kleines Beispiel:
def reverse_decorator(func):
def wrapper(text):
make_reverse = "".join(reversed(text))
return func(make_reverse)
return wrapper
@reverse_decorator
def format_message(text):
return f'Text: {text}'
print(format_message('Hallo'))
Die Funktion format_message soll den übergebenen text
nur aufbereiten, aber der Decorator sorgt für ein nicht zufriedenstellendes Ergebnis.

Fazit
Richtig angewendet können Dekoratoren dir eine Menge Arbeit ersparen.
Natürlich findest du alle Codebeispiele im Repo.