Cosa fa functools.wraps?
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
?
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 dif
sug
. Questo elenco predefinito è inWRAPPER_ASSIGNMENTS
, puoi vederlo in functools source . - aggiorna il
__dict__
dig
con tutti gli elementi dif .__ dict__
. (vediWRAPPER_UPDATES
nella fonte) - imposta un nuovo attributo
__wrapped __ = f
sug
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)
-
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.
-
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:
- avvolge (f) restituisce un oggetto, ad esempio O1 . È un oggetto della partial class >
- Il passaggio successivo è @ O1 ... che è la notazione del decoratore in Python. Significa
involucro = O1 .__ chiamata __ (involucro)
Verifica dell'implementazione di