Decoradores con parámetros?

401

Tengo un problema con la transferencia de la variable 'insurance_mode' por el decorador. Lo haría con la siguiente declaración de decorador:

 @execute_complete_reservation(True)
 def test_booking_gta_object(self):
     self.test_select_gta_object()

pero desafortunadamente, esta afirmación no funciona. Quizás quizás haya una mejor manera de resolver este problema.

def execute_complete_reservation(test_case,insurance_mode):
    def inner_function(self,*args,**kwargs):
        self.test_create_qsf_query()
        test_case(self,*args,**kwargs)
        self.test_select_room_option()
        if insurance_mode:
            self.test_accept_insurance_crosseling()
        else:
            self.test_decline_insurance_crosseling()
        self.test_configure_pax_details()
        self.test_configure_payer_details

    return inner_function
falek.marcin
fuente
3
Su ejemplo no es sintácticamente válido. execute_complete_reservationtoma dos parámetros, pero lo está pasando uno. Los decoradores son simplemente azúcar sintáctica para envolver funciones dentro de otras funciones. Consulte docs.python.org/reference/compound_stmts.html#function para obtener la documentación completa.
Brian Clapper

Respuestas:

687

La sintaxis para los decoradores con argumentos es un poco diferente: el decorador con argumentos debe devolver una función que tomará una función y devolverá otra función. Por lo tanto, realmente debería devolver un decorador normal. Un poco confuso, ¿verdad? Lo que quiero decir es:

def decorator_factory(argument):
    def decorator(function):
        def wrapper(*args, **kwargs):
            funny_stuff()
            something_with_argument(argument)
            result = function(*args, **kwargs)
            more_funny_stuff()
            return result
        return wrapper
    return decorator

Aquí puede leer más sobre el tema: también es posible implementar esto utilizando objetos invocables y eso también se explica allí.

t.dubrownik
fuente
56
Me pregunto por qué GVR no lo implementó al pasar los parámetros como argumentos decoradores posteriores después de 'función'. 'Oye, te escuché como cierres ...', etcétera.
Michel Müller
3
> ¿Funcionaría sería el primer argumento o el último? Obviamente primero, ya que los parámetros son una lista de parámetros de longitud variable. > También es extraño que "llame" a la función con una firma diferente a la de la definición. Como señala, en realidad encajaría bastante bien: es bastante análogo a cómo se llama un método de clase. Para que quede más claro, podría tener algo como la convención decoradora (self_func, param1, ...). Pero tenga en cuenta: no estoy abogando por ningún cambio aquí, Python está demasiado lejos para eso y podemos ver cómo han funcionado los cambios importantes ...
Michel Müller
21
te olvidaste de functools.wraps MUY ÚTILES para decorar el envoltorio :)
socketpair
10
Olvidó el retorno al llamar a la función, es decir return function(*args, **kwargs)
formiaczek
36
Quizás sea obvio, pero por si acaso: debe usar este decorador como @decorator()y no solo @decorator, incluso si solo tiene argumentos opcionales.
Patrick Mevzek
327

Editar : para una comprensión profunda del modelo mental de decoradores, eche un vistazo a esto increíble Pycon Talk. Bien vale la pena los 30 minutos.

Una forma de pensar en decoradores con argumentos es

@decorator
def foo(*args, **kwargs):
    pass

se traduce en

foo = decorator(foo)

Entonces, si el decorador tenía argumentos,

@decorator_with_args(arg)
def foo(*args, **kwargs):
    pass

se traduce en

foo = decorator_with_args(arg)(foo)

decorator_with_args es una función que acepta un argumento personalizado y que devuelve el decorador real (que se aplicará a la función decorada).

Utilizo un truco simple con parciales para facilitar mis decoradores

from functools import partial

def _pseudo_decor(fun, argument):
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def foo(*args, **kwargs):
    pass

Actualizar:

Arriba, se fooconviertereal_decorator(foo)

Un efecto de decorar una función es que el nombre foose anula al declarar el decorador. fooes "anulado" por lo que sea devuelto porreal_decorator . En este caso, un nuevo objeto de función.

Se fooanulan todos los metadatos, especialmente la cadena de documentos y el nombre de la función.

>>> print(foo)
<function _pseudo_decor.<locals>.ret_fun at 0x10666a2f0>

