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.

Foi útil?

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.

Exemplo:

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...
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top