¿Cómo crear un decorador de Python que se pueda usar con o sin parámetros?

88

Me gustaría crear un decorador de Python que se pueda usar con parámetros:

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

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

@redirect_output
def foo():
    ....

¿Es eso posible?

Tenga en cuenta que no estoy buscando una solución diferente al problema de redirigir la salida, es solo un ejemplo de la sintaxis que me gustaría lograr.

elifiner
fuente
El aspecto predeterminado @redirect_outputes notablemente poco informativo. Sugeriría que es una mala idea. Usa la primera forma y simplifica mucho tu vida.
S.Lott
Sin embargo, una pregunta interesante: hasta que la vi y revisé la documentación, habría asumido que @f era lo mismo que @f (), y todavía creo que debería serlo, para ser honesto (cualquier argumento proporcionado simplemente se agregaría en el argumento de la función)
rog

Respuestas:

63

Sé que esta pregunta es antigua, pero algunos de los comentarios son nuevos, y si bien todas las soluciones viables son esencialmente las mismas, la mayoría de ellas no son muy limpias ni fáciles de leer.

Como dice la respuesta de thobe, la única forma de manejar ambos casos es verificar ambos escenarios. La forma más fácil es simplemente verificar si hay un solo argumento y es callabe (NOTA: serán necesarias verificaciones adicionales si su decorador solo toma 1 argumento y resulta 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, haces lo que hace cualquier decorador normal, devuelve una versión modificada o envuelta de la función pasada.

En el segundo caso, devuelve un decorador 'nuevo' que de alguna manera usa la información pasada con * args, ** kwargs.

Esto está bien y todo, pero tener que escribirlo para cada decorador que hagas puede ser bastante molesto y no tan limpio. En cambio, sería bueno poder modificar automágicamente nuestros decoradores sin tener que volver a escribirlos ... ¡pero para eso están los decoradores!

Usando el siguiente decorador decorador, podemos decorar nuestros decoradores para que puedan usarse 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 a nuestros decoradores con @doublewrap, y trabajarán con y sin argumentos, con una advertencia:

Lo señalé anteriormente, pero debería repetir aquí, la verificación en este decorador hace una suposición sobre los argumentos que un decorador puede recibir (es decir, que no puede recibir un solo argumento invocable). Dado que ahora lo estamos haciendo aplicable a cualquier generador, debe tenerse en cuenta o modificarse si se contradice.

Lo siguiente demuestra 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
bj0
fuente
31

Usar argumentos de palabras clave con valores predeterminados (como sugiere kquinn) es una buena idea, pero requerirá que incluya el paréntesis:

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

Si desea una versión que funcione sin el paréntesis en el decorador, tendrá que tener en cuenta ambos escenarios en su código de decorador.

Si estuviera usando Python 3.0, podría usar argumentos de solo palabras clave para esto:

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 se puede emular con trucos de varargs:

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 permitiría escribir código como este:

@redirected_output
def foo():
    ...

@redirected_output(destination="somewhere.log")
def bar():
    ...
thobe
fuente
1
¿Qué pones your code here? ¿Cómo se llama a la función que está decorada? fn(*args, **kwargs)no funciona.
lum
Creo que hay una respuesta mucho más simple, crea una clase que será el decorador con argumentos opcionales. crea otra función con los mismos argumentos con valores predeterminados y devuelve una nueva instancia de las clases de decorador. debería verse algo como: def f(a = 5): return MyDecorator( a = a) y lo class MyDecorator( object ): def __init__( self, a = 5 ): .... siento, es difícil escribirlo en un comentario, pero espero que sea lo suficientemente simple de entender
Omer Ben Haim
17

Sé que esta es una pregunta antigua, pero realmente no me gusta ninguna de las técnicas propuestas, así que quería agregar otro método. Vi que django usa un método realmente limpio en su login_requireddecorador endjango.contrib.auth.decorators . Como se puede ver en la documentación del decorador , que puede ser utilizado solo como @login_requiredo con argumentos, @login_required(redirect_field_name='my_redirect_field').

La forma en que lo hacen es bastante simple. Añaden un kwarg( function=None) antes de sus argumentos de decorador. Si el decorador se usa solo, functionserá la función real que está decorando, mientras que si se llama con argumentos, functionserá 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

Encuentro que este enfoque que usa django es más elegante y más fácil de entender que cualquiera de las otras técnicas propuestas aquí.

dgel
fuente
Sí, me gusta este método. Ten en cuenta que usted tiene que utilizar kwargs al llamar al decorador de lo contrario el primer argumento posicional se asigna a functiony luego las cosas se rompen debido a los intentos decorador para llamar a ese primer argumento posicional como si fuera su función decorado.
Dustin Wyatt
12

Debe detectar ambos casos, por ejemplo, utilizando el tipo del primer argumento y, en consecuencia, devolver el contenedor (cuando se usa sin parámetro) o un decorador (cuando se usa 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 usa la @redirect_output("output.log")sintaxis, redirect_outputse llama con un solo argumento "output.log"y debe devolver un decorador que acepte la función que se decorará como argumento. Cuando se usa como @redirect_output, se llama directamente con la función a decorar como argumento.

O en otras palabras: la @sintaxis debe ir seguida de una expresión cuyo resultado sea una función que acepta una función a decorar como su único argumento y devuelve la función decorada. La expresión en sí misma puede ser una llamada a función, que es el caso de @redirect_output("output.log"). Enrevesado, pero cierto :-)

Remy en blanco
fuente
9

Varias respuestas aquí ya abordan su problema muy bien. Sin embargo, con respecto al estilo, prefiero resolver este problema del decorador usando functools.partial, como se sugiere en el Python Cookbook 3 de David Beazley :

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í, puedes hacer

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

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

En cuanto al objetivo de la misión secundaria, la redirección de la salida de una función se aborda en esta publicación de Stack Overflow .


Si desea profundizar más, consulte el Capítulo 9 (Metaprogramación) en Python Cookbook 3 , que está disponible gratuitamente para leer en línea .

Parte de ese material se muestra en vivo (¡y más!) En el increíble video de YouTube Python 3 Metaprogramming de Beazley .

Feliz codificación :)

