Decoradores com parâmetros?

401

Estou com um problema com a transferência da variável 'insurance_mode' pelo decorador. Eu o faria pela seguinte declaração do decorador:

 @execute_complete_reservation(True)
 def test_booking_gta_object(self):
     self.test_select_gta_object()

mas, infelizmente, esta afirmação não funciona. Talvez haja uma maneira melhor de resolver esse problema.

def execute_complete_reservation(test_case,insurance_mode):
    def inner_function(self,*args,**kwargs):
        self.test_create_qsf_query()
        test_case(self,*args,**kwargs)
        self.test_select_room_option()
        if insurance_mode:
            self.test_accept_insurance_crosseling()
        else:
            self.test_decline_insurance_crosseling()
        self.test_configure_pax_details()
        self.test_configure_payer_details

    return inner_function
falek.marcin
fonte
3
Seu exemplo não é sintaticamente válido. execute_complete_reservationrequer dois parâmetros, mas você está passando um. Decoradores são apenas açúcar sintático para envolver funções dentro de outras funções. Consulte docs.python.org/reference/compound_stmts.html#function para obter a documentação completa.
Brian Clapper

Respostas:

687

A sintaxe para decoradores com argumentos é um pouco diferente - o decorador com argumentos deve retornar uma função que terá uma função e retornará outra função. Portanto, ele realmente deve retornar um decorador normal. Um pouco confuso, certo? O que eu quero dizer é:

def decorator_factory(argument):
    def decorator(function):
        def wrapper(*args, **kwargs):
            funny_stuff()
            something_with_argument(argument)
            result = function(*args, **kwargs)
            more_funny_stuff()
            return result
        return wrapper
    return decorator

Aqui você pode ler mais sobre o assunto - também é possível implementar isso usando objetos de chamada e isso também é explicado lá.

t.dubrownik
fonte
56
Eu me pergunto por que o GVR não o implementou passando os parâmetros como argumentos subsequentes do decorador após 'function'. 'Yo dawg ouvi dizer que você gosta de fechamentos ...' etc.
Michel Müller
3
> Função seria o primeiro ou último argumento? Obviamente, primeiro, já que os parâmetros são uma lista de parâmetros de comprimento variável. > Também é estranho que você "chame" a função com uma assinatura diferente daquela na definição. Como você aponta, ele se encaixaria muito bem, na verdade - é praticamente análogo ao modo como um método de classe é chamado. Para deixar mais claro, você pode ter algo como a convenção decorador (self_func, param1, ...). Mas nota: Eu não estou defendendo para qualquer mudança aqui, Python é muito longe no caminho para isso e podemos ver como alterações significativas ter trabalhado fora ..
Michel Müller
21
você esqueceu functools.wraps muito útil para decorar envoltório :)
socketpair
10
Você esqueceu o retorno ao chamar a função, ou seja, return function(*args, **kwargs)
formiaczek
36
Talvez óbvio, mas apenas no caso: você precisa usar esse decorador como @decorator()e não apenas @decorator, mesmo se tiver apenas argumentos opcionais.
Patrick Mevzek
326

Edit : para uma compreensão profunda do modelo mental dos decoradores, dê uma olhada neste incrível Pycon Talk. vale a pena os 30 minutos.

Uma maneira de pensar sobre decoradores com argumentos é

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

traduz para

foo = decorator(foo)

Então, se o decorador tiver argumentos,

@decorator_with_args(arg)
def foo(*args, **kwargs):
    pass

traduz para

foo = decorator_with_args(arg)(foo)

decorator_with_args é uma função que aceita um argumento personalizado e retorna o decorador real (que será aplicado à função decorada).

Eu uso um truque simples com parciais para facilitar meus decoradores

from functools import partial

def _pseudo_decor(fun, argument):
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def foo(*args, **kwargs):
    pass

Atualizar:

Acima, footorna-sereal_decorator(foo)

Um efeito de decorar uma função é que o nome foo é substituído na declaração do decorador. fooé "substituído" por tudo o que é retornado por real_decorator. Nesse caso, um novo objeto de função.

Tudo de foo os metadados de são substituídos, principalmente docstring e nome da função.

>>> print(foo)
<function _pseudo_decor.<locals>.ret_fun at 0x10666a2f0>

