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.
@redirect_output
es notablemente poco informativo. Sugeriría que es una mala idea. Usa la primera forma y simplifica mucho tu vida.Respuestas:
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
fuente
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(): ...
fuente
your code here
? ¿Cómo se llama a la función que está decorada?fn(*args, **kwargs)
no funciona.def f(a = 5): return MyDecorator( a = a)
y loclass MyDecorator( object ): def __init__( self, a = 5 ): ....
siento, es difícil escribirlo en un comentario, pero espero que sea lo suficientemente simple de entenderSé 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_required
decorador endjango.contrib.auth.decorators
. Como se puede ver en la documentación del decorador , que puede ser utilizado solo como@login_required
o 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,function
será la función real que está decorando, mientras que si se llama 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
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í.
fuente
function
y luego las cosas se rompen debido a los intentos decorador para llamar a ese primer argumento posicional como si fuera su función decorado.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_output
se 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 :-)fuente
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 :)
fuente
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_output
se llama con el argumento dado, que se espera que devuelva una función decoradora, que a su vez se llama confoo
un 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)
fuente
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
fuente
Para dar una respuesta más completa que la anterior:
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
bj0
s , 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
decopatch
que 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 :)
fuente
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
fuente
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
arg1
se pasan correctamente al constructor y la función decorada se pasa a__call__
Pero si el decorador se usa sin argumentos, esto equivale a:
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!
fuente
¿Ha probado argumentos de palabras clave con valores predeterminados? Algo como
def decorate_something(foo=bar, baz=quux): pass
fuente
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.
fuente
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...
fuente
@redirect_output("somewhere.log") def foo()
ejemplo de la pregunta.