Python Decorators Tutorial Teil 28

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.

Python Logo (CC-BY-SA The people from the Tango! project / Wikipedia)
Python Logo (CC-BY-SA The people from the Tango! project / Wikipedia)
1Python Programmierkurs
2Python: Methoden
3Kontrollstrukturen
4Strings in Python
5Container
6Objekte in Python
7Module
8Exceptions in Python
9Typkonvertierung
10Python und Dateien
11Datum und Zeit mit Python verarbeiten
12Multithreading
13Netzwerk in Python
14Logging in Python
15GPIO
16Automatische Tests
17Datenbanken mit Python
18Python: Generatoren und List Comprehension
19Python: Webseiten mit Flask
20Python virtuelle Umgebungen
21Interrupts & Signale
22NumPy
23Matplotlib
24match
25Reguläre Ausdrücke
26lambda Funktionen
27logging.config
28Decorators
29GUI

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.

Olli Graf - raspithek.de
nested_functionCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
Ausgabe von nested_function.py

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.

func_param.py gibt zunächst 10 und beim zweiten Aufruf 24 aus.
Olli Graf - raspithek.de
func_paramCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
func_param.py gibt zunächst 10 und beim zweiten Aufruf 24 aus.

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.

Ausgabe von timer.py mit den Laufzeiten der beiden Testfunktionen
Olli Graf - raspithek.de
timerCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
Ausgabe von timer.py mit den Laufzeiten der beiden Testfunktionen

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.

Ausgabe von classdecor.py
Olli Graf - raspithek.de
classdecorCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
Ausgabe von classdecor.py

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:

Der Decorator zählt 2692537 Aufrufe der fib() Funktion.
Olli Graf - raspithek.de
counterCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
Der Decorator zählt 2692537 Aufrufe der fib() Funktion.

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.

Olli Graf - raspithek.de
reverseCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . loading=
Der Decorator sorgt dafür, dass der Text umgekehrt ausgegeben wird.

Fazit

Richtig angewendet können Dekoratoren dir eine Menge Arbeit ersparen.

Natürlich findest du alle Codebeispiele im Repo.

Schreibe einen Kommentar

Insert math as
Block
Inline
Additional settings
Formula color
Text color
#333333
Type math using LaTeX
Preview
\({}\)
Nothing to preview
Insert
Creative Commons License
Except where otherwise noted, the content on this site is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Olli Graf - raspithek.de
WordPress Cookie Hinweis von Real Cookie Banner