Henrywallace
fuente
8

Un decorador de Python se llama de una manera fundamentalmente diferente dependiendo de si le da argumentos o no. La decoración es en realidad solo una expresión (sintácticamente restringida).

En tu primer ejemplo:

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

la función redirect_outputse llama con el argumento dado, que se espera que devuelva una función decoradora, que a su vez se llama con fooun argumento, que (¡finalmente!) se espera que devuelva la función decorada final.

El código equivalente se ve así:

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

El código equivalente para su segundo ejemplo se ve así:

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

Entonces tu puedes hacer lo que quiera, pero no de una manera totalmente fluida:

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)

Esto debería estar bien a menos que desee utilizar una función como argumento para su decorador, en cuyo caso el decorador asumirá 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 simplemente requerir que siempre se llame a la función decoradora, incluso si no tiene argumentos. En este caso, su segundo ejemplo se vería así:

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

El código de la función decoradora se vería así:

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)
rog
fuente
2

De hecho, el caso de advertencia en la solución de @ bj0 se puede verificar 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

A continuación, se muestran algunos casos de prueba para esta versión a prueba de fallas 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
Ainz Titor
fuente
1
Gracias. ¡Esto es útil!
Pragy Agarwal
0

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

"¿Hay alguna forma de construir un decorador que se pueda usar tanto con argumentos como sin ellos?"

No, no hay una forma genérica porque actualmente falta algo en el lenguaje Python para detectar los dos casos de uso diferentes.

