Was macht functools.wraps?

649

In einem Kommentar zu dieser Antwort auf eine andere Frage sagte jemand, dass er nicht sicher sei, was er functools.wrapstue. Ich stelle diese Frage, damit sie in StackOverflow aufgezeichnet wird, damit Sie später darauf zurückgreifen können: Was functools.wrapsgenau macht das?

Eli Courtwright
quelle

Antworten:

1068

Wenn Sie einen Dekorateur verwenden, ersetzen Sie eine Funktion durch eine andere. Mit anderen Worten, wenn Sie einen Dekorateur haben

def logged(func):
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

dann, wenn du sagst

@logged
def f(x):
   """does some math"""
   return x + x * x

es ist genau das gleiche wie zu sagen

def f(x):
    """does some math"""
    return x + x * x
f = logged(f)

und Ihre Funktion fwird durch die Funktion ersetzt with_logging. Leider bedeutet das, wenn Sie dann sagen

print(f.__name__)

Es wird gedruckt, with_loggingda dies der Name Ihrer neuen Funktion ist. Wenn Sie sich die Dokumentzeichenfolge ansehen f, ist sie leer, da with_loggingsie keine Dokumentzeichenfolge enthält. Daher ist die von Ihnen geschriebene Dokumentzeichenfolge nicht mehr vorhanden. Wenn Sie sich das pydoc-Ergebnis für diese Funktion ansehen, wird es nicht als ein Argument aufgeführt x. Stattdessen wird es als take aufgeführt *argsund **kwargsweil with_logging das braucht.

Wenn die Verwendung eines Dekorateurs immer den Verlust dieser Informationen über eine Funktion bedeutete, wäre dies ein ernstes Problem. Deshalb haben wir functools.wraps. Dies übernimmt eine in einem Dekorateur verwendete Funktion und fügt die Funktionalität zum Kopieren des Funktionsnamens, der Dokumentzeichenfolge, der Argumentliste usw. hinzu. Da wrapses sich selbst um einen Dekorateur handelt, führt der folgende Code das Richtige aus:

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

print(f.__name__)  # prints 'f'
print(f.__doc__)   # prints 'does some math'
Eli Courtwright
quelle
6
Ja, ich bevorzuge es, das Dekorationsmodul zu vermeiden, da functools.wraps Teil der Standardbibliothek ist und daher keine weitere externe Abhängigkeit einführt. Aber das Dekorationsmodul löst tatsächlich das Hilfsproblem, das hoffentlich auch eines Tages funktioniert.
Eli Courtwright
6
Hier ist ein Beispiel dafür, was passieren kann, wenn Sie keine Wraps verwenden: Doctools-Tests können plötzlich verschwinden. Das liegt daran, dass Doctools die Tests nicht in dekorierten Funktionen finden können, es sei denn, etwas wie wraps () hat sie kopiert.
Andrew Cooke
87
Warum brauchen wir functools.wrapsfür diesen Job, sollte er nicht in erster Linie Teil des Dekorationsmusters sein? Wann möchten Sie @wraps nicht verwenden?
wim
56
@wim: Ich habe einige Dekorateure geschrieben, die ihre eigene Version von erstellen @wraps, um verschiedene Arten von Änderungen oder Anmerkungen an den kopierten Werten vorzunehmen. Grundsätzlich ist es eine Erweiterung der Python-Philosophie, dass explizit besser als implizit ist und Sonderfälle nicht speziell genug sind, um die Regeln zu brechen. (Der Code ist viel einfacher und die Sprache leichter zu verstehen, wenn @wrapssie manuell bereitgestellt werden muss, anstatt einen speziellen Opt-out-Mechanismus zu verwenden.)
ssokolow
35
@LucasMalor Nicht alle Dekorateure verpacken die Funktionen, die sie dekorieren. Einige wenden Nebenwirkungen an, z. B. die Registrierung in einem Suchsystem.
ssokolow
21

Ich benutze sehr oft Klassen anstelle von Funktionen für meine Dekorateure. Ich hatte einige Probleme damit, weil ein Objekt nicht dieselben Attribute hat, die von einer Funktion erwartet werden. Beispielsweise verfügt ein Objekt nicht über das Attribut __name__. Ich hatte ein spezielles Problem damit, das ziemlich schwer zu verfolgen war, wo Django den Fehler "Objekt hat kein Attribut" gemeldet hat __name__. Leider glaube ich für Dekorateure im Klassenstil nicht, dass @wrap den Job machen wird. Ich habe stattdessen eine Basisdekorationsklasse wie folgt erstellt:

class DecBase(object):
    func = None

    def __init__(self, func):
        self.__func = func

    def __getattribute__(self, name):
        if name == "func":
            return super(DecBase, self).__getattribute__(name)

        return self.func.__getattribute__(name)

    def __setattr__(self, name, value):
        if name == "func":
            return super(DecBase, self).__setattr__(name, value)

        return self.func.__setattr__(name, value)

Diese Klasse überträgt alle Attributaufrufe an die Funktion, die dekoriert wird. Sie können jetzt einen einfachen Dekorator erstellen, der überprüft, ob zwei Argumente wie folgt angegeben sind:

class process_login(DecBase):
    def __call__(self, *args):
        if len(args) != 2:
            raise Exception("You can only specify two arguments")

        return self.func(*args)
Josh
quelle
7
Wie die Dokumentation von @wrapssagt, @wrapsist dies nur eine praktische Funktion functools.update_wrapper(). Im Falle eines Klassendekorateurs können Sie update_wrapper()direkt von Ihrer __init__()Methode aus aufrufen . Also, Sie brauchen nicht zu schaffen DecBaseüberhaupt, können Sie einfach sind auf __init__()der process_loginLinie: update_wrapper(self, func). Das ist alles.
Fabiano
13

