Cosa fa functools.wraps?


651

In un commento su questa risposta a un'altra domanda , qualcuno ha detto che non erano sicuri di cosa functools.wrapsstesse facendo. Quindi, sto ponendo questa domanda in modo che ci sia una sua registrazione su StackOverflow per riferimento futuro: cosa fa functools.wrapsesattamente?

Risposte:


1070

Quando usi un decoratore, stai sostituendo una funzione con un'altra. In altre parole, se hai un decoratore

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

poi quando dici

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

è esattamente come dire

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

e la tua funzione fviene sostituita con la funzione with_logging. Sfortunatamente, questo significa che se poi lo dici

print(f.__name__)

verrà stampato with_loggingperché è il nome della tua nuova funzione. In effetti, se guardi il docstring f, sarà vuoto perché with_loggingnon ha docstring, e quindi il docstring che hai scritto non ci sarà più. Inoltre, se si guarda al risultato pydoc per quella funzione, non verrà elencato come prendendo un argomento x; invece verrà elencato come take *argse **kwargsperché è quello che richiede with_logging.

Se usare un decoratore significava sempre perdere queste informazioni su una funzione, sarebbe un problema serio. Ecco perché abbiamo functools.wraps. Questo prende una funzione usata in un decoratore e aggiunge la funzionalità di copia su nome della funzione, docstring, lista di argomenti, ecc. E poiché wrapsè esso stesso un decoratore, il seguente codice fa la cosa giusta:

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'

7
Sì, preferisco evitare il modulo decoratore poiché functools.wraps fa parte della libreria standard e quindi non introduce un'altra dipendenza esterna. Ma il modulo decoratore risolve davvero il problema della guida, che si spera che un giorno funzionerà anche.
Eli Courtwright,

6
ecco un esempio di cosa può accadere se non si utilizzano gli involucri: i test di doctools possono improvvisamente scomparire. questo perché doctools non può trovare i test nelle funzioni decorate a meno che qualcosa come wraps () non li abbia copiati.
Andrew Cooke,

88
perché abbiamo bisogno functools.wrapsdi questo lavoro, non dovrebbe essere solo parte del motivo decorativo in primo luogo? quando non vorresti usare @wraps?
mercoledì

56
@wim: ho scritto alcuni decoratori che fanno la propria versione @wrapsper eseguire vari tipi di modifiche o annotazioni sui valori copiati. Fondamentalmente, è un'estensione della filosofia di Python secondo cui esplicito è meglio di implicito e casi speciali non sono abbastanza speciali da infrangere le regole. (Il codice è molto più semplice e la lingua più semplice da capire se @wrapsdeve essere fornita manualmente, piuttosto che usare un qualche tipo di meccanismo speciale di opt-out.)
ssokolow

35
@LucasMalor Non tutti i decoratori avvolgono le funzioni che decorano. Alcuni applicano effetti collaterali, come la loro registrazione in una sorta di sistema di ricerca.
ssokolow,

22

Uso molto spesso le lezioni, piuttosto che le funzioni, per i miei decoratori. Ho avuto qualche problema con questo perché un oggetto non avrà tutti gli stessi attributi che ci si aspetta da una funzione. Ad esempio, un oggetto non avrà l'attributo __name__. Ho avuto un problema specifico con questo che era piuttosto difficile da rintracciare dove Django stava segnalando l'errore "oggetto non ha attributo ' __name__'". Sfortunatamente, per i decoratori di classe, non credo che @wrap farà il lavoro. Ho invece creato una classe decoratore di base in questo modo:

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)

Questa classe inoltra tutti gli attributi richiamati alla funzione che viene decorata. Quindi, ora puoi creare un semplice decoratore che controlla che 2 argomenti siano specificati in questo modo:

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

        return self.func(*args)

7
Come @wrapsdice la documentazione di , @wrapsè solo una funzione di convenienza per functools.update_wrapper(). In caso di decoratore di classe, puoi chiamare update_wrapper()direttamente dal tuo __init__()metodo. Quindi, non è necessario creare DecBasea tutti, si può semplicemente includere __init__()della process_loginlinea: update_wrapper(self, func). È tutto.
Fabiano,

15

A partire da Python 3.5+:

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