Sin embargo, sí, como ya se señaló en otras respuestas como bj0s , hay una solución torpe que es verificar el tipo y el valor del primer argumento posicional recibido (y verificar si ningún otro argumento tiene un valor no predeterminado). Si se le garantiza que los usuarios nunca pasarán un invocable como primer argumento de su decorador, puede utilizar esta solución. Tenga en cuenta que esto es lo mismo para los decoradores de clases (reemplace invocables por clase en la oración anterior).

Para estar seguro de lo anterior, investigué bastante e incluso implementé una biblioteca nombrada decopatchque usa una combinación de todas las estrategias citadas anteriormente (y muchas más, incluida la introspección) para realizar "la solución alternativa más inteligente" según en su necesidad.

Pero, francamente, lo mejor sería no necesitar ninguna biblioteca aquí y obtener esa característica directamente del lenguaje Python. Si, como yo, cree que es una lástima que el lenguaje Python no sea capaz de proporcionar una respuesta clara a esta pregunta, no dude en apoyar esta idea en el seguimiento de errores de Python : https: //bugs.python .org / issue36553 !

Muchas gracias por su ayuda para hacer de Python un mejor lenguaje :)

smarie
fuente
0

Esto hace el trabajo sin ningún problema:

from functools import wraps

def memoize(fn=None, hours=48.0):
  def deco(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
      return fn(*args, **kwargs)
    return wrapper

  if callable(fn): return deco(fn)
  return deco
j4hangir
fuente
0

Como nadie mencionó esto, también hay una solución que utiliza la clase invocable que encuentro más elegante, especialmente en los casos en que el decorador es complejo y uno puede desear dividirlo en múltiples métodos (funciones). Esta solución utiliza un __new__método mágico para hacer esencialmente lo que otros han señalado. Primero detecte cómo se usó el decorador y luego ajuste el retorno de manera apropiada.

class decorator_with_arguments(object):

    def __new__(cls, decorated_function=None, **kwargs):

        self = super().__new__(cls)
        self._init(**kwargs)

        if not decorated_function:
            return self
        else:
            return self.__call__(decorated_function)

    def _init(self, arg1="default", arg2="default", arg3="default"):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

    def __call__(self, decorated_function):

        def wrapped_f(*args):
            print("Decorator arguments:", self.arg1, self.arg2, self.arg3)
            print("decorated_function arguments:", *args)
            decorated_function(*args)

        return wrapped_f

@decorator_with_arguments(arg1=5)
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments()
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

Si el decorador se usa con argumentos, esto equivale a:

result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)

Se puede ver que los argumentos arg1se pasan correctamente al constructor y la función decorada se pasa a__call__

Pero si el decorador se usa sin argumentos, esto equivale a:

result = decorator_with_arguments(sayHello)(a1, a2, a3, a4)

Verá que en este caso la función decorada se pasa directamente al constructor y la llamada a __call__se omite por completo. Es por eso que necesitamos emplear la lógica para encargarnos de este caso en el __new__método mágico.

¿Por qué no podemos usar en __init__lugar de __new__? La razón es simple: Python prohíbe devolver cualquier otro valor que Ninguno de__init__

ADVERTENCIA

Este enfoque tiene un efecto secundario. ¡No conservará la firma de la función!

Majo
fuente
-1

¿Ha probado argumentos de palabras clave con valores predeterminados? Algo como

def decorate_something(foo=bar, baz=quux):
    pass
kquinn
fuente
-2

Generalmente puede dar argumentos predeterminados en Python ...

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

Sin embargo, no estoy seguro de si eso funciona con los decoradores. No conozco ninguna razón por la que no lo haría.

David Z
fuente
2
Si dice @dec (abc), la función no se pasa directamente a dec. dec (abc) devuelve algo, y este valor devuelto se usa como decorador. Entonces dec (abc) tiene que devolver una función, que luego obtiene la función decorada pasada como parámetro. (Ver también el código thobes)
sth
-2

Sobre la base de la respuesta de vartec:

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...
muhuk
fuente
esto no se puede usar como decorador como en el @redirect_output("somewhere.log") def foo()ejemplo de la pregunta.
ehabkost