Domanda

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

È stato utile?

Soluzione

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

allora quando dici

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

è esattamente lo stesso che dire

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

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

print(f.__name__)

stamperà with_logging perché è il nome della tua nuova funzione. In effetti, se guardi il docstring per f , sarà vuoto perché with_logging non ha docstring, quindi il docstring che hai scritto non ci sarà più. Inoltre, se si guarda al risultato pydoc per quella funzione, non verrà elencato come un argomento x ; invece verrà elencato come prendendo * args e ** kwargs perché è ciò 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 sul nome della funzione, il docstring, l'elenco degli argomenti, ecc. E poiché avvolge è 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'

Altri suggerimenti

Uso molto spesso le classi, 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 abbastanza difficile da rintracciare dove Django stava segnalando l'errore "L'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)

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 __module__ , __name__ , __qualname__ , __doc__ e __annotations __ attributi di f su g . Questo elenco predefinito è in WRAPPER_ASSIGNMENTS , puoi vederlo in functools source .
  • aggiorna il __dict__ di g con tutti gli elementi di f .__ dict__ . (vedi WRAPPER_UPDATES nella fonte)
  • imposta un nuovo attributo __wrapped __ = f su g

La conseguenza è che g sembra 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.signature segue le catene wrapper per impostazione predefinita. Puoi verificarlo utilizzando inspect.signature (g, follow_wrapped = False) come spiegato in doc . 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 si dovrebbe gestire tutti i casi (posizionale, parola chiave, default) e quindi usare qualcosa come Signature.bind () .

Ora c'è un po 'di confusione tra functools.wraps e 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 decoratori facilmente e makefun per fornire una sostituzione che preserva la firma di @wraps . Nota che makefun si basa sullo stesso trucco provato della famosa libreria decorator .

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)
  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 , si dice che

  

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

     

È equivalente a parziale (update_wrapper, wrapped = 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 che

  

Il partial () è usato per l'applicazione di funzioni parziali che & # 8220; congela & # 8221; una parte degli argomenti e / o delle parole chiave di una funzione che risulta 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 è dentro la funzione wrapper e non la stessa funzione wrapper.

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

  1. avvolge (f) restituisce un oggetto, ad esempio O1 . È un oggetto della partial class >
  2. Il passaggio successivo è @ O1 ... che è la notazione del decoratore in Python. Significa
  

involucro = O1 .__ chiamata __ (involucro)

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top