990 Stimmen

Was macht functools.wraps?

In einem Kommentar zu diesem Antwort auf eine andere Frage Jemand sagte, er sei sich nicht sicher, was er mit den functools.wraps getan hat. Ich stelle also diese Frage, damit es eine Aufzeichnung davon auf StackOverflow für zukünftige Referenzen gibt: Was bedeutet functools.wraps was genau tun?

1584voto

Eli Courtwright Punkte 174547

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

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

Wenn Sie dann sagen

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

ist es genau dasselbe, als wenn man sagt

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

und Ihre Funktion f wird ersetzt durch die Funktion with_logging . Leider bedeutet dies, dass, wenn Sie dann sagen

print(f.__name__)

es wird gedruckt with_logging weil das der Name Ihrer neuen Funktion ist. In der Tat, wenn Sie sich den Docstring für f wird sie leer sein, weil with_logging hat keinen Docstring, so dass der Docstring, den Sie geschrieben haben, nicht mehr vorhanden sein wird. Wenn Sie sich das pydoc-Ergebnis für diese Funktion ansehen, wird sie auch nicht als Funktion mit einem Argument aufgeführt x ; stattdessen wird es als Einnahme von *args und **kwargs denn das ist es, was with_logging braucht.

Wenn die Verwendung eines Dekorators immer bedeuten würde, dass diese Information über eine Funktion verloren geht, wäre das ein ernstes Problem. Das ist der Grund, warum wir functools.wraps . Dies nimmt eine Funktion, die in einem Dekorator verwendet wird und fügt die Funktionalität des Kopierens des Funktionsnamens, des docstring, der Argumentenliste, etc. hinzu. Und da wraps selbst ein Dekorator ist, tut der folgende Code das Richtige:

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'

3 Stimmen

Es gibt eine Lösung für das Hilfeproblem, die das Decorator-Modul verwendet und in den Kommentaren zu dieser Antwort erwähnt wird: stackoverflow.com/questions/1782843/

11 Stimmen

Ja, ich ziehe es vor, das Decorator-Modul zu vermeiden, da functools.wraps Teil der Standardbibliothek ist und somit keine weitere externe Abhängigkeit einführt. Aber das Decorator-Modul löst in der Tat das Hilfe-Problem, was functools.wraps hoffentlich eines Tages auch tun wird.

9 Stimmen

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 in dekorierten Funktionen nicht finden kann, es sei denn, etwas wie wraps() hat sie hinüber kopiert.

68voto

smarie Punkte 3528

Ab Python 3.5+:

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

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

  • kopiert er die __module__ , __name__ , __qualname__ , __doc__ und __annotations__ Eigenschaften von f auf g . Diese Standardliste ist in WRAPPER_ASSIGNMENTS können Sie es in der Funktools Quelle .
  • aktualisiert er die __dict__ von g mit allen Elementen aus f.__dict__ . (siehe WRAPPER_UPDATES in der Quelle)
  • setzt sie eine neue __wrapped__=f Attribut an g

Die Folge ist, dass g erscheint mit demselben Namen, Docstring, Modulnamen und derselben Signatur wie f . Das einzige Problem ist, dass dies in Bezug auf die Unterschrift nicht wirklich stimmt: Es ist nur so, dass inspect.signature folgt standardmäßig Wrapper-Ketten. Sie können dies überprüfen, indem Sie inspect.signature(g, follow_wrapped=False) wie in der doc . Das hat ärgerliche Folgen:

  • wird der Wrapper-Code auch dann ausgeführt, wenn die angegebenen Argumente ungültig sind.
  • der Wrapper-Code kann nicht einfach auf ein Argument über seinen Namen zugreifen, von den empfangenen *args, **kwargs. In der Tat müsste man alle Fälle behandeln (positional, keyword, default) und daher etwas wie Signature.bind() .

Nun gibt es ein wenig Verwirrung zwischen functools.wraps und Dekoratoren, denn ein sehr häufiger Anwendungsfall für die Entwicklung von Dekoratoren ist es, Funktionen zu verpacken. Aber beide sind völlig unabhängige Konzepte. Wenn Sie daran interessiert sind, den Unterschied zu verstehen, habe ich Hilfsbibliotheken für beide implementiert: decopatch um einfach Dekoratoren zu schreiben, und makefun um einen signaturerhaltenden Ersatz zu bieten für @wraps . Beachten Sie, dass makefun beruht auf demselben bewährten Trick wie der berühmte decorator Bibliothek.

