Pregunta

Supongamos que he escrito un decorador que hace algo muy genérico. Por ejemplo, podría convertir todos los argumentos a un tipo específico, realizar el registro, implementar la memorización, etc.

Aquí hay un ejemplo:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

>>> funny_function("3", 4.0, z="5")
22

Todo bien hasta ahora. Hay un problema, sin embargo. La función decorada no conserva la documentación de la función original:

>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

Afortunadamente, hay una solución:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

Esta vez, el nombre de la función y la documentación son correctos:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

Pero todavía hay un problema: la firma de la función es incorrecta. La información " * args, ** kwargs " es casi inútil.

¿Qué hacer? Puedo pensar en dos soluciones simples pero defectuosas:

1: incluya la firma correcta en la cadena de documentación:

def funny_function(x, y, z=3):
    """funny_function(x, y, z=3) -- computes x*y + 2*z"""
    return x*y + 2*z

Esto es malo debido a la duplicación. La firma aún no se mostrará correctamente en la documentación generada automáticamente. Es fácil actualizar la función y olvidarse de cambiar la cadena de documentación o hacer un error tipográfico. [ Y sí, soy consciente del hecho de que la cadena de documentación ya duplica el cuerpo de la función. Por favor ignora esto; funny_function es solo un ejemplo aleatorio. ]

2 - No use un decorador, o use un decorador de propósito especial para cada firma específica:

def funny_functions_decorator(f):
    def g(x, y, z=3):
        return f(int(x), int(y), z=int(z))
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

Esto funciona bien para un conjunto de funciones que tienen una firma idéntica, pero en general es inútil. Como dije al principio, quiero poder usar decoradores completamente genéricos.

Estoy buscando una solución que sea completamente general y automática.

Entonces la pregunta es: ¿hay alguna forma de editar la firma de la función decorada después de que se haya creado?

De lo contrario, ¿puedo escribir un decorador que extraiga la firma de la función y use esa información en lugar de " * kwargs, ** kwargs " ¿Cuándo construir la función decorada? ¿Cómo extraigo esa información? ¿Cómo debo construir la función decorada - con exec?

¿Algún otro enfoque?

¿Fue útil?

Solución

  1. Instalar decorador :

    $ pip install decorator
    
  2. Adapte la definición de args_as_ints () :

    import decorator
    
    @decorator.decorator
    def args_as_ints(f, *args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    
    @args_as_ints
    def funny_function(x, y, z=3):
        """Computes x*y + 2*z"""
        return x*y + 2*z
    
    print funny_function("3", 4.0, z="5")
    # 22
    help(funny_function)
    # Help on function funny_function in module __main__:
    # 
    # funny_function(x, y, z=3)
    #     Computes x*y + 2*z
    

Python 3.4+

functools.wraps () de stdlib conserva firmas desde Python 3.4:

import functools


def args_as_ints(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

functools.wraps () está disponible al menos desde Python 2.5 pero no conserva la firma allí:

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(*args, **kwargs)
#    Computes x*y + 2*z

Aviso: * args, ** kwargs en lugar de x, y, z = 3 .

Otros consejos

Esto se resuelve con la biblioteca estándar de Python functools y específicamente functools.wraps , que está diseñada para " actualizar una función de envoltura para que se parezca a la función envuelta " ;. Su comportamiento depende de la versión de Python, sin embargo, como se muestra a continuación. Aplicado al ejemplo de la pregunta, el código se vería así:

from functools import wraps

def args_as_ints(f):
    @wraps(f) 
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

Cuando se ejecuta en Python 3, esto produciría lo siguiente:

>>> funny_function("3", 4.0, z="5")
22
>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

Su único inconveniente es que en Python 2, sin embargo, no actualiza la lista de argumentos de la función. Cuando se ejecuta en Python 2, producirá:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

Hay un módulo decorador con decorador decorador que puedes usar:

@decorator
def args_as_ints(f, *args, **kwargs):
    args = [int(x) for x in args]
    kwargs = dict((k, int(v)) for k, v in kwargs.items())
    return f(*args, **kwargs)

Luego se conservan la firma y la ayuda del método:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

EDITAR: J. F. Sebastian señaló que no modifiqué la función args_as_ints ; ahora está arreglada.

Eche un vistazo al módulo decorator , específicamente el < a href = "http://www.phyast.pitt.edu/~micheles/python/documentation.html#decorator-is-a-decorator" rel = "noreferrer"> decorador decorador, que resuelve este problema .

Segunda opción:

  1. Instalar módulo wrapt:

$ easy_install wrapt

wrapt tiene un bono, preserva la firma de la clase.


import wrapt
import inspect

@wrapt.decorator def args_as_ints(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z >>> funny_function(3, 4, z=5)) # 22 >>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*z

Como se comentó anteriormente en la respuesta de jfs ; si le preocupa la firma en términos de apariencia ( help e inspect.signature ), entonces usar functools.wraps está perfectamente bien.

Si le preocupa la firma en términos de comportamiento (en particular TypeError en caso de discrepancia de argumentos), functools.wraps no la conserva. Debería usar decorator para eso, o mi generalización de su motor central, llamado makefun .

from makefun import wraps

def args_as_ints(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("wrapper executes")
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# wrapper executes
# 22

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

funny_function(0)  
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)

Consulte también esta publicación sobre functools.wraps .

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