functools.wraps nos brinda un método conveniente para "levantar" la cadena de documentos y el nombre de la función devuelta.

from functools import partial, wraps

def _pseudo_decor(fun, argument):
    # magic sauce to lift the name and doc of the function
    @wraps(fun)
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def bar(*args, **kwargs):
    pass

>>> print(bar)
<function __main__.bar(*args, **kwargs)>
srj
fuente
44
Su respuesta explicó perfectamente la ortogonalidad inherente del decorador, gracias
zsf222
¿Podrías agregar @functools.wraps?
Mr_and_Mrs_D
1
@Mr_and_Mrs_D, he actualizado la publicación con un ejemplo con functool.wraps. Agregarlo en el ejemplo puede confundir aún más a los lectores.
srj
77
¿Qué hay argaquí?
mostrar
1
¿Cómo pasarás el argumento pasado al barargumento de real_decorator?
Chang Zhao
85

Me gustaría mostrar una idea que en mi humilde opinión es bastante elegante. La solución propuesta por t.dubrownik muestra un patrón que siempre es el mismo: necesita el envoltorio de tres capas independientemente de lo que haga el decorador.

Entonces pensé que este es un trabajo para un meta-decorador, es decir, un decorador para decoradores. Como un decorador es una función, en realidad funciona como un decorador regular con argumentos:

def parametrized(dec):
    def layer(*args, **kwargs):
        def repl(f):
            return dec(f, *args, **kwargs)
        return repl
    return layer

Esto se puede aplicar a un decorador regular para agregar parámetros. Entonces, por ejemplo, digamos que tenemos el decorador que duplica el resultado de una función:

def double(f):
    def aux(*xs, **kws):
        return 2 * f(*xs, **kws)
    return aux

@double
def function(a):
    return 10 + a

print function(3)    # Prints 26, namely 2 * (10 + 3)

Con @parametrizedpodemos construir un @multiplydecorador genérico que tenga un parámetro

@parametrized
def multiply(f, n):
    def aux(*xs, **kws):
        return n * f(*xs, **kws)
    return aux

@multiply(2)
def function(a):
    return 10 + a

print function(3)    # Prints 26

@multiply(3)
def function_again(a):
    return 10 + a

print function(3)          # Keeps printing 26
print function_again(3)    # Prints 39, namely 3 * (10 + 3)

Convencionalmente, el primer parámetro de un parametrizado decorador es la función, mientras que los argumentos restantes corresponderán al parámetro del decorador parametrizado.

Un ejemplo de uso interesante podría ser un decorador asertivo de tipo seguro:

import itertools as it

@parametrized
def types(f, *types):
    def rep(*args):
        for a, t, n in zip(args, types, it.count()):
            if type(a) is not t:
                raise TypeError('Value %d has not type %s. %s instead' %
                    (n, t, type(a))
                )
        return f(*args)
    return rep

@types(str, int)  # arg1 is str, arg2 is int
def string_multiply(text, times):
    return text * times

print(string_multiply('hello', 3))    # Prints hellohellohello
print(string_multiply(3, 3))          # Fails miserably with TypeError

Una nota final: aquí no estoy usando functools.wrapspara las funciones de contenedor, pero recomendaría usarlo todo el tiempo.

Dacav
fuente
3
No usé esto exactamente, pero me ayudó a entender el concepto :) ¡Gracias!
mouckatron
Intenté esto y tuve algunos problemas .
Jeff
@ Jeff, ¿podría compartir con nosotros el tipo de problemas que tuvo?
Dacav
Lo tenía vinculado a mi pregunta, y lo resolví ... Tenía que llamar @wrapsal mío para mi caso particular.
Jeff
44
Oh chico, perdí un día entero en esto. Afortunadamente, encontré esta respuesta (que, por cierto, podría ser la mejor respuesta jamás creada en Internet). Ellos también usan tu @parametrizedtruco. El problema que tuve fue que olvidé que la @sintaxis es igual a las llamadas reales (de alguna manera lo sabía y no lo sabía al mismo tiempo que se puede deducir de mi pregunta). Entonces, si desea traducir la @sintaxis en llamadas mundanas para verificar cómo funciona, es mejor que coméntelo temporalmente primero o termine llamándolo dos veces y obteniendo resultados
mumbojumbo
79

