with pattern decorators decoradores create python decorator

pattern - python decorator with arguments



Preservar firmas de funciones decoradas (5)

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 memorando, 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 inútil.

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

1 - Incluye la firma correcta en la docstring:

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 docstring o hacer un error tipográfico. [ Y sí, soy consciente del hecho de que el docstring ya duplica el cuerpo de la función. Por favor ignora esto; funny_function es solo un ejemplo al azar. ]

2 - No use un decorador, o use un decorador 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 es inútil en general. Como dije al principio, quiero poder usar decoradores de manera completamente genérica.

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

Entonces la pregunta es: ¿hay alguna manera 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" cuando construya la función decorada? ¿Cómo extraigo esa información? ¿Cómo debería construir la función decorada con exec?

Cualquier otro enfoque?


  1. Instalar el módulo decorator :

    $ 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 lugar de x, y, z=3 .


Eche un vistazo al módulo decorador , específicamente al decorator decorador, que resuelve este problema.


Esto se resuelve con los functools biblioteca estándar de functools y específicamente la función functools.wraps , que está diseñada para " actualizar una función de envoltura para que parezca 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 decorator decorador que puede 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)

Entonces se conserva 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: JF Sebastian señaló que no modifiqué la función args_as_ints - ahora está arreglada.


Segunda opción:

  1. Instale el módulo wrapt:

$ easy_install wrapt

wrapt tiene una bonificación, preservar 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