functools.wraps nos fornece um método conveniente para "levantar" a docstring e o nome da função retornada.

from functools import partial, wraps

def _pseudo_decor(fun, argument):
    # magic sauce to lift the name and doc of the function
    @wraps(fun)
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def bar(*args, **kwargs):
    pass

>>> print(bar)
<function __main__.bar(*args, **kwargs)>
srj
fonte
4
Sua resposta perfeitamente explicou a ortogonalidade inerente do decorador, obrigado
zsf222
Você poderia adicionar @functools.wraps?
Mr_and_Mrs_D 26/08/18
11
@Mr_and_Mrs_D, atualizei a postagem com um exemplo com functool.wraps. Adicioná-lo no exemplo pode confundir ainda mais os leitores.
srj 28/08
7
O que está argaqui?
displayname 25/09
11
Como você passará o argumento passado barpara o argumento de real_decorator?
Chang Zhao
85

Eu gostaria de mostrar uma ideia que é IMHO bastante elegante. A solução proposta por t.dubrownik mostra um padrão que é sempre o mesmo: você precisa do wrapper de três camadas, independentemente do que o decorador faz.

Então eu pensei que este é um trabalho para um meta-decorador, ou seja, um decorador para decoradores. Como um decorador é uma função, ele realmente funciona como um decorador regular com argumentos:

def parametrized(dec):
    def layer(*args, **kwargs):
        def repl(f):
            return dec(f, *args, **kwargs)
        return repl
    return layer

Isso pode ser aplicado a um decorador regular para adicionar parâmetros. Por exemplo, digamos que temos o decorador que dobra o resultado de uma função:

def double(f):
    def aux(*xs, **kws):
        return 2 * f(*xs, **kws)
    return aux

@double
def function(a):
    return 10 + a

print function(3)    # Prints 26, namely 2 * (10 + 3)

Com @parametrizednós podemos construir um @multiplydecorador genérico com um parâmetro

@parametrized
def multiply(f, n):
    def aux(*xs, **kws):
        return n * f(*xs, **kws)
    return aux

@multiply(2)
def function(a):
    return 10 + a

print function(3)    # Prints 26

@multiply(3)
def function_again(a):
    return 10 + a

print function(3)          # Keeps printing 26
print function_again(3)    # Prints 39, namely 3 * (10 + 3)

Convencionalmente, o primeiro parâmetro de um parâmetro decorador é a função, enquanto os argumentos restantes corresponderão ao parâmetro do decorador parametrizado.

Um exemplo de uso interessante pode ser um decorador assertivo com segurança de tipo:

import itertools as it

@parametrized
def types(f, *types):
    def rep(*args):
        for a, t, n in zip(args, types, it.count()):
            if type(a) is not t:
                raise TypeError('Value %d has not type %s. %s instead' %
                    (n, t, type(a))
                )
        return f(*args)
    return rep

@types(str, int)  # arg1 is str, arg2 is int
def string_multiply(text, times):
    return text * times

print(string_multiply('hello', 3))    # Prints hellohellohello
print(string_multiply(3, 3))          # Fails miserably with TypeError

Uma observação final: aqui não estou usando functools.wrapspara as funções do wrapper, mas eu recomendaria usá-lo o tempo todo.

Dacav
fonte
3
Não usei isso exatamente, mas me ajudou a entender o conceito :) Obrigado!
mouckatron
Eu tentei isso e tive alguns problemas .
Jeff
@ Jeff, você poderia compartilhar conosco o tipo de problemas que teve?
Dacav # 14/17
Eu o vinculei à minha pergunta e descobri ... eu precisava chamar a @wrapsminha para o meu caso particular.
Jeff
4
Oh garoto, eu perdi um dia inteiro nisso. Felizmente, encontrei esta resposta (que aliás poderia ser a melhor resposta já criada em toda a Internet). Eles também usam seu @parametrizedtruque. O problema que tive foi que esqueci que a @sintaxe é igual a chamadas reais (de alguma forma eu sabia disso e não sabia disso ao mesmo tempo que você pode entender da minha pergunta). Portanto, se você deseja traduzir a @sintaxe em chamadas comuns, para verificar como ele funciona, é melhor comentar temporariamente primeiro ou acabar chamando duas vezes e obtendo resultados mumbojumbo
z33k
79