È un alias per g = functools.update_wrapper(g, f). Fa esattamente tre cose:

  • copia i __module__, __name__, __qualname__, __doc__, e __annotations__attributi di fon g. Questo elenco predefinito è in WRAPPER_ASSIGNMENTS, puoi vederlo nella sorgente di functools .
  • aggiorna il __dict__di gcon tutti gli elementi di f.__dict__. (vedi WRAPPER_UPDATESnella fonte)
  • imposta un nuovo __wrapped__=fattributo sug

La conseguenza è che gsembra avere lo stesso nome, docstring, nome del modulo e firma di f. L'unico problema è che per quanto riguarda la firma questo non è in realtà vero: è solo che inspect.signaturesegue le catene wrapper per impostazione predefinita. Puoi verificarlo usando inspect.signature(g, follow_wrapped=False)come spiegato nel documento . Ciò ha conseguenze fastidiose:

  • il codice wrapper verrà eseguito anche quando gli argomenti forniti non sono validi.
  • il codice wrapper non può accedere facilmente a un argomento usando il suo nome, dai * args ricevuti, ** kwargs. In effetti uno dovrebbe gestire tutti i casi (posizionale, parola chiave, default) e quindi usare qualcosa di simile Signature.bind().

Ora c'è un po 'di confusione tra functools.wrapse decoratori, perché un caso d'uso molto frequente per lo sviluppo di decoratori è avvolgere le funzioni. Ma entrambi sono concetti completamente indipendenti. Se sei interessato a capire la differenza, ho implementato librerie di supporto per entrambi: decopatch per scrivere facilmente decoratori e makefun per fornire un sostituto per conservare la firma @wraps. Nota che makefunsi basa sullo stesso trucco provato della famosa decoratorlibreria.


3

questo è il codice sorgente di 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)

2
  1. Prerequisito: devi sapere come usare i decoratori e specialmente con gli involucri. Questo commento lo spiega un po 'chiaro o questo link lo spiega anche abbastanza bene.

  2. Ogni volta che utilizziamo Ad esempio: @wraps seguito dalla nostra funzione wrapper. Secondo i dettagli forniti in questo link , lo dice

functools.wraps è una funzione comoda per richiamare update_wrapper () come decoratore di funzioni, quando si definisce una funzione wrapper.

È equivalente a parziale (update_wrapper, wrapping = wrapping, assegnato = assegnato, aggiornato = aggiornato).

Quindi @wraps decorator in realtà dà una chiamata a functools.partial (func [, * args] [, ** parole chiave]).

La definizione di functools.partial () dice questo

Partial () viene utilizzato per un'applicazione di funzione parziale che "congela" una parte degli argomenti di una funzione e / o parole chiave risultanti in un nuovo oggetto con una firma semplificata. Ad esempio, partial () può essere usato per creare un callable che si comporta come la funzione int () in cui l'argomento base è impostato per default su due:

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

Il che mi porta alla conclusione che @wraps dà una chiamata a partial () e gli passa la funzione wrapper come parametro. Il partial () alla fine restituisce la versione semplificata, cioè l'oggetto di ciò che è all'interno della funzione wrapper e non la funzione wrapper stessa.


-4

In breve, functools.wraps è solo una funzione normale. Consideriamo questo esempio ufficiale . Con l'aiuto del codice sorgente , possiamo vedere più dettagli sull'implementazione e i passaggi in esecuzione come segue:

  1. wraps (f) restituisce un oggetto, ad esempio O1 . È un oggetto della classe Parziale
  2. Il prossimo passo è @ O1 ... che è la notazione del decoratore in Python. Significa

involucro = O1 .__ chiamata __ (involucro)

Controllando l'implementazione di __call__ , vediamo che dopo questo passaggio, il wrapper (sul lato sinistro) diventa l'oggetto risultante da self.func (* self.args, * args, ** newkeywords) Verifica la creazione di O1 in __new__ , noi sapere self.func è la funzione update_wrapper . Utilizza il parametro * args , il wrapper sul lato destro , come primo parametro. Controllando l'ultimo passaggio di update_wrapper , si può vedere che viene restituito il wrapper sul lato destro , con alcuni degli attributi modificati secondo necessità.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.