Decoradores são funções que recebem outra função como argumento com o objetivo de estender ou substituir o comportamento da função passada.
Se a função A é um decorador e a função B é o seu argumento, então a função A modifica, estende ou substitui o comportamento da função B sem modificar o código da função B.
Dizemos que a função decoradora A envolve a função B.
Embora falemos em "modificar" o comportamento, a função envolvida não é de fato alterada.
O comportamento ou é adicionado em volta da função envolvida (e do que ela retorna), ou o comportamento da função envolvida é substituído por algum outro comportamento.
Uma função de ordem superior é uma função que aceita uma ou mais funções como argumentos e/ou retorna uma ou mais funções. Uma função usada como argumento ou retornada por outra função é uma função de primeira classe. Uma função Python, como objeto chamável, é uma função de primeira classe que pode ser guardada em uma variável ou usada como argumento, assim como qualquer outro valor ou objeto. Funções de ordem superior e funções de primeira classe trabalham juntas para tornar os decoradores possíveis.
O símbolo @ é colocado antes do nome da função decoradora, logo acima da função a ser decorada, assim:
@decorator
def decorated_function():
pass
Alguns decoradores aceitam argumentos:
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
Se um decorador tiver argumentos padrão definidos, você precisa usar parênteses na chamada @decorator() para que o decorador funcione, como faria ao chamar qualquer função:
@decorator_with_default_arg()
def decorated_function3():
pass
Se um decorador recebe argumentos posicionais, não informar os argumentos vai gerar um erro parecido com este:
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
A sintaxe @decorator é um açúcar sintático, ou uma forma abreviada de chamar a função decoradora e passar a função decorada para ela como argumento.
A seguir estão exemplos de formas alternativas de chamar um decorador:
def function():
pass
function = decorator(function)
def function2():
pass
function2 = decorator_with_arg(name="Bob")(function2)
def function3():
pass
function3 = decorator_with_default_arg()(function3)
A maioria dos decoradores tem a intenção de estender ou substituir o comportamento de outra função, mas alguns decoradores podem não fazer nada além de retornar as funções que estão envolvendo.
Decoradores são funções que recebem pelo menos um argumento: a função que estão envolvendo. Normalmente retornam a função envolvida ou o resultado de uma expressão que usa a função envolvida.
Um decorador simples, que apenas retorna a função que envolve, pode ser escrito assim:
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
Um decorador pode apenas adicionar efeitos colaterais, como informações adicionais usadas para logging:
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
Um decorador não retorna a si mesmo. Ele pode retornar seus argumentos de função, outra função, ou um ou mais valores que substituem o retorno da função passada ou decorada. Se um decorador retornar outra função, normalmente vai retornar uma função interna.
Uma função pode ser definida dentro de outra função. Essa função aninhada é chamada de função interna. Um decorador pode usar uma função interna para envolver seu argumento de função. O decorador então retorna sua função interna. A função interna pode então retornar o argumento de função original.
A seguir está um exemplo de um decorador usado para validação:
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
@my_validator
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!
Na primeira linha, temos a definição do argumento func do decorador.
Na linha seguinte está a definição da função interna do decorador, que envolve o argumento func.
Como a função interna envolve o argumento func do decorador, ela recebe o mesmo argumento que é passado para func.
Repare que o envoltório não precisa usar o mesmo nome para o argumento que foi definido em func.
A função original usa planet e o decorador usa world, e o decorador continua funcionando.
A função interna ou retorna func ou, se world == "Pluto", imprime que Plutão não é um planeta.
Ela poderia ser escrita para lançar um ValueError em vez disso.
Então, a função interna envolve func e ou retorna func ou faz algo que substitui o que func faria.
O decorador retorna sua função interna.
A função interna pode ou não retornar a função original passada.
Dependendo de qual código é executado condicionalmente no envoltório ou na função interna, func pode ser retornado, um erro pode ser lançado, ou um valor do tipo de retorno de func pode ser retornado.
É possível escrever decoradores para funções que recebem um número arbitrário de argumentos usando a sintaxe *args e **kwargs.
A seguir está um exemplo de um decorador para uma função que recebe um número arbitrário de argumentos:
def double(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 18
print(add(2, 3, 4, 5, 6))
#-> 40
Isso funciona para dobrar o valor de retorno da função passada como argumento. Se quisermos triplicar, quadruplicar etc. o valor de retorno, podemos adicionar um parâmetro ao próprio decorador, como mostramos na próxima seção.
A seguir está um exemplo de um decorador que pode ser configurado para multiplicar o valor de retorno da função decorada por um valor arbitrário:
def multi(factor=1):
if factor == 0:
raise ValueError("factor must not be 0")
def outer_wrapper(func):
def inner_wrapper(*args, **kwargs):
return func(*args, **kwargs) * factor
return inner_wrapper
return outer_wrapper
@multi(factor=3)
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 27
print(add(2, 3, 4, 5, 6))
#-> 60
As primeiras linhas validam que factor não é 0.
Depois, o envoltório externo é definido.
Ele tem a mesma assinatura que esperamos de um decorador sem parâmetros.
O envoltório externo tem uma função interna com a mesma assinatura da função original.
O envoltório interno faz o trabalho de multiplicar o valor retornado pela função original pelo argumento passado ao decorador.
O envoltório externo retorna o envoltório interno, e o decorador retorna o envoltório externo.
A seguir está um exemplo de um decorador com parâmetros que controla se ele valida o argumento passado para a função original:
def check_for_pluto(check=True):
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if check and world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
return my_validator
@check_for_pluto(check=False)
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!
Isso permite alternar facilmente entre verificar Pluto ou não, sem precisar modificar my_func.