Ab Python 3.5+:

@functools.wraps(f)
def g():
    pass

Ist ein Alias ​​für g = functools.update_wrapper(g, f). Es macht genau drei Dinge:

  • kopiert es die __module__, __name__, __qualname__, __doc__, und __annotations__Attribute fauf g. Diese Standardliste befindet sich in WRAPPER_ASSIGNMENTS, Sie können sie in der functools-Quelle sehen .
  • es aktualisiert das __dict__von gmit allen Elementen von f.__dict__. (siehe WRAPPER_UPDATESin der Quelle)
  • es setzt ein neues __wrapped__=fAttribut aufg

Die Folge ist, gdass der Name, die Dokumentzeichenfolge, der Modulname und die Signatur denselben Namen haben wie f. Das einzige Problem ist, dass dies in Bezug auf die Signatur nicht wirklich zutrifft: Es folgt nur inspect.signaturestandardmäßig den Wrapper-Ketten. Sie können dies überprüfen, indem Sie inspect.signature(g, follow_wrapped=False)wie im Dokument erläutert verwenden . Das hat ärgerliche Folgen:

  • Der Wrapper-Code wird auch dann ausgeführt, wenn die angegebenen Argumente ungültig sind.
  • Der Wrapper-Code kann mit seinem Namen aus den empfangenen * args, ** kwargs nicht einfach auf ein Argument zugreifen. In der Tat müsste man alle Fälle (Position, Schlüsselwort, Standard) behandeln und daher so etwas wie verwenden Signature.bind().

Jetzt gibt es ein wenig Verwirrung zwischen functools.wrapsund Dekorateuren, da ein sehr häufiger Anwendungsfall für die Entwicklung von Dekorateuren das Umschließen von Funktionen ist. Aber beide sind völlig unabhängige Konzepte. Wenn Sie den Unterschied verstehen möchten , habe ich Hilfsbibliotheken für beide implementiert: Decopatch , um Dekorateure einfach schreiben zu können, und makefun , um einen signaturerhaltenden Ersatz für bereitzustellen @wraps. Beachten Sie, dass dies makefunauf demselben bewährten Trick beruht wie die berühmte decoratorBibliothek.

smarie
quelle
3

Dies ist der Quellcode für Wraps:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')

WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):

    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

   Returns a decorator that invokes update_wrapper() with the decorated
   function as the wrapper argument and the arguments to wraps() as the
   remaining arguments. Default arguments are as for update_wrapper().
   This is a convenience function to simplify applying partial() to
   update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)
Baliang
quelle
2
  1. Voraussetzung: Sie müssen wissen, wie man Dekorateure und speziell mit Wraps verwendet. Dieser Kommentar erklärt es ein bisschen klar oder dieser Link erklärt es auch ziemlich gut.

  2. Wann immer wir For verwenden, zB: @wraps, gefolgt von unserer eigenen Wrapper-Funktion. Gemäß den Angaben in diesem Link heißt es, dass

functools.wraps ist eine praktische Funktion zum Aufrufen von update_wrapper () als Funktionsdekorator beim Definieren einer Wrapper-Funktion.

Es ist äquivalent zu partiell (update_wrapper, verpackt = verpackt, zugewiesen = zugewiesen, aktualisiert = aktualisiert).

@Wraps Decorator ruft also tatsächlich functools.partial auf (func [, * args] [, ** keywords]).

Die Definition von functools.partial () besagt dies

Partial () wird für Teilfunktionsanwendungen verwendet, bei denen ein Teil der Argumente und / oder Schlüsselwörter einer Funktion "eingefroren" wird, was zu einem neuen Objekt mit einer vereinfachten Signatur führt. Mit partiell () kann beispielsweise ein Aufrufobjekt erstellt werden, das sich wie die Funktion int () verhält, bei der das Basisargument standardmäßig zwei ist:

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18

Was mich zu dem Schluss bringt, dass @wraps partiell () aufruft und Ihre Wrapper-Funktion als Parameter an sie übergibt. Das partielle () am Ende gibt die vereinfachte Version zurück, dh das Objekt dessen, was sich in der Wrapper-Funktion befindet, und nicht die Wrapper-Funktion selbst.

3rdi
quelle
-4

Kurz gesagt, functools.wraps ist nur eine reguläre Funktion. Betrachten wir dieses offizielle Beispiel . Mithilfe des Quellcodes können wir weitere Details zur Implementierung und den laufenden Schritten wie folgt anzeigen:

  1. wraps (f) gibt ein Objekt zurück, z . B. O1 . Es ist ein Objekt der Klasse Partial
  2. Der nächste Schritt ist @ O1 ... das ist die Dekorationsnotation in Python. Es bedeutet

wrapper = O1 .__ call __ (wrapper)

Überprüfung der Umsetzung der __call__ , sehen wir , dass nach diesem Schritt (die linke Seite) Wrapper das Objekt durch geführt wird self.func (* self.args, * args, ** newkeywords) Überprüfen der Erstellung von O1 in __new__ , wir know self.func ist die Funktion update_wrapper . Es verwendet den Parameter * args , den Wrapper auf der rechten Seite , als ersten Parameter. Wenn Sie den letzten Schritt von update_wrapper überprüfen , können Sie sehen, dass der Wrapper auf der rechten Seite zurückgegeben wird und einige Attribute nach Bedarf geändert werden.

Yong Yang
quelle