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 ?

¿Fue útil?

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 de f en g . Esta lista predeterminada está en WRAPPER_ASSIGNMENTS , puede verla en fuente de functools .
  • actualiza el __dict__ de g con todos los elementos de f .__ dict__ . (consulte WRAPPER_UPDATES en la fuente)
  • establece un nuevo atributo __wrapped __ = f en g

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)
  1. 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.

  2. 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:

  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)

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top