Aquí hay una versión ligeramente modificada de la respuesta de t.dubrownik . ¿Por qué?

  1. Como plantilla general, debe devolver el valor de retorno de la función original.
  2. Esto cambia el nombre de la función, lo que podría afectar a otros decoradores / código.

Entonces usa @functools.wraps():

from functools import wraps

def decorator(argument):
    def real_decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            funny_stuff()
            something_with_argument(argument)
            retval = function(*args, **kwargs)
            more_funny_stuff()
            return retval
        return wrapper
    return real_decorator
Ross R
fuente
37

Supongo que su problema es pasar argumentos a su decorador. Esto es un poco complicado y no es sencillo.

Aquí hay un ejemplo de cómo hacer esto:

class MyDec(object):
    def __init__(self,flag):
        self.flag = flag
    def __call__(self, original_func):
        decorator_self = self
        def wrappee( *args, **kwargs):
            print 'in decorator before wrapee with flag ',decorator_self.flag
            original_func(*args,**kwargs)
            print 'in decorator after wrapee with flag ',decorator_self.flag
        return wrappee

@MyDec('foo de fa fa')
def bar(a,b,c):
    print 'in bar',a,b,c

bar('x','y','z')

Huellas dactilares:

in decorator before wrapee with flag  foo de fa fa
in bar x y z
in decorator after wrapee with flag  foo de fa fa

Vea el artículo de Bruce Eckel para más detalles.

Ross Rogers
fuente
20
Cuidado con las clases de decorador. No funcionan en métodos a menos que reinvente manualmente la lógica de los descriptores de método de instancia.
99
Delnan, ¿quieres elaborar? Solo he tenido que usar este patrón una vez, por lo que aún no he alcanzado ninguno de los escollos.
Ross Rogers
2
@RossRogers Supongo que @delnan se refiere a cosas como las __name__que no tendrá una instancia de la clase de decorador.
jamesc
99
@jamesc Eso también, aunque eso es relativamente fácil de resolver. El caso específico al que me refería era class Foo: @MyDec(...) def method(self, ...): blahque no funciona porque Foo().methodno será un método vinculado y no se aprobará selfautomáticamente. Esto también se puede solucionar haciendo MyDecun descriptor y creando métodos enlazados __get__, pero es más complicado y mucho menos obvio. Al final, las clases de decorador no son tan convenientes como parecen.
2
@delnan Me gustaría ver esta advertencia más destacada. Lo estoy golpeando y estoy interesado en ver una solución que sí funcione (más complicada aunque menos obvia).
HaPsantran
12
def decorator(argument):
    def real_decorator(function):
        def wrapper(*args):
            for arg in args:
                assert type(arg)==int,f'{arg} is not an interger'
            result = function(*args)
            result = result*argument
            return result
        return wrapper
    return real_decorator

Uso del decorador

@decorator(2)
def adder(*args):
    sum=0
    for i in args:
        sum+=i
    return sum

Entonces el

adder(2,3)

produce

10

pero

adder('hi',3)

produce

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-143-242a8feb1cc4> in <module>
----> 1 adder('hi',3)

<ipython-input-140-d3420c248ebd> in wrapper(*args)
      3         def wrapper(*args):
      4             for arg in args:
----> 5                 assert type(arg)==int,f'{arg} is not an interger'
      6             result = function(*args)
      7             result = result*argument

AssertionError: hi is not an interger
Gajendra D Ambi
fuente
8

Esta es una plantilla para un decorador de funciones que no requiere ()si no se dan parámetros:

import functools


def decorator(x_or_func=None, *decorator_args, **decorator_kws):
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kws):
            if 'x_or_func' not in locals() \
                    or callable(x_or_func) \
                    or x_or_func is None:
                x = ...  # <-- default `x` value
            else:
                x = x_or_func
            return func(*args, **kws)

        return wrapper

    return _decorator(x_or_func) if callable(x_or_func) else _decorator

Un ejemplo de esto se da a continuación:

def multiplying(factor_or_func=None):
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if 'factor_or_func' not in locals() \
                    or callable(factor_or_func) \
                    or factor_or_func is None:
                factor = 1
            else:
                factor = factor_or_func
            return factor * func(*args, **kwargs)
        return wrapper
    return _decorator(factor_or_func) if callable(factor_or_func) else _decorator


