Decorators sind Funktionen, die eine andere Funktion als Argument entgegennehmen, um das Verhalten der übergebenen Funktion zu erweitern oder zu ersetzen.
Wenn Funktion A ein Decorator ist und Funktion B ihr Argument ist, dann ändert, erweitert oder ersetzt Funktion A das Verhalten von Funktion B, ohne den Code von Funktion B zu ändern.
Wir sagen, dass die Decorator-Funktion A die Funktion B umschließt.
Während wir davon sprechen, Verhalten zu „ändern“, wird die umschlossene Funktion nicht wirklich geändert.
Verhalten wird entweder um die umschlossene Funktion herum hinzugefügt (und um das, was sie zurückgibt), oder das Verhalten der umschlossenen Funktion wird durch ein anderes Verhalten ersetzt.
Eine Funktion höherer Ordnung ist eine Funktion, die eine oder mehrere Funktionen als Argumente akzeptiert und/oder eine oder mehrere Funktionen zurückgibt. Eine Funktion, die als Argument verwendet oder von einer anderen Funktion zurückgegeben wird, ist eine First-Class-Funktion. Eine Python-Funktion ist als aufrufbares Objekt eine First-Class-Funktion, die in einer Variablen gespeichert oder als Argument verwendet werden kann, ganz ähnlich wie jeder andere Wert oder jedes andere Objekt. Funktionen höherer Ordnung und First-Class-Funktionen arbeiten zusammen, um Decorators zu ermöglichen.
Das Symbol @ wird dem Namen der Decorator-Funktion vorangestellt und direkt über die zu dekorierende Funktion gesetzt, etwa so:
@decorator
def decorated_function():
pass
Manche Decorators akzeptieren Argumente:
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
Wenn ein Decorator Standardargumente definiert hat, musst du im Aufruf @decorator() runde Klammern verwenden, damit der Decorator funktioniert, genau wie beim Aufruf jeder anderen Funktion:
@decorator_with_default_arg()
def decorated_function3():
pass
Wenn ein Decorator positionale Argumente entgegennimmt und du die Argumente nicht angibst, führt das zu einem Fehler, der ungefähr so aussieht:
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
Die Syntax @decorator ist syntaktischer Zucker oder eine Kurzform dafür, die dekorierende Funktion aufzurufen und ihr die dekorierte Funktion als Argument zu übergeben.
Es folgen Beispiele für alternative Arten, einen Decorator aufzurufen:
def function():
pass
function = decorator(function)
def function2():
pass
function2 = decorator_with_arg(name="Bob")(function2)
def function3():
pass
function3 = decorator_with_default_arg()(function3)
Die meisten Decorators sollen das Verhalten einer anderen Funktion erweitern oder ersetzen, aber manche Decorators tun nichts anderes, als die Funktionen zurückzugeben, die sie umschließen.
Decorators sind Funktionen, die mindestens ein Argument entgegennehmen: die Funktion, die sie umschließen. Normalerweise geben sie entweder die umschlossene Funktion oder das Ergebnis eines Ausdrucks zurück, der die umschlossene Funktion verwendet.
Ein einfacher Decorator, also einer, der einfach seine umschlossene Funktion zurückgibt, kann so geschrieben werden:
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
Ein Decorator kann auch nur Nebenwirkungen hinzufügen, etwa zusätzliche Informationen, die für die Protokollierung verwendet werden:
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
Ein Decorator gibt nicht sich selbst zurück. Er kann seine Funktionsargumente, eine andere Funktion oder einen oder mehrere Werte zurückgeben, die den Rückgabewert der übergebenen oder dekorierten Funktion ersetzen. Wenn ein Decorator eine andere Funktion zurückgibt, gibt er normalerweise eine innere Funktion zurück.
Eine Funktion kann innerhalb einer Funktion definiert werden. Eine solche verschachtelte Funktion heißt innere Funktion. Ein Decorator kann eine innere Funktion verwenden, um sein Funktionsargument zu umschließen. Der Decorator gibt dann seine innere Funktion zurück. Die innere Funktion kann dann das ursprüngliche Funktionsargument zurückgeben.
Es folgt ein Beispiel für einen Decorator, der zur Validierung verwendet wird:
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
@my_validator
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!
In der ersten Zeile steht die Definition für das Argument func des Decorators.
In der nächsten Zeile steht die Definition für die innere Funktion des Decorators, die das Argument func umschließt.
Da die innere Funktion das Argument func des Decorators umschließt, wird ihr dasselbe Argument übergeben, das an func übergeben wird.
Beachte, dass der Wrapper nicht denselben Namen für das Argument verwenden muss, der in func definiert wurde.
Die ursprüngliche Funktion verwendet planet und der Decorator verwendet world, und der Decorator funktioniert trotzdem.
Die innere Funktion gibt entweder func zurück, oder, wenn world == "Pluto" gilt, gibt sie aus, dass Pluto kein Planet ist.
Sie könnte stattdessen so programmiert sein, dass sie einen ValueError auslöst.
Die innere Funktion umschließt also func und gibt entweder func zurück oder tut etwas, das ersetzt, was func tun würde.
Der Decorator gibt seine innere Funktion zurück.
Die innere Funktion kann die ursprüngliche, übergebene Funktion zurückgeben oder auch nicht.
Je nachdem, welcher Code in der Wrapper-Funktion oder der inneren Funktion bedingt ausgeführt wird, kann func zurückgegeben, ein Fehler ausgelöst oder ein Wert vom Rückgabetyp von func zurückgegeben werden.
Decorators können für Funktionen geschrieben werden, die eine beliebige Anzahl von Argumenten entgegennehmen, indem man die Syntax *args und **kwargs verwendet.
Es folgt ein Beispiel für einen Decorator für eine Funktion, die eine beliebige Anzahl von Argumenten entgegennimmt:
def double(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 18
print(add(2, 3, 4, 5, 6))
#-> 40
Damit lässt sich der Rückgabewert der als Argument übergebenen Funktion verdoppeln. Wenn wir den Rückgabewert verdreifachen, vervierfachen usw. möchten, können wir dem Decorator selbst einen Parameter hinzufügen, wie wir im nächsten Abschnitt zeigen.
Es folgt ein Beispiel für einen Decorator, der so konfiguriert werden kann, dass er den Rückgabewert der dekorierten Funktion mit einer beliebigen Zahl multipliziert:
def multi(factor=1):
if factor == 0:
raise ValueError("factor must not be 0")
def outer_wrapper(func):
def inner_wrapper(*args, **kwargs):
return func(*args, **kwargs) * factor
return inner_wrapper
return outer_wrapper
@multi(factor=3)
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 27
print(add(2, 3, 4, 5, 6))
#-> 60
Die ersten Zeilen stellen sicher, dass factor nicht 0 ist.
Dann wird der äußere Wrapper definiert.
Dieser hat dieselbe Signatur, die wir für einen Decorator ohne Parameter erwarten.
Der äußere Wrapper hat eine innere Funktion mit derselben Signatur wie die ursprüngliche Funktion.
Der innere Wrapper erledigt die Arbeit, den Rückgabewert der ursprünglichen Funktion mit dem an den Decorator übergebenen Argument zu multiplizieren.
Der äußere Wrapper gibt den inneren Wrapper zurück, und der Decorator gibt den äußeren Wrapper zurück.
Es folgt ein Beispiel für einen parametrisierten Decorator, der steuert, ob er das an die ursprüngliche Funktion übergebene Argument validiert:
def check_for_pluto(check=True):
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if check and world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
return my_validator
@check_for_pluto(check=False)
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!
So lässt sich leicht umschalten, ob auf Pluto geprüft wird oder nicht, und das, ohne my_func ändern zu müssen.