Como criar um decorador Python que pode ser usado com ou sem parâmetros?
Pergunta
Eu gostaria de criar um decorador Python que pode ser usado tanto com parâmetros:
@redirect_output("somewhere.log")
def foo():
....
ou sem eles (por exemplo, para redirecionar a saída para stderr por padrão):
@redirect_output
def foo():
....
Isso é de todo possível?
Note que não estou à procura de uma solução diferente para o problema de redirecionamento de saída, é apenas um exemplo da sintaxe eu gostaria de alcançar.
Solução
Eu sei que esta questão é antiga, mas alguns dos comentários são novos, e enquanto todas as soluções viáveis ??são essencialmente os mesmos, a maioria deles não são muito limpos ou de fácil leitura.
A resposta de como thobe diz, a única maneira de lidar com ambos os casos é para verificar se ambos os cenários. A maneira mais fácil é simplesmente verificar para ver se há um único argumento e é callabe (NOTA: As verificações extras será necessário se o seu decorador leva apenas 1 argumento e ele passa a ser um objeto que pode ser chamado):
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)
No primeiro caso, você faz o que qualquer decorador normal faz, retornar uma versão modificada ou envolvidos do passado em função.
No segundo caso, você retorna um decorador 'novo' que de alguma forma utiliza as informações passadas com args *, ** kwargs.
Isso é bom e tudo, mas ter que escrevê-lo fora para cada decorador que você faz pode ser muito chato e não tão limpo. Em vez disso, seria bom ser capaz de modificar automagicamente nossos decoradores, sem ter que re-escrever-los ... mas é o que decoradores são para!
Usando a seguinte decorador decorador, podemos deocrate nossos decoradores de modo que eles podem ser usados ??com ou sem 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
Agora, podemos decorar as nossas decoradores com @doublewrap, e eles vão trabalhar com e sem argumentos, com uma ressalva:
I mencionado acima, mas deve repetir aqui, o check-in este decorador faz uma suposição sobre os argumentos que um decorador pode receber (ou seja, que ele não pode receber um único argumento, que pode ser chamado). Uma vez que estamos tornando-se aplicável a qualquer gerador de agora, ele precisa ser mantido em mente, ou modificado se ele vai ser contrariada.
A seguir demonstra o seu 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
Outras dicas
Usando argumentos de palavra-chave com valores padrão (como sugerido por kquinn) é uma boa idéia, mas vai exigir que você inclua o parêntese:
@redirect_output()
def foo():
...
Se você gostaria de uma versão que funciona sem o parêntese sobre o decorador terá que conta ambos os cenários em seu código decorador.
Se você estava usando o Python 3.0, você poderia usar palavra-chave únicos argumentos para isso:
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)
Em Python 2.x isso pode ser emulado com varargs truques:
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
Qualquer uma destas versões permitem que você escrever código como este:
@redirected_output
def foo():
...
@redirected_output(destination="somewhere.log")
def bar():
...
Você precisa detectar ambos os casos, por exemplo, usando o tipo do primeiro argumento, e, consequentemente, retornar tanto o invólucro (quando usado sem parâmetros) ou um decorador (quando usado com 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
Ao usar a sintaxe @redirect_output("output.log")
, redirect_output
é chamado com um único "output.log"
argumento, e deve retornar um decorador de aceitar a função a ser decorado como um argumento. Quando usado como @redirect_output
, ele é chamado diretamente com a função a ser decorado como um argumento.
Ou em outras palavras: a sintaxe @
deve ser seguido por uma expressão cujo resultado é uma função de aceitar uma função a ser decorado como seu único argumento, e retornando a função decorados. A expressão em si pode ser uma chamada de função, que é o caso com @redirect_output("output.log")
. Complicado, mas é verdade: -)
Um decorador python é chamado de uma forma fundamentalmente diferente dependendo se você lhe dá argumentos ou não. A decoração é realmente apenas um (sintaticamente restrito) expressão.
Em seu primeiro exemplo:
@redirect_output("somewhere.log")
def foo():
....
o redirect_output
função é chamada com o
dado argumento, que é esperado para retornar um decorador
função, que em si é chamado com foo
como um argumento,
que (finalmente!) é esperado para retornar a função decorado final.
Os olhares de código equivalentes como este:
def foo():
....
d = redirect_output("somewhere.log")
foo = d(foo)
O código equivalente para os seus segundo exemplo se parece com:
def foo():
....
d = redirect_output
foo = d(foo)
Assim você pode não o que você gostaria, mas não de uma forma totalmente sem costura:
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)
Esta deve ser ok, a menos que você deseja usar uma função como um argumento para o seu decorador, no caso do decorador que vai erradamente, que não tem argumentos. Ele também irá falhar se esta decoração é aplicada a outra decoração que não retorna um tipo de função.
Um método alternativo é apenas exigir que o função decorador é sempre chamado, mesmo que seja sem argumentos. Neste caso, o seu segundo exemplo ficaria assim:
@redirect_output()
def foo():
....
O código de função decorador ficaria assim:
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)
Eu sei que isto é uma questão de idade, mas eu realmente não gosto de qualquer uma das técnicas propostas assim que eu queria acrescentar outro método. Eu vi que o Django usa um método realmente limpo em sua login_required
decorador em django.contrib.auth.decorators
. Como você pode ver na docs do decorador do , ele pode ser usado sozinho como @login_required
ou com argumentos, @login_required(redirect_field_name='my_redirect_field')
.
A forma como eles fazem isso é bastante simples. Eles acrescentam um kwarg
(function=None
) antes de seus argumentos decorador. Se o decorador é usado sozinho, function
será a função real que está decorando, ao passo que se for chamado com argumentos, function
será None
.
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
Eu acho essa abordagem que o Django usa para ser mais elegante e mais fácil de entender do que qualquer das outras técnicas aqui propostas.
Várias respostas aqui já resolver o seu problema muito bem. Com relação ao estilo, no entanto, eu prefiro resolver esta situação decorador usando functools.partial
, como sugerido na de 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
Enquanto sim, você pode apenas fazer
@decorator()
def f(*args, **kwargs):
pass
sem soluções funky, eu achar estranho olhando, e eu como ter a opção de simplesmente decorar com @decorator
.
Quanto à missão secundária objetivo, redirecionando a saída de uma função é abordado neste Stack Overflow post.
Se você quiser mergulho mais profundo, veja Capítulo 9 (Metaprogramação) em Python Cookbook 3 , que está disponível gratuitamente para ser ler online.
Alguns dos que o material é demoed ao vivo (e muito mais!) Em vídeo do YouTube incrível de Beazley Python 3 Metaprogramação .
Happy codificação:)
Na verdade, o caso ressalva em @ de bj0 solução pode ser verificado facilmente:
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
Aqui estão alguns casos de teste para esta versão à prova de falhas 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
Você já tentou argumentos de palavra-chave com valores padrão? Algo como
def decorate_something(foo=bar, baz=quux):
pass
Geralmente você pode dar argumentos padrão em Python ...
def redirect_output(fn, output = stderr):
# whatever
Não tenho certeza se isso funciona com decoradores, bem como, no entanto. Eu não sei de nenhuma razão para que ele não iria.
Com base na resposta 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...