@multiplying
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying()
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying(10)
def summing(x): return sum(x)

print(summing(range(10)))
# 450
norok2
fuente
Nótese también que factor_or_func(o cualquier otro parámetro) nunca debe se reasigna en wrapper().
norok2
1
¿Por qué necesitas registrarte locals()?
Shital Shah
@ShitalShah que cubre el caso donde se usa el decorador sin ().
norok2
4

En mi caso, decidí resolver esto mediante una lambda de una línea para crear una nueva función de decorador:

def finished_message(function, message="Finished!"):

    def wrapper(*args, **kwargs):
        output = function(*args,**kwargs)
        print(message)
        return output

    return wrapper

@finished_message
def func():
    pass

my_finished_message = lambda f: finished_message(f, "All Done!")

@my_finished_message
def my_func():
    pass

if __name__ == '__main__':
    func()
    my_func()

Cuando se ejecuta, esto imprime:

Finished!
All Done!

Quizás no sea tan extensible como otras soluciones, pero funcionó para mí.

ZacBook
fuente
Esto funciona. Aunque sí, esto dificulta establecer el valor para el decorador.
Arindam Roychowdhury
3

¡Escribir un decorador que funcione con y sin parámetros es un desafío porque Python espera un comportamiento completamente diferente en estos dos casos! Muchas respuestas han tratado de solucionar esto y a continuación se muestra una mejora de la respuesta de @ norok2. Específicamente, esta variación elimina el uso delocals() .

Siguiendo el mismo ejemplo dado por @ norok2:

import functools

def multiplying(f_py=None, factor=1):
    assert callable(f_py) or f_py is None
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return factor * func(*args, **kwargs)
        return wrapper
    return _decorator(f_py) if callable(f_py) else _decorator


@multiplying
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying()
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying(factor=10)
def summing(x): return sum(x)

print(summing(range(10)))
# 450

Juega con este código .

El problema es que el usuario debe proporcionar pares de parámetros clave y de valor en lugar de parámetros posicionales y el primer parámetro está reservado.

Shital Shah
fuente
2

Es bien sabido que las siguientes dos piezas de código son casi equivalentes:

@dec
def foo():
    pass    foo = dec(foo)

############################################
foo = dec(foo)

Un error común es pensar que @simplemente oculta el argumento más a la izquierda.

@dec(1, 2, 3)
def foo():
    pass    
###########################################
foo = dec(foo, 1, 2, 3)

Sería mucho más fácil escribir decoradores si lo anterior es cómo @funcionó. Desafortunadamente, esa no es la forma en que se hacen las cosas.


Considere un decorador Waitque frena la ejecución del programa durante unos segundos. Si no pasa un tiempo de espera, el valor predeterminado es 1 segundo. Los casos de uso se muestran a continuación.

##################################################
@Wait
def print_something(something):
    print(something)

##################################################
@Wait(3)
def print_something_else(something_else):
    print(something_else)

##################################################
@Wait(delay=3)
def print_something_else(something_else):
    print(something_else)

Cuando Waittiene un argumento, como @Wait(3), entonces la llamada Wait(3) se ejecuta antes que suceda algo más.

Es decir, las siguientes dos piezas de código son equivalentes

@Wait(3)
def print_something_else(something_else):
    print(something_else)

###############################################
return_value = Wait(3)
@return_value
def print_something_else(something_else):
    print(something_else)

Esto es un problema.

if `Wait` has no arguments:
    `Wait` is the decorator.
else: # `Wait` receives arguments
    `Wait` is not the decorator itself.
    Instead, `Wait` ***returns*** the decorator

A continuación se muestra una solución:

Comencemos creando la siguiente clase DelayedDecorator:

class DelayedDecorator:
    def __init__(i, cls, *args, **kwargs):
        print("Delayed Decorator __init__", cls, args, kwargs)
        i._cls = cls
        i._args = args
        i._kwargs = kwargs
    def __call__(i, func):
        print("Delayed Decorator __call__", func)
        if not (callable(func)):
            import io
            with io.StringIO() as ss:
                print(
                    "If only one input, input must be callable",
                    "Instead, received:",
                    repr(func),
                    sep="\n",
                    file=ss
                )
                msg = ss.getvalue()
            raise TypeError(msg)
        return i._cls(func, *i._args, **i._kwargs)

