¿Qué hace functools.wraps?
Pregunta
En un comentario sobre este responda a otra pregunta , alguien dijo que no estaba seguro de lo que estaba haciendo functools.wraps
. Entonces, hago esta pregunta para que haya un registro de ella en StackOverflow para referencia futura: ¿qué hace exactamente functools.wraps
?
Solución
Cuando usa un decorador, está reemplazando una función con otra. En otras palabras, si tienes un decorador
def logged(func):
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
entonces cuando dices
@logged
def f(x):
"""does some math"""
return x + x * x
es exactamente lo mismo que decir
def f(x):
"""does some math"""
return x + x * x
f = logged(f)
y su función f
se reemplaza con la función with_logging. Desafortunadamente, esto significa que si dices
print(f.__name__)
imprimirá with_logging
porque ese es el nombre de su nueva función. De hecho, si observa la cadena de documentación de f
, estará en blanco porque with_logging
no tiene cadena de documentación, por lo que la cadena de documentación que escribió ya no estará allí. Además, si observa el resultado de pydoc para esa función, no aparecerá como un argumento x
; en su lugar, se mostrará como tomando * args
y ** kwargs
porque eso es lo que requiere with_logging.
Si usar un decorador siempre significa perder esta información sobre una función, sería un problema grave. Es por eso que tenemos functools.wraps
. Esto toma una función utilizada en un decorador y agrega la funcionalidad de copiar sobre el nombre de la función, la cadena de documentos, la lista de argumentos, etc. Y dado que wraps
es en sí mismo un decorador, el siguiente código hace lo correcto:
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'
Otros consejos
Muy a menudo uso clases, en lugar de funciones, para mis decoradores. Estaba teniendo algunos problemas con esto porque un objeto no tendrá los mismos atributos que se esperan de una función. Por ejemplo, un objeto no tendrá el atributo __name__
. Tuve un problema específico con esto que fue bastante difícil de rastrear cuando Django informaba el error '' el objeto no tiene el atributo ' __name__
' " ;. Desafortunadamente, para los decoradores de estilo de clase, no creo que @wrap haga el trabajo. En su lugar, he creado una clase de decorador base como esta:
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)
Esta clase representa todas las llamadas de atributos a la función que se está decorando. Entonces, ahora puede crear un decorador simple que verifique que se especifiquen 2 argumentos de esta manera:
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 partir de python 3.5+:
@functools.wraps(f)
def g():
pass
Es un alias para g = functools.update_wrapper (g, f)
. Hace exactamente tres cosas:
- copia el
__module__
,__name__
,__qualname__
,__doc__
y__annotations __
atributos def
eng
. Esta lista predeterminada está enWRAPPER_ASSIGNMENTS
, puede verla en fuente de functools . - actualiza el
__dict__
deg
con todos los elementos def .__ dict__
. (consulteWRAPPER_UPDATES
en la fuente) - establece un nuevo atributo
__wrapped __ = f
eng
La consecuencia es que g
aparece con el mismo nombre, cadena de documentación, nombre del módulo y firma que f
. El único problema es que con respecto a la firma, esto no es realmente cierto: es solo que inspect.signature
sigue las cadenas de envoltura de forma predeterminada. Puede verificarlo utilizando inspect.signature (g, follow_wrapped = False)
como se explica en doc . Esto tiene consecuencias molestas:
- el código contenedor se ejecutará incluso cuando los argumentos proporcionados no sean válidos.
- el código contenedor no puede acceder fácilmente a un argumento usando su nombre, desde los * args, ** kwargs recibidos. De hecho, uno tendría que manejar todos los casos (posicional, palabra clave, predeterminado) y, por lo tanto, usar algo como
Signature.bind ()
.
Ahora hay un poco de confusión entre functools.wraps
y los decoradores, porque un caso de uso muy frecuente para desarrollar decoradores es ajustar las funciones. Pero ambos son conceptos completamente independientes. Si está interesado en comprender la diferencia, implementé bibliotecas auxiliares para ambos: decopatch para escribir decoradores fácilmente, y makefun para proporcionar un reemplazo de preservación de firma para @wraps . Tenga en cuenta que
makefun
se basa en el mismo truco probado que la famosa biblioteca decorator
.
este es el código fuente sobre 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)
-
Requisito previo: debe saber cómo usar decoradores y especialmente con envolturas. Este comentario lo explica un poco claro o esto link también lo explica bastante bien.
-
Siempre que usamos For eg: @wraps seguido de nuestra propia función wrapper. Según los detalles proporcionados en este enlace , dice que
functools.wraps es una función conveniente para invocar update_wrapper () como decorador de funciones, al definir una función de envoltura.
Es equivalente a parcial (update_wrapper, envuelto = envuelto, asignado = asignado, actualizado = actualizado).
Entonces, el decorador @wraps realmente llama a functools.partial (func [, * args] [, ** keywords]).
La definición functools.partial () dice que
El parcial () se utiliza para la aplicación de funciones parciales que "congela" alguna parte de los argumentos y / o palabras clave de una función, lo que resulta en un nuevo objeto con una firma simplificada. Por ejemplo, partial () se puede usar para crear un invocable que se comporte como la función int () donde el argumento base se predetermina a dos:
>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18
Lo que me lleva a la conclusión de que @wraps llama a partial () y le pasa la función de contenedor como parámetro. El parcial () al final devuelve la versión simplificada, es decir, el objeto de lo que está dentro de la función de contenedor y no la función de contenedor en sí.
En resumen, functools.wraps es solo una función normal. Consideremos este ejemplo oficial . Con la ayuda del código fuente , podemos vea más detalles sobre la implementación y los pasos de ejecución de la siguiente manera:
- wraps (f) devuelve un objeto, digamos O1 . Es un objeto de la clase Parcial
- El siguiente paso es @ O1 ... que es la notación de decorador en python. Significa
wrapper=O1.__call__(wrapper)
Comprobación de la implementación de __call __call __call __call __call __call __call > , vemos que después de este paso, (el lado izquierdo) wrapper se convierte en el objeto resultante de self.func (* self.args, * args, ** newkeywords ) Al comprobar la creación de O1 en __new__ , sabemos que self.func es la función update_wrapper . Utiliza el parámetro * args , el contenedor del lado derecho , como su primer parámetro. Al verificar el último paso de update_wrapper , se puede ver que se devuelve el wrapper del lado derecho, con algunos de los atributos modificados según sea necesario.