2009-03-17 8 views
53

Me gustaría crear un decorador de Python que se puede utilizar ya sea con parámetros:¿Cómo crear un decorador de Python que se pueda usar con o sin 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 que sea posible?

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

+0

La apariencia predeterminada '@ redirect_output' es notablemente poco informativa. Sugeriría que es una mala idea. Usa la primera forma y simplifica tu vida mucho. –

+0

pregunta interesante, hasta que lo vi y revisé la documentación, habría supuesto que @f era lo mismo que @f(), y aún así creo que debería ser, para ser sincero (cualquier argumento proporcionado sería solo adherido al argumento de la función) – rog

Respuesta

33

Sé que esta pregunta es antiguo, pero algunos de los comentarios son nuevos , y aunque 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 para comprobar para ver si hay un solo argumento y es callabe (NOTA: Las comprobaciones adicionales serán necesarios si su decorador sólo 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, usted hace lo que hace cualquier decorador normal, devuelve una versión modificada o envuelta de la función aprobada.

En el segundo caso, devuelve un "nuevo" decorador que de alguna manera utiliza la información que se transmite con * args, ** kwargs.

Esto está bien y todo, pero tener que escribirlo para cada decorador que haga 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 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 advertencia:

Anoté anteriormente pero debería repetir aquí, el cheque en este decorador hace una suposición sobre los argumentos que un decorador puede recibir (a saber, que no puede recibir un solo argumento invocable). Como ahora lo hacemos aplicable a cualquier generador, debe tenerse en cuenta o modificarse si se lo contradice.

La siguiente muestra 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 
+1

esto es tan enrevesada y enrevesada! – gooli

-2

general le puede dar argumentos por defecto en Python ...

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

No estoy seguro si esto funciona con decoradores, así, sin embargo. No sé de ninguna razón por la que no lo haría.

+0

decoradores son funciones. los argumentos predeterminados funcionan – Geo

+2

Si dice @dec (abc) la función no se pasa directamente a dec. dec (abc) devuelve algo, y este valor de retorno se usa como decorador. Entonces dec (abc) tiene que devolver una función, que luego pasa la función decorada como un parámetro. (También vea el código de thobes) – sth

-1

¿Has probado los argumentos de palabra clave con valores predeterminados? Algo así como

def decorate_something(foo=bar, baz=quux): 
    pass 
-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... 
+0

esto no se puede usar como un decorador como en el ejemplo '@redirect_output (" somewhere.log ") def foo()' en la pregunta. – ehabkost

29

Usando argumentos de palabras clave con valores por defecto (como se sugiere en kquinn) es una buena idea, pero será necesario que incluya el paréntesis:

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

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

Si estaba utilizando Python 3.0 se puede utilizar la palabra clave únicos argumentos 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 puede ser emulado con trucos 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(): 
    ... 
+1

¿Qué pones en 'tu código aquí'? ¿Cómo llamas a la función que está decorada? 'fn (* args, ** kwargs)' no funciona. – lum

11

Necesita detectar ambos casos, f o ejemplo usando el tipo del primer argumento, y en consecuencia devuelve 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 utiliza la sintaxis @redirect_output("output.log"), redirect_output se llama con un solo argumento "output.log", y debe devolver un decorador de aceptar la función a decorar como argumento. Cuando se usa como @redirect_output, se llama directamente con la función para decorar como un argumento.

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

8

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

En el primer ejemplo:

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

la función redirect_output se llama con el argumento dada, que se espera para devolver una función decoradora , que a su vez es llamado con foo como argumento, cuales (finalmente!) se espera que devuelva la función decorada final.

El código equivalente es el siguiente:

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

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

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

Por lo que podemos hacer lo desea, pero no en una totalmente inconsútil:

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) 

This shoul d 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 se llame siempre a la función de decorador , incluso si no tiene argumentos. En este caso, el segundo ejemplo sería el siguiente: código de función

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

El decorador 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) 
5

Sé que esto es una vieja pregunta, pero realmente no les gusta ninguna de las técnicas propuestas por lo que quería añadir otro método. Vi que django usa un método realmente limpio en su login_required decorator in django.contrib.auth.decorators. Como puede ver en el decorator's docs, se puede usar solo como @login_required o con argumentos, @login_required(redirect_field_name='my_redirect_field').

La forma en que lo hacen es bastante simple. Agregan 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 este enfoque que nos django es ser más elegante y fácil de entender que cualquiera de las otras técnicas propuestas aquí.

1

De hecho, el caso salvedad en solución de @ bj0 se puede comprobar 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 algunos casos de prueba para esta versión a prueba de fallos 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 
+1

Gracias. Esto es útil! –

2

Varias respuestas aquí ya resuelven bien su problema. Con respecto al estilo, sin embargo, prefiero resolver esta situación decorador usando functools.partial, como se sugiere en David Beazley Python Cookbook 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 

Mientras que sí, que sólo puede hacer

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

sin cobarde soluciones alternativas, 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, se redirecciona la salida de una función en este Stack Overflow post.


Si quieres bucear más profundo, consulte el Capítulo 9 (Metaprogramación) en Python Cookbook 3, que está disponible gratuitamente para ser read online.

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

Codificación feliz :)

Cuestiones relacionadas