Mit einem Schwerpunkt auf dem Python functools.wraps Decorator bietet dieser Beitrag einen Einblick in die Welt der Python-Decorator und die Bedeutung der Metadatenübertragung.
Decorator in Python sind großartig! Aufgrund der zugrunde liegenden Mechanik der Sprache kann jedoch das Einhüllen eines Objekts über ein anderes dazu führen, dass wertvolle Metadaten des umschlossenen Objekts verloren gehen. Aus diesem Grund ist es entscheidend, den wraps Decorator aus dem functools Modul der Python-Standardbibliothek zu verwenden, wenn man eigene Python-Decorator entwickelt.
Was macht functools.wraps?
functools.wraps, eine benutzerfreundliche Schnittstelle für functools.update_wrapper, ist ein Decorator, der automatisch die Schlüsselmetadaten von einem aufrufbaren Objekt (in der Regel eine Funktion oder Klasse) auf seinen Wrapper überträgt. Typischerweise ist dieser Wrapper eine weitere Funktion, kann aber auch ein beliebiges aufrufbares Objekt sein.
Neben dem „wrapped“-Parameter, der das aufrufbare Objekt akzeptiert, das vom Wrapper umschlossen wird, gibt es zwei weitere Argumente, mit denen wir spielen können:
@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
Der Parameter „assigned“ gibt functools.wraps an, welche Attribute vom umschlossenen Objekt übernommen und auf den Wrapper übertragen werden sollen. Der Parameter hat standardmäßig ein Tupel WRAPPER_ASSIGNMENTS, das folgende Attribute enthält:
- __module__: Der Name des Moduls, in dem das Objekt deklariert ist.
- __name__: Der Objektname.
- __qualname__: Eine ausführlichere Version von __name__.
- __doc__: Die Doc-String oben am Objekt.
Der Parameter „updated“ hat ebenfalls standardmäßig einen Tupelwert von (‚__dict__‘,). Der Parameter „updated“ gibt dem wraps-Decorator an, welche Attribute des Wrapper-Aufrufs mit den Werten aus dem ursprünglichen Objekt aktualisiert werden müssen. Standardmäßig wird das __dict__-Attribut des umschließenden Objekts mit den Schlüssel-Wert-Paaren des __dict__ des umschlossenen Objekts aktualisiert.
Der wraps-Decorator fügt dem Wrapper-Objekt auch ein neues Attribut mit dem Namen __wrapped__ hinzu, das einen Zeiger auf die umschlossene Funktion oder Klasse enthält. Dies ist sehr nützlich, da es ermöglicht, in das tatsächliche Objekt, das umschlossen wird, zu blicken, um mehr Metadaten über das Objekt zu sehen, die functools.wraps nicht automatisch einschließt, wie z.B. __defaults__.
Decorator ohne functools.wraps
Zuerst wollen wir sehen, wie eine Funktion aussieht, die einen Decorator hat, wenn keine Metadaten übertragen werden. Der Decorator, der im Beispiel verwendet wird, macht nichts, aber es ist zu beachten, dass er einen eigene Docstring („““Wrapper function“”) hat und der __name__ der Umschließungsfunktion „wrapper“ heißt.
def example_decorator(func): def wrapper(*args, **kwargs): """Wrapper function""" return func(*args, **kwargs)
return wrapper
Nun verwenden wir den Decorator auf der folgenden Funktion und betrachten einige ihrer Metadaten:
@example_decorator def hello_world(planet: str = 'earth'): """Say hello to a world""" print(f"Hello, {planet}!")
# Überprüfung der Metadaten der dekorierten Funktion print(f'{hello_world.__name__ = }') print(f'{hello_world.__doc__ = }') print(f'{hello_world.__annotations__ = }') print(f'{hello_world.__dict__ = }')
Ausgabe:
hello_world.__name__ = 'wrapper' hello_world.__doc__ = 'Wrapper function' hello_world.__annotations__ = {} hello_world.__dict__ = {}
Aus der Ausgabe geht hervor, dass keine der Metadaten auf die umschließende Funktion übertragen wurde.
Wenn wir außerdem das Funktionsobjekt ausgeben:
print(hello_world)
Erhalten wir die folgende Ausgabe, die uns keine Informationen über die hello_world-Funktion liefert:
<function example_decorator.<locals>.wrapper at 0x7a122c8a9090>
Decorator mit functools.wraps
Nun erstellen wir denselben Decorator, fügen jedoch den functools.wraps-Decorator hinzu. Um functools.wraps zu verwenden, muss man es nur dem Wrapper-Objekt hinzufügen:
from functools import wraps
def example_decorator(func): @wraps(func) def wrapper(*args, **kwargs): """Wrapper function""" return func(*args, **kwargs)
return wrapper
Und wenn wir jetzt dieselben Metadaten betrachten wie zuvor:
@example_decorator def hello_world(planet: str='earth'): """Say hello to a world""" print(f"Hello, {planet}!")
# Überprüfung der Metadaten der dekorierten Funktion print(f'{hello_world.__name__ = }') print(f'{hello_world.__doc__ = }') print(f'{hello_world.__annotations__ = }') print(f'{hello_world.__dict__ = }')
Erhalten wir die folgende Ausgabe:
hello_world.__name__ = 'hello_world' hello_world.__doc__ = 'Say hello to a world' hello_world.__annotations__ = {'planet': <class 'str'>} hello_world.__dict__ = {'__wrapped__': <function hello_world at 0x7b49d5fccc10>}
Wir sehen, dass das __name__-Attribut aktualisiert wurde, um mit dem Namen der Funktion übereinzustimmen, die der Decorator umschließt. Die Docstring der umschlossenen Funktion wird ebenfalls aktualisiert, ebenso wie die statischen Typisierungsannotationen. Das __dict__-Attribut enthält auch alle Attribute von hello_world (was nichts ist, da es sich um eine Funktion handelt) und hat auch das __wrapped__-Attribut, das einen direkten Link zur Originalfunktion enthält.
Wie oben erwähnt, enthält das __wrapped__-Attribut einen Zeiger auf das Originalobjekt. Dies kann sehr nützlich sein, wenn ein anderes Programm oder ein Entwickler die Möglichkeit haben möchte, vom Umschlossenen-Objekt mehr Informationen zu erhalten. Es gibt ein weiteres Modul der Python-Standardbibliothek dafür namens „inspect.“ In diesem Modul gibt es eine Funktion namens inspect.signature(), die tatsächlich nach einem __wrapped__-Attribut in __dict__ sucht und, wenn eins gefunden wird, dem __wrapped__-Wert folgt, um das tatsächliche Objekt inspizieren zu können!
Man kann aber auch manuell das __wrapped__-Attribut verwenden, um zusätzliche Informationen über das umschlossene Objekt zu erhalten. Zum Beispiel überträgt functools.wraps nicht automatisch Standardwerte auf das Wrapper-Objekt (weil Klassen kein __defaults__-Attribut haben und eine Klasse ein Wrapper sein kann), aber mit __wrapped__ können wir die Standardwerte sehen, die im __defaults__-Attribut gespeichert sind:
print(f'{hello_world.__dict__["__wrapped__"].__defaults__ = }')
Und wir sehen, dass die Ausgabe wie erwartet ist; das planet-Argument der obigen Funktion hat einen Standardwert von „earth.“
hello_world.__dict__["__wrapped__"].__defaults__ = ('earth',)
Schließlich können wir auch das Funktionsobjekt ausgeben; es zeigt die korrekte Funktion:
print(hello_world)
Ausgabe:
<function hello_world at 0x7a122d7295a0>
Zusätzliche Metadaten übertragen
Die Verwendung der vordefinierten Werte des wraps-Decorators ist normalerweise ausreichend, aber wenn man mehr (oder weniger) Metadaten auf die Wrapper-Funktion übertragen möchte, kann man eigene Argumente übergeben. Angenommen, wir möchten die Standardwerte der Argumente Ihrer Funktion speichern; Wir können weitere Doppelunterstrichattribute zum „assigned“-Argument hinzufügen:
MORE_WRAPPER_ASSIGNMENTS = ( '__module__', '__name__', '__qualname__', '__annotations__', '__doc__', '__defaults__', '__kwdefaults__' )
def example_decorator(func): @wraps(func, assigned=MORE_WRAPPER_ASSIGNMENTS) def wrapper(*args, **kwargs): """Wrapper function""" return func(*args, **kwargs)
return wrapper
@example_decorator
def hello_world(planet: str='earth'): """Say hello to a world""" print(f"Hello, {planet}!")
Wenn wir nun versuchen, das __defaults__-Doppelunterstrichattribut auszudrucken, sehen wir (‚earth‘,), weil der Parameter „planet“ in der Funktion hello_world einen Standardwert von ‚earth‘ hat:
print(f'{hello_world.__defaults__ = }')
Und die Ausgabe:
hello_world.__defaults__ = ('earth',)
Abschließende Gedanken
Das Speichern der Metadaten von Objekten, die Decorator verwenden, ist äußerst wichtig, da dies zu einem Code führt, der einfacher zu debuggen ist und sicherstellt, dass Objekte immer noch mit anderen Teilen der Sprache, wie z.B. der Introspektion, funktionieren. Das Hinzufügen von functools.wraps zum Decorator ermöglicht es, die wichtigsten Attribute leicht auf das Wrapper-Objekt zu übertragen. Dabei sollte jedoch beachtet werden, dass functools.wraps nicht automatisch jedes Attribut überträgt.