Aqui está uma versão ligeiramente modificada da resposta de t.dubrownik . Por quê?

  1. Como modelo geral, você deve retornar o valor de retorno da função original.
  2. Isso altera o nome da função, o que poderia afetar outros decoradores / código.

Então use @functools.wraps():

from functools import wraps

def decorator(argument):
    def real_decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            funny_stuff()
            something_with_argument(argument)
            retval = function(*args, **kwargs)
            more_funny_stuff()
            return retval
        return wrapper
    return real_decorator
Ross R
fonte
37

Presumo que seu problema seja passar argumentos para o seu decorador. Isso é um pouco complicado e não é direto.

Aqui está um exemplo de como fazer isso:

class MyDec(object):
    def __init__(self,flag):
        self.flag = flag
    def __call__(self, original_func):
        decorator_self = self
        def wrappee( *args, **kwargs):
            print 'in decorator before wrapee with flag ',decorator_self.flag
            original_func(*args,**kwargs)
            print 'in decorator after wrapee with flag ',decorator_self.flag
        return wrappee

@MyDec('foo de fa fa')
def bar(a,b,c):
    print 'in bar',a,b,c

bar('x','y','z')

Impressões:

in decorator before wrapee with flag  foo de fa fa
in bar x y z
in decorator after wrapee with flag  foo de fa fa

Veja o artigo de Bruce Eckel para mais detalhes.

Ross Rogers
fonte
20
Cuidado com as aulas de decorador. Eles não funcionam em métodos, a menos que você reinvente manualmente a lógica dos descritores de métodos de instâncias.
9
delnan, gostaria de elaborar? Eu só tive que usar esse padrão uma vez, então ainda não atingi nenhuma das armadilhas.
Ross Rogers
2
@RossRogers Meu palpite é que @delnan está se referindo a coisas como __name__uma instância da classe decoradora não terá?
jamesc
9
@jamesc Isso também, embora seja relativamente fácil de resolver. O caso específico ao class Foo: @MyDec(...) def method(self, ...): blahqual me referi foi o que não funciona porque Foo().methodnão será um método vinculado e não será aprovado selfautomaticamente. Isso também pode ser corrigido, criando MyDecum descritor e criando métodos vinculados __get__, mas é mais envolvido e muito menos óbvio. No final, as aulas de decorador não são tão convenientes quanto parecem.
2
@delnan Eu gostaria de ver essa ressalva em destaque. Estou acertando e estou interessado em encontrar uma solução que FUNCIONA (mais envolvida, embora menos óbvia).
HaPsantran 13/03/16
12
def decorator(argument):
    def real_decorator(function):
        def wrapper(*args):
            for arg in args:
                assert type(arg)==int,f'{arg} is not an interger'
            result = function(*args)
            result = result*argument
            return result
        return wrapper
    return real_decorator

Uso do decorador

@decorator(2)
def adder(*args):
    sum=0
    for i in args:
        sum+=i
    return sum

Então o

adder(2,3)

produz

10

mas

adder('hi',3)

produz

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-143-242a8feb1cc4> in <module>
----> 1 adder('hi',3)

<ipython-input-140-d3420c248ebd> in wrapper(*args)
      3         def wrapper(*args):
      4             for arg in args:
----> 5                 assert type(arg)==int,f'{arg} is not an interger'
      6             result = function(*args)
      7             result = result*argument

AssertionError: hi is not an interger
Gajendra D Ambi
fonte
8

Este é um modelo para um decorador de funções que não requer ()que nenhum parâmetro seja fornecido:

import functools


def decorator(x_or_func=None, *decorator_args, **decorator_kws):
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kws):
            if 'x_or_func' not in locals() \
                    or callable(x_or_func) \
                    or x_or_func is None:
                x = ...  # <-- default `x` value
            else:
                x = x_or_func
            return func(*args, **kws)

        return wrapper

    return _decorator(x_or_func) if callable(x_or_func) else _decorator

um exemplo disso é dado abaixo:

def multiplying(factor_or_func=None):
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if 'factor_or_func' not in locals() \
                    or callable(factor_or_func) \
                    or factor_or_func is None:
                factor = 1
            else:
                factor = factor_or_func
            return factor * func(*args, **kwargs)
        return wrapper
    return _decorator(factor_or_func) if callable(factor_or_func) else _decorator


