¿Qué hace functools.wraps?

651

En un comentario sobre esta respuesta a otra pregunta , alguien dijo que no estaba seguro de lo que functools.wrapsestaba haciendo. Entonces, hago esta pregunta para que haya un registro de ella en StackOverflow para referencia futura: ¿qué hace functools.wrapsexactamente?

Eli Courtwright
fuente

Respuestas:

1070

Cuando usas un decorador, estás 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 fse reemplaza con la función with_logging. Desafortunadamente, esto significa que si dices

print(f.__name__)

se imprimirá with_loggingporque ese es el nombre de su nueva función. De hecho, si observa la cadena de documentación f, estará en blanco porque with_loggingno 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, aparecerá como tomando *argsy **kwargsporque eso es lo que toma with_logging.

Si usar un decorador siempre significa perder esta información sobre una función, sería un problema grave. Por eso lo 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 como wrapses 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'
Eli Courtwright
fuente
77
Sí, prefiero evitar el módulo decorador ya que functools.wraps es parte de la biblioteca estándar y, por lo tanto, no introduce otra dependencia externa. Pero el módulo decorador sí resuelve el problema de ayuda, que con suerte functools.wraps algún día también lo hará.
Eli Courtwright el
66
Aquí hay un ejemplo de lo que puede suceder si no usa envolturas: las pruebas de doctools pueden desaparecer repentinamente. eso se debe a que doctools no puede encontrar las pruebas en funciones decoradas a menos que algo como wraps () las haya copiado.
Andrew Cooke
88
¿Por qué necesitamos functools.wrapseste trabajo? ¿No debería ser parte del patrón decorador en primer lugar? ¿Cuándo no quieres usar @wraps?
wim
56
@wim: He escrito algunos decoradores que hacen su propia versión @wrapspara realizar varios tipos de modificación o anotación en los valores copiados. Fundamentalmente, es una extensión de la filosofía de Python que explícito es mejor que implícito y los casos especiales no son lo suficientemente especiales como para romper las reglas. (El código es mucho más simple y el idioma más fácil de entender si @wrapsse debe proporcionar de forma manual, en lugar de utilizar algún tipo de mecanismo especial de exclusión.)
ssokolow
35
@LucasMalor No todos los decoradores envuelven las funciones que decoran. Algunos aplican efectos secundarios, como registrarlos en algún tipo de sistema de búsqueda.
ssokolow
22

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 donde Django informaba el error "el objeto no tiene atributo ' __name__'". Desafortunadamente, para los decoradores de estilo de clase, no creo que @wrap haga el trabajo. En cambio, 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 la siguiente 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)
Josh
fuente
77
Como @wrapsdice la documentación de , @wrapses solo una función conveniente functools.update_wrapper(). En el caso del decorador de clase, puede llamar update_wrapper()directamente desde su __init__()método. Por lo tanto, no es necesario crear DecBaseen absoluto, sólo puede incluir el __init__()de process_loginla línea: update_wrapper(self, func). Eso es todo.
Fabiano
15

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 los __module__, __name__, __qualname__, __doc__, y __annotations__atributos de fsobre g. Esta lista predeterminada está en WRAPPER_ASSIGNMENTS, puede verla en la fuente de functools .
  • actualiza el __dict__de gcon todos los elementos de f.__dict__. (ver WRAPPER_UPDATESen la fuente)
  • establece un nuevo __wrapped__=fatributo eng

La consecuencia es que gparece tener 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.signaturesigue las cadenas de envoltura por defecto. Puede verificarlo utilizando inspect.signature(g, follow_wrapped=False)como se explica en el documento . 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 recibidos, ** kwargs. 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.wrapsy los decoradores, porque un caso de uso muy frecuente para los decoradores en desarrollo 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 para preservar la firma @wraps. Tenga en cuenta que se makefunbasa en el mismo truco probado que la famosa decoratorbiblioteca.

smarie
fuente
3

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)
Baliang
fuente
2
  1. Requisito previo: debe saber cómo usar decoradores y especialmente con envolturas. Este comentario lo explica un poco claro o este enlace también lo explica bastante bien.

  2. Siempre que usamos For eg: @wraps seguido de nuestra propia función wrapper. Según los detalles dados 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, parcial () 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í.

3rdi
fuente
-4

En resumen, functools.wraps es solo una función regular. Consideremos este ejemplo oficial . Con la ayuda del código fuente , podemos ver más detalles sobre la implementación y los pasos de ejecución de la siguiente manera:

  1. wraps (f) devuelve un objeto, digamos O1 . Es un objeto de la clase Parcial
  2. El siguiente paso es @ O1 ... que es la notación de decorador en python. Significa

wrapper = O1 .__ call __ (wrapper)

Al verificar la implementación de __call__ , vemos que después de este paso, el contenedor (el lado izquierdo) se convierte en el objeto resultante de self.func (* self.args, * args, ** newkeywords). Al verificar la creación de O1 en __new__ , nosotros saber 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 contenedor del lado derecho , con algunos de los atributos modificados según sea necesario.

Yong Yang
fuente