48voto

BlueJapan Punkte 586
  1. Nehmen wir an, wir haben dies: Ein einfacher Dekorator, der die Ausgabe einer Funktion in eine Zeichenkette umwandelt, gefolgt von drei !!!!.

    def mydeco(func): def wrapper(*args, *kwargs): return f'{func(args, **kwargs)}!!!' return wrapper

  2. Lassen Sie uns nun zwei verschiedene Funktionen mit "mydeco" dekorieren:

    @mydeco def add(a, b): '''Add two objects together, the long way''' return a + b

    @mydeco def mysum(*args): '''Sum any numbers together, the long way''' total = 0 for one_item in args: total += one_item return total

  3. Wenn ich add(10,20), mysum(1,2,3,4) ausführe, funktioniert es!

    add(10,20) '30!!!'

    mysum(1,2,3,4) '10!!!!'

  4. Allerdings ist die Name Attribut, das uns den Namen einer Funktion gibt, wenn wir sie definieren,

    add.name 'wrapper`

    mysum.name 'wrapper'

  5. Schlimmer

    help(add) Help on function wrapper in module main: wrapper(*args, **kwargs)

    help(mysum) Help on function wrapper in module main: wrapper(*args, **kwargs)

  6. können wir teilweise beheben, indem:

    def mydeco(func): def wrapper(*args, *kwargs): return f'{func(args, **kwargs)}!!!' wrapper.name = func.name wrapper.doc = func.doc return wrapper

  7. Nun führen wir Schritt 5 (2. Mal) erneut aus:

    help(add) Help on function add in module main:

    add(*args, **kwargs) Add two objects together, the long way

    help(mysum) Help on function mysum in module main:

    mysum(*args, **kwargs) Sum any numbers together, the long way

  8. aber wir können functools.wraps (decotator tool) verwenden

    from functools import wraps

    def mydeco(func): @wraps(func) def wrapper(args, kwargs): return f'{func(*args, **kwargs)}!!!' return wrapper

  9. führen Sie nun Schritt 5 (3. Mal) erneut aus

    help(add) Help on function add in module main: add(a, b) Add two objects together, the long way

    help(mysum) Help on function mysum in module main: mysum(*args) Sum any numbers together, the long way

Referenz

1 Stimmen

Danke für den Hinweis

31voto

Josh Punkte 11770

Ich verwende sehr oft Klassen und keine Funktionen für meine Dekoratoren. Ich hatte einige Probleme damit, weil ein Objekt nicht alle Attribute hat, die von einer Funktion erwartet werden. Zum Beispiel hat ein Objekt nicht das Attribut __name__ . Ich hatte ein spezielles Problem mit dieser, die ziemlich schwer zu verfolgen war, wo Django war die Meldung der Fehler "Objekt hat kein Attribut ' __name__ '". Leider glaube ich nicht, dass @wrap für Dekoratoren im Klassenstil ausreicht. Ich habe stattdessen eine Basis-Dekorator-Klasse 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 leitet alle Attributaufrufe an die Funktion weiter, die gerade dekoriert wird. So können Sie nun einen einfachen Dekorator erstellen, der prüft, ob 2 Argumente angegeben sind, wie folgt:

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

        return self.func(*args)

12 Stimmen

Wie die Dokumente von @wraps sagt, @wraps ist lediglich eine Komfortfunktion für functools.update_wrapper() . Im Falle eines Klassendekorators können Sie update_wrapper() direkt von Ihrem __init__() Methode. Sie müssen also nicht die Methode DecBase können Sie einfach einfügen __init__() de process_login die Linie: update_wrapper(self, func) . Das ist alles.

0 Stimmen

Nur, damit auch andere diese Antwort finden: Flask mit seinem add_url_route erfordert (in einigen Fällen?), dass die bereitgestellten view_func Funktion hat eine __name__ , was nicht mehr der Fall ist, wenn die bereitgestellte Funktion tatsächlich eine dekorierte Methode ist, selbst wenn functools.wraps im Dekorator verwendet wird.

0 Stimmen

Und als Ergebnis, +1 für @Fabiano: mit update_wrapper anstelle von @wraps erfüllt die Aufgabe :)

6voto

Baliang Punkte 85

Dies ist der Quellcode über 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)

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X