@multiplying
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying()
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying(10)
def summing(x): return sum(x)

print(summing(range(10)))
# 450
norok2
fonte
Note também que factor_or_func(ou qualquer outro parâmetro) nunca deve se transferido em wrapper().
Norok2 28/08/19
Por que você precisa fazer o check-in locals()?
Shital Shah 24/03
@ShitalShah que cobre o caso em que o decorador é usado sem ().
norok2 24/03
4

No meu exemplo, decidi resolver isso por meio de uma lambda de uma linha para criar uma nova função de decorador:

def finished_message(function, message="Finished!"):

    def wrapper(*args, **kwargs):
        output = function(*args,**kwargs)
        print(message)
        return output

    return wrapper

@finished_message
def func():
    pass

my_finished_message = lambda f: finished_message(f, "All Done!")

@my_finished_message
def my_func():
    pass

if __name__ == '__main__':
    func()
    my_func()

Quando executado, isso imprime:

Finished!
All Done!

Talvez não seja tão extensível quanto outras soluções, mas funcionou para mim.

ZacBook
fonte
Isso funciona. Embora sim, isso torna difícil definir o valor para o decorador.
Arindam Roychowdhury
3

Escrever um decorador que funcione com e sem parâmetro é um desafio, porque o Python espera um comportamento completamente diferente nesses dois casos! Muitas respostas tentaram contornar isso e abaixo está uma melhoria da resposta por @ norok2. Especificamente, essa variação elimina o uso delocals() .

Seguindo o mesmo exemplo dado por @ norok2:

import functools

def multiplying(f_py=None, factor=1):
    assert callable(f_py) or f_py is None
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return factor * func(*args, **kwargs)
        return wrapper
    return _decorator(f_py) if callable(f_py) else _decorator


@multiplying
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying()
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying(factor=10)
def summing(x): return sum(x)

print(summing(range(10)))
# 450

Brinque com este código .

O problema é que o usuário deve fornecer chave, pares de parâmetros de valor em vez de parâmetros posicionais e o primeiro parâmetro é reservado.

Shital Shah
fonte
2

É sabido que os dois seguintes códigos são quase equivalentes:

@dec
def foo():
    pass    foo = dec(foo)

############################################
foo = dec(foo)

Um erro comum é pensar que @simplesmente oculta o argumento mais à esquerda.

@dec(1, 2, 3)
def foo():
    pass    
###########################################
foo = dec(foo, 1, 2, 3)

Seria muito mais fácil escrever decoradores se o que precede é como @funcionou. Infelizmente, não é assim que as coisas são feitas.


Considere um decorador Waitque interrompe a execução do programa por alguns segundos. Se você não passar um tempo de espera, o valor padrão será 1 segundo. Os casos de uso são mostrados abaixo.

##################################################
@Wait
def print_something(something):
    print(something)

##################################################
@Wait(3)
def print_something_else(something_else):
    print(something_else)

##################################################
@Wait(delay=3)
def print_something_else(something_else):
    print(something_else)

Quando Waittem um argumento, como @Wait(3), então, a chamada Wait(3) é executada antes que qualquer outra coisa aconteça.

Ou seja, os dois seguintes códigos são equivalentes

@Wait(3)
def print_something_else(something_else):
    print(something_else)

###############################################
return_value = Wait(3)
@return_value
def print_something_else(something_else):
    print(something_else)

Isto é um problema.

if `Wait` has no arguments:
    `Wait` is the decorator.
else: # `Wait` receives arguments
    `Wait` is not the decorator itself.
    Instead, `Wait` ***returns*** the decorator

Uma solução é mostrada abaixo:

Vamos começar criando a seguinte classe DelayedDecorator:

class DelayedDecorator:
    def __init__(i, cls, *args, **kwargs):
        print("Delayed Decorator __init__", cls, args, kwargs)
        i._cls = cls
        i._args = args
        i._kwargs = kwargs
    def __call__(i, func):
        print("Delayed Decorator __call__", func)
        if not (callable(func)):
            import io
            with io.StringIO() as ss:
                print(
                    "If only one input, input must be callable",
                    "Instead, received:",
                    repr(func),
                    sep="\n",
                    file=ss
                )
                msg = ss.getvalue()
            raise TypeError(msg)
        return i._cls(func, *i._args, **i._kwargs)