Ahora podemos escribir cosas como:

 dec = DelayedDecorator(Wait, delay=4)
 @dec
 def delayed_print(something):
    print(something)

Tenga en cuenta que:

  • dec no acepta múltiples argumentos.
  • dec solo acepta la función a envolver.

    importar inspeccionar clase PolyArgDecoratorMeta (type): def call (Wait, * args, ** kwargs): try: arg_count = len (args) if (arg_count == 1): if callable (args [0]): SuperClass = inspect. getmro (PolyArgDecoratorMeta) [1] r = Superclase. llamada (Wait, args [0]) else: r = DelayedDecorator (Wait, * args, ** kwargs) else: r = DelayedDecorator (Wait, * args, ** kwargs) finalmente: pasar retorno r

    import time class Wait (metaclass = PolyArgDecoratorMeta): def init (i, func, delay = 2): i._func = func i._delay = delay

    def __call__(i, *args, **kwargs):
        time.sleep(i._delay)
        r = i._func(*args, **kwargs)
        return r 

Las siguientes dos piezas de código son equivalentes:

@Wait
def print_something(something):
     print (something)

##################################################

def print_something(something):
    print(something)
print_something = Wait(print_something)

Podemos imprimir "something"en la consola muy lentamente, de la siguiente manera:

print_something("something")

#################################################
@Wait(delay=1)
def print_something_else(something_else):
    print(something_else)

##################################################
def print_something_else(something_else):
    print(something_else)

dd = DelayedDecorator(Wait, delay=1)
print_something_else = dd(print_something_else)

##################################################

print_something_else("something")

Notas finales

Puede parecer mucho código, pero no tiene que escribir las clases DelayedDecoratory PolyArgDecoratorMetatodo el tiempo. El único código que tiene que escribir personalmente es algo como lo siguiente, que es bastante corto:

from PolyArgDecoratorMeta import PolyArgDecoratorMeta
import time
class Wait(metaclass=PolyArgDecoratorMeta):
 def __init__(i, func, delay = 2):
     i._func = func
     i._delay = delay

 def __call__(i, *args, **kwargs):
     time.sleep(i._delay)
     r = i._func(*args, **kwargs)
     return r
Samuel Muldoon
fuente
1

defina esta "función decoradora" para generar una función decoradora personalizada:

def decoratorize(FUN, **kw):
    def foo(*args, **kws):
        return FUN(*args, **kws, **kw)
    return foo

úsalo de esta manera:

    @decoratorize(FUN, arg1 = , arg2 = , ...)
    def bar(...):
        ...
chen.wq
fuente
1

Grandes respuestas arriba. Este también ilustra @wraps, que toma la cadena de documentación y el nombre de la función de la función original y la aplica a la nueva versión envuelta:

from functools import wraps

def decorator_func_with_args(arg1, arg2):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            print("Before orginal function with decorator args:", arg1, arg2)
            result = f(*args, **kwargs)
            print("Ran after the orginal function")
            return result
        return wrapper
    return decorator

@decorator_func_with_args("foo", "bar")
def hello(name):
    """A function which prints a greeting to the name provided.
    """
    print('hello ', name)
    return 42

print("Starting script..")
x = hello('Bob')
print("The value of x is:", x)
print("The wrapped functions docstring is:", hello.__doc__)
print("The wrapped functions name is:", hello.__name__)

Huellas dactilares:

Starting script..
Before orginal function with decorator args: foo bar
hello  Bob
Ran after the orginal function
The value of x is: 42
The wrapped functions docstring is: A function which prints a greeting to the name provided.
The wrapped functions name is: hello
run_the_race
fuente
0

En caso de que tanto la función como el decorador tengan que tomar argumentos, puede seguir el siguiente enfoque.

Por ejemplo, hay un decorador llamado decorator1que toma un argumento

@decorator1(5)
def func1(arg1, arg2):
    print (arg1, arg2)

func1(1, 2)

Ahora, si el decorator1argumento tiene que ser dinámico, o pasado mientras se llama a la función,

def func1(arg1, arg2):
    print (arg1, arg2)


a = 1
b = 2
seconds = 10

decorator1(seconds)(func1)(a, b)

En el código anterior

  • seconds es el argumento para decorator1
  • a, b son los argumentos de func1
SuperNova
fuente