Cómo crear un Python decorador, que puede ser utilizado con o sin parámetros?

Me gustaría crear un Python decorador, que puede ser utilizado ya sea con los parámetros:

@redirect_output("somewhere.log")
def foo():
    ....

o sin ellos (por ejemplo, para redirigir la salida stderr por defecto):

@redirect_output
def foo():
    ....

Es que es posible?

Tenga en cuenta que yo no estoy buscando una solución diferente al problema de la reorientación de la producción, es sólo un ejemplo de la sintaxis que me gustaría conseguir.

  • El valor predeterminado de aspecto @redirect_output es muy poco informativo. Diría que es una mala idea. El uso de la primera forma y simplificar su vida un montón.
  • interesante pregunta, aunque – hasta que me vio y miró a través de la documentación, he asumido que @f era el mismo que @f(), y todavía creo que debe ser, para ser honesto (cualquier proporcionado argumentos acaba de ser fijadas al argumento de la función)
InformationsquelleAutor elifiner | 2009-03-17

11 Kommentare

  1. 56

    Sé que esta pregunta es viejo, pero algunos de los comentarios son nuevos, y aunque todas las soluciones viables son esencialmente los mismos, la mayoría de ellos no son muy limpios o fáciles de leer.

    Como thobe la respuesta, dice, la única forma de manejar ambos casos es comprobar para ambos escenarios. La forma más fácil es simplemente para comprobar si hay un solo argumento y es callabe (NOTA: cheque extra será necesario si su decorador solo toma 1 argumento y pasa a ser un objeto invocable):

    def decorator(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # called as @decorator
        else:
            # called as @decorator(*args, **kwargs)

    En el primer caso, puedes hacer lo que cualquier normal decorador hace, de retorno modificada o ajustada versión del pasado en función.

    En el segundo caso, se puede devolver un ‘nuevo’ decorador, que de alguna manera se utiliza la información que se pasa en con *args, **kwargs.

    Esto está muy bien y todo, pero el tener que escribir para cada decorador que usted hace pueden ser bastante molesto y no es tan limpio. En cambio, sería agradable ser capaz de automágicamente modificar nuestros decoradores sin tener que volver a escribir… pero eso es lo que los decoradores son para!

    Utilizando la siguiente decorador decorador, podemos deocrate nuestros decoradores para que puedan ser utilizados con o sin argumentos:

    def doublewrap(f):
        '''
        a decorator decorator, allowing the decorator to be used as:
        @decorator(with, arguments, and=kwargs)
        or
        @decorator
        '''
        @wraps(f)
        def new_dec(*args, **kwargs):
            if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
                # actual decorated function
                return f(args[0])
            else:
                # decorator arguments
                return lambda realf: f(realf, *args, **kwargs)
    
        return new_dec

    Ahora, podemos decorar nuestros decoradores con @doublewrap, y que van a trabajar con y sin argumentos, con una salvedad:

    He señalado más arriba, pero se debe repetir aquí, la verificación de este decorador hace una suposición acerca de los argumentos que un decorador puede recibir (es decir, que puede no recibir una sola, que se puede llamar argumento). Ya que estamos por lo que es aplicable a cualquier generador de ahora, debe tenerse en cuenta, o de modificar, si va a ser contradicho.

    La siguiente muestra de su uso:

    def test_doublewrap():
        from util import doublewrap
        from functools import wraps    
    
        @doublewrap
        def mult(f, factor=2):
            '''multiply a function's return value'''
            @wraps(f)
            def wrap(*args, **kwargs):
                return factor*f(*args,**kwargs)
            return wrap
    
        # try normal
        @mult
        def f(x, y):
            return x + y
    
        # try args
        @mult(3)
        def f2(x, y):
            return x*y
    
        # try kwargs
        @mult(factor=5)
        def f3(x, y):
            return x - y
    
        assert f(2,3) == 10
        assert f2(2,5) == 30
        assert f3(8,1) == 5*7
  2. 30

    El uso de palabras clave argumentos con valores por defecto (como se sugiere por kquinn) es una buena idea, pero se requieren para incluir los paréntesis:

    @redirect_output()
    def foo():
        ...

    Si quieres una versión que funciona sin los paréntesis en el decorador tendrá en cuenta los dos escenarios en su decorador de código.

    Si estuviera usando Python 3.0, se puede utilizar la palabra clave sólo argumentos para ello:

    def redirect_output(fn=None,*,destination=None):
      destination = sys.stderr if destination is None else destination
      def wrapper(*args, **kwargs):
        ... # your code here
      if fn is None:
        def decorator(fn):
          return functools.update_wrapper(wrapper, fn)
        return decorator
      else:
        return functools.update_wrapper(wrapper, fn)

    En Python 2.x esto puede ser emulado con varargs trucos:

    def redirected_output(*fn,**options):
      destination = options.pop('destination', sys.stderr)
      if options:
        raise TypeError("unsupported keyword arguments: %s" % 
                        ",".join(options.keys()))
      def wrapper(*args, **kwargs):
        ... # your code here
      if fn:
        return functools.update_wrapper(wrapper, fn[0])
      else:
        def decorator(fn):
          return functools.update_wrapper(wrapper, fn)
        return decorator

    Cualquiera de estas versiones le permiten escribir código como este:

    @redirected_output
    def foo():
        ...
    
    @redirected_output(destination="somewhere.log")
    def bar():
        ...
    • Lo que se pone en your code here? ¿Cómo se llama a la función que está decorado? fn(*args, **kwargs) no funciona.
  3. 12

    Usted necesita para detectar ambos casos, por ejemplo, utilizando el tipo del primer argumento, y, en consecuencia, devolver el contenedor (cuando se utiliza sin parámetros) o un decorador (cuando se utiliza con argumentos).

    from functools import wraps
    import inspect
    
    def redirect_output(fn_or_output):
        def decorator(fn):
            @wraps(fn)
            def wrapper(*args, **args):
                # Redirect output
                try:
                    return fn(*args, **args)
                finally:
                    # Restore output
            return wrapper
    
        if inspect.isfunction(fn_or_output):
            # Called with no parameter
            return decorator(fn_or_output)
        else:
            # Called with a parameter
            return decorator

    Cuando se utiliza el @redirect_output("output.log") sintaxis, redirect_output se llama con un argumento único "output.log", y debe devolver un decorador de la aceptación de la función a ser decorado como un argumento. Cuando se utiliza como @redirect_output, es llamado directamente con la función de ser decorado como un argumento.

    O en otras palabras: la @ sintaxis debe ser seguido por una expresión cuyo resultado es una función de la aceptación de una función para ser decorado como su único argumento, y el regreso de la función decorado. La expresión de sí mismo puede ser una llamada a una función, que es el caso con @redirect_output("output.log"). Complicada, pero cierto 🙂

  4. 9

    Sé que esto es una vieja pregunta, pero en realidad no me gusta ninguna de las técnicas propuestas para que yo quería añadir otro método. Vi que django utiliza un muy limpio método en sus login_required decorador en django.contrib.auth.decoradores. Como se puede ver en la del decorador docs, puede ser utilizado solo como @login_required o con argumentos, @login_required(redirect_field_name='my_redirect_field').

    La manera en que lo hacen es bastante simple. Agregar un kwarg (function=None) antes de su decorador de argumentos. Si el decorador se utiliza solo, function será la función real es la decoración, mientras que si es llamado con argumentos, function será None.

    Ejemplo:

    from functools import wraps
    
    def custom_decorator(function=None, some_arg=None, some_other_arg=None):
        def actual_decorator(f):
            @wraps(f)
            def wrapper(*args, **kwargs):
                # Do stuff with args here...
                if some_arg:
                    print(some_arg)
                if some_other_arg:
                    print(some_other_arg)
                return f(*args, **kwargs)
            return wrapper
        if function:
            return actual_decorator(function)
        return actual_decorator

    @custom_decorator
    def test1():
        print('test1')
    
    >>> test1()
    test1

    @custom_decorator(some_arg='hello')
    def test2():
        print('test2')
    
    >>> test2()
    hello
    test2

    @custom_decorator(some_arg='hello', some_other_arg='world')
    def test3():
        print('test3')
    
    >>> test3()
    hello
    world
    test3

    Me parece que este enfoque que django utiliza para ser más elegante y más fácil de entender que cualquiera de las otras técnicas que aquí se propone.

  5. 8

    Python decorador se llama de una forma fundamentalmente diferente dependiendo de si la vas a dar argumentos o no. La decoración es en realidad sólo una (sintácticamente restringido) de la expresión.

    En su primer ejemplo:

    @redirect_output("somewhere.log")
    def foo():
        ....

    la función redirect_output se llama con el
    el argumento dado, que se espera que devuelva un decorador
    la función, que en sí se llama con foo como un argumento,
    que (¡por fin!) se espera que regrese el decorado final de la función.

    El código equivalente se parece a esto:

    def foo():
        ....
    d = redirect_output("somewhere.log")
    foo = d(foo)

    El código equivalente para el segundo ejemplo se ve como:

    def foo():
        ....
    d = redirect_output
    foo = d(foo)

    Para que puede hacer lo que quieras, pero no de una manera totalmente transparente:

    import types
    def redirect_output(arg):
        def decorator(file, f):
            def df(*args, **kwargs):
                print 'redirecting to ', file
                return f(*args, **kwargs)
            return df
        if type(arg) is types.FunctionType:
            return decorator(sys.stderr, arg)
        return lambda f: decorator(arg, f)

    Este debe estar bien a menos que usted desea utilizar una función como
    argumento a su decorador, en cuyo caso el decorador
    se cree erróneamente que no tiene argumentos. También fallará
    si esta decoración se aplica a otra decoración que
    no devuelve un tipo de función.

    Un método alternativo es sólo para exigir que la
    decorador de la función es llamada, incluso si es con argumentos.
    En este caso, el segundo ejemplo sería así:

    @redirect_output()
    def foo():
        ....

    El decorador código de la función tendría este aspecto:

    def redirect_output(file = sys.stderr):
        def decorator(file, f):
            def df(*args, **kwargs):
                print 'redirecting to ', file
                return f(*args, **kwargs)
            return df
        return lambda f: decorator(file, f)
  6. 4

    Varias respuestas aquí ya frente a su problema muy bien. Con respecto al estilo, sin embargo, yo prefiero la solución de este decorador situación mediante functools.partial, como sugiere David Beazley del Python libro de cocina de 3:

    from functools import partial, wraps
    
    def decorator(func=None, foo='spam'):
        if func is None:
             return partial(decorator, foo=foo)
    
        @wraps(func)
        def wrapper(*args, **kwargs):
            # do something with `func` and `foo`, if you're so inclined
            pass
    
        return wrapper

    Si bien sí, usted puede hacer

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

    sin funky soluciones, me parece extraño, y me gusta tener la opción de simplemente decorar con @decorator.

    Como para el segundo objetivo de la misión, la redirección de una función de salida es abordado en este Desbordamiento de pila post.


    Si quieres profundizar más, consulte el Capítulo 9 (Metaprogramación) en Python libro de cocina de 3, que está disponible gratuitamente para ser leer online.

    Algunos de los que el material se hizo una demostración en vivo (y más!) en Beazley impresionante vídeo de YouTube Python 3 Metaprogramación.

    Feliz de codificación 🙂

  7. 2

    De hecho, la advertencia de que en caso @bj0 la solución puede ser comprobado fácilmente:

    def meta_wrap(decor):
        @functools.wraps(decor)
        def new_decor(*args, **kwargs):
            if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
                # this is the double-decorated f. 
                # Its first argument should not be a callable
                doubled_f = decor(args[0])
                @functools.wraps(doubled_f)
                def checked_doubled_f(*f_args, **f_kwargs):
                    if callable(f_args[0]):
                        raise ValueError('meta_wrap failure: '
                                    'first positional argument cannot be callable.')
                    return doubled_f(*f_args, **f_kwargs)
                return checked_doubled_f 
            else:
                # decorator arguments
                return lambda real_f: decor(real_f, *args, **kwargs)
    
        return new_decor

    Aquí hay un par de casos de prueba para esta variante de seguridad de meta_wrap.

        @meta_wrap
        def baddecor(f, caller=lambda x: -1*x):
            @functools.wraps(f)
            def _f(*args, **kwargs):
                return caller(f(args[0]))
            return _f
    
        @baddecor  # used without arg: no problem
        def f_call1(x):
            return x + 1
        assert f_call1(5) == -6
    
        @baddecor(lambda x : 2*x) # bad case
        def f_call2(x):
            return x + 1
        f_call2(5)  # raises ValueError
    
        # explicit keyword: no problem
        @baddecor(caller=lambda x : 100*x)
        def f_call3(x):
            return x + 1
        assert f_call3(5) == 600
    • Gracias. Esto es útil!
  8. 0

    Para dar una respuesta más completa que la anterior:

    «¿Hay una manera de construir un decorador, que puede ser utilizado tanto con y sin argumentos ?»

    No no hay manera genérica, porque actualmente hay algo que falta en el lenguaje python para detectar las dos diferentes casos de uso.

    Sin embargo como ya se ha señalado con otras respuestas, tales como bj0s, hay un alternativa que es para comprobar el tipo y el valor del primer argumento posicional recibido (y para comprobar si no hay otros argumentos que no tienen valor por defecto). Si tienes la garantía de que los usuarios nunca pasar un exigible como primer argumento de su decorador, entonces usted puede utilizar esta solución. Tenga en cuenta que este es el mismo para la clase de los decoradores (reemplazar exigible según la clase en la frase anterior).

    Para estar seguro de lo anterior, hice un poco de investigación por ahí y hasta implementado una biblioteca denominada decopatch que utiliza una combinación de todas las estrategias anteriormente citadas (y muchos más, incluyendo la introspección) para llevar a cabo «lo que es el más inteligente solución» dependiendo de su necesidad.

    Pero, francamente, lo mejor sería no necesitar de cualquier biblioteca de aquí y para conseguir que la función directamente desde el lenguaje python. Si, como yo, piensan que es una lástima que el lenguaje python no es como el de hoy es capaz de ofrecer una cuidada respuesta a esta pregunta, no dude en apoyo de esta idea en el python bugtracker: https://bugs.python.org/issue36553 !

    Muchas gracias por su ayuda para hacer de python un mejor lenguaje 🙂

  9. -1

    ¿Has probado palabra clave argumentos con valores por defecto? Algo así como

    def decorate_something(foo=bar, baz=quux):
        pass
  10. -2

    Generalmente puede dar argumentos predeterminados en Python…

    def redirect_output(fn, output = stderr):
        # whatever

    No estoy seguro si funciona con decoradores así, sin embargo. No conozco ninguna razón por qué no.

    • Si usted dice @dec(abc) la función no se pasa directamente a dic. dec(abc) devuelve algo, y este valor es utilizado como el decorador. Así que dec(abc) tiene que devolver una función, que a continuación se presenta la decoración de la función que se pasa como un parámetro. (Ver también thobes código)
  11. -2

    Edificio en vartec la respuesta:

    imports sys
    
    def redirect_output(func, output=None):
        if output is None:
            output = sys.stderr
        if isinstance(output, basestring):
            output = open(output, 'w') # etc...
        # everything else...
    • esto no puede ser utilizado como un decorador como en el @redirect_output("somewhere.log") def foo() ejemplo en la pregunta.

Kommentieren Sie den Artikel

Bitte geben Sie Ihren Kommentar ein!
Bitte geben Sie hier Ihren Namen ein

Pruebas en línea