Agora podemos escrever coisas como:

 dec = DelayedDecorator(Wait, delay=4)
 @dec
 def delayed_print(something):
    print(something)

Observe que:

  • dec não aceita vários argumentos.
  • dec aceita apenas a função a ser quebrada.

    importar inspecione a classe PolyArgDecoratorMeta (tipo): def call (Aguarde, * args, ** kwargs): tente: arg_count = len (args) if (arg_count == 1): se for possível chamar (args [0]): SuperClass = inspecionar. getmro (PolyArgDecoratorMeta) [1] r = SuperClasse. ligar (Aguarde, args [0]) else: r = DelayedDecorator (espera, * args, ** kwargs) else: r = DelayedDecorator (espera, * args, ** kwargs) finalmente: passa retorno r

    classe de tempo de importação Wait (metaclass = PolyArgDecoratorMeta): def init (i, func, delay = 2): i._func = func i._delay = delay

    def __call__(i, *args, **kwargs):
        time.sleep(i._delay)
        r = i._func(*args, **kwargs)
        return r 

Os dois seguintes pedaços de código são equivalentes:

@Wait
def print_something(something):
     print (something)

##################################################

def print_something(something):
    print(something)
print_something = Wait(print_something)

Podemos imprimir "something"no console muito lentamente, da seguinte maneira:

print_something("something")

#################################################
@Wait(delay=1)
def print_something_else(something_else):
    print(something_else)

##################################################
def print_something_else(something_else):
    print(something_else)

dd = DelayedDecorator(Wait, delay=1)
print_something_else = dd(print_something_else)

##################################################

print_something_else("something")

Notas Finais

Pode parecer muito código, mas você não precisa escrever as classes DelayedDecoratore PolyArgDecoratorMetasempre. O único código que você precisa escrever pessoalmente é o seguinte, que é bastante curto:

from PolyArgDecoratorMeta import PolyArgDecoratorMeta
import time
class Wait(metaclass=PolyArgDecoratorMeta):
 def __init__(i, func, delay = 2):
     i._func = func
     i._delay = delay

 def __call__(i, *args, **kwargs):
     time.sleep(i._delay)
     r = i._func(*args, **kwargs)
     return r
Samuel Muldoon
fonte
1

defina esta "função decoratorize" para gerar a função decorator personalizada:

def decoratorize(FUN, **kw):
    def foo(*args, **kws):
        return FUN(*args, **kws, **kw)
    return foo

use-o desta maneira:

    @decoratorize(FUN, arg1 = , arg2 = , ...)
    def bar(...):
        ...
chen.wq
fonte
1

Ótimas respostas acima. Este também ilustra @wraps, que pega a sequência de documentos e o nome da função da função original e a aplica à nova versão agrupada:

from functools import wraps

def decorator_func_with_args(arg1, arg2):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            print("Before orginal function with decorator args:", arg1, arg2)
            result = f(*args, **kwargs)
            print("Ran after the orginal function")
            return result
        return wrapper
    return decorator

@decorator_func_with_args("foo", "bar")
def hello(name):
    """A function which prints a greeting to the name provided.
    """
    print('hello ', name)
    return 42

print("Starting script..")
x = hello('Bob')
print("The value of x is:", x)
print("The wrapped functions docstring is:", hello.__doc__)
print("The wrapped functions name is:", hello.__name__)

Impressões:

Starting script..
Before orginal function with decorator args: foo bar
hello  Bob
Ran after the orginal function
The value of x is: 42
The wrapped functions docstring is: A function which prints a greeting to the name provided.
The wrapped functions name is: hello
run_the_race
fonte
0

Caso a função e o decorador precisem usar argumentos, você pode seguir a abordagem abaixo.

Por exemplo, há um decorador chamado decorator1que recebe um argumento

@decorator1(5)
def func1(arg1, arg2):
    print (arg1, arg2)

func1(1, 2)

Agora, se o decorator1argumento precisar ser dinâmico ou passado ao chamar a função,

def func1(arg1, arg2):
    print (arg1, arg2)


a = 1
b = 2
seconds = 10

decorator1(seconds)(func1)(a, b)

No código acima

  • seconds é o argumento para decorator1
  • a, b são os argumentos de func1
Super Nova
fonte