Decorators: um resumo
Quando falamos de decorators no Python, nos referimos à seguinte sintaxe:
@meu_decorator
def minha_funcao(param):
...
Ou seja: eu tenho uma função (mas poderia ser um método ou uma classe) chamada minha_funcao que está 'decorada' pelo decorator meu_decorator.
É comum ver essa sintaxe em diversas bibliotecas, como Flask e FastAPI (ao criar rotas), Pytest (ao criar fixtures), Dataclass (para definir uma dataclass) etc. Mas também há decorators "nativos", como @classmethod e @staticmethod.
Esse artigo é para você que já usou decorators em algum cenário, mas não sabe como poderia criar um por conta própria.
Entender a estrutura de um decorator pode ser uma tarefa complexa, mas irei dividir em passos mais simples de entender. Vou começar pegando leve e depois pode ficar mais pesado, mas você consegue! Vamos lá? 💜
Passo-a-passo
Passo 0: Crie uma função, e execute-a
Bem simples né? Vou adicionar alguns prints especiais que vão facilitar o entendimento do fluxo 😉
def my_function(my_param):
print(f">> Iniciando my_function({my_param})")
print(f">> Finalizando my_function({my_param})")
print("[Começando tudo]")
my_function("meu querido parâmetro")
Passo 1: atribuir a função a outro nome/variável
Caso você não saiba, funções também são objetos no Python! Elas possuem tipo (<class 'function'>) e atributos, e podemos "guardá-las" em outras variáveis.
Para esse passo, ficamos assim:
def my_function(my_param):
print(f">> Iniciando my_function({my_param})")
print(f">> Finalizando my_function({my_param})")
print("[Começando tudo]")
other_name = my_function # Passo 1
print("[Troca de nome realizada]") # Passo 1
other_name("meu querido parâmetro") # Passo 1
Ou seja: other_name guarda my_function, então se eu chamar other_name na verdade estarei executando my_function.
Passo 2: criar uma função que retorna outra
Como funções são objetos, eu posso usar uma função para retornar outra:
def my_function(my_param):
print(f">> Iniciando my_function({my_param})")
print(f">> Finalizando my_function({my_param})")
# Passo 2
def get_function():
print("> Iniciando get_function()")
print("> Finalizando get_function()")
return my_function
print("[Começando tudo]")
other_name = get_function() # Passo 2
print("[Troca de nome realizada]")
other_name("meu querido parâmetro")
Repare que get_function não retorna o resultado de my_function, mas a função my_function em si.
Executando o código que temos até agora, a saída será:
[Começando tudo]
> Iniciando get_function()
> Finalizando get_function()
[Troca de nome realizada]
>> Iniciando my_function(meu querido parâmetro)
>> Finalizando my_function(meu querido parâmetro)
Passo 3: declarar uma função dentro da outra (função "mãe")
Aqui é uma refatoração relativamente simples, mas é essencial para garantirmos o comportamento do decorator: vamos deslocar a declaração de my_function para dentro de get_function.
def get_function():
print("> Iniciando get_function()")
def my_function(my_param): # Passo 3
print(f">> Iniciando my_function({my_param})") # Passo 3
print(f">> Finalizando my_function({my_param})") # Passo 3
print("> Finalizando get_function()")
return my_function
print("[Começando tudo]")
other_name = get_function()
print("[Troca de nome realizada]")
other_name("meu querido parâmetro")
Em termos de comportamento, nada vai mudar e a saída no terminal continuará a mesma. A diferença é que agora
- não é mais possível acessar
my_functiondiretamente pelo escopo global -
my_functioncompartilha do escopo deget_function(como veremos a seguir)
Passo 4: Compartilhar parâmetro da "mãe" com execução da "filha"
Se seus neurônios ainda não tinham fritado, provavelmente chegou a sua hora 😅
Eis o que vamos fazer:
- renomear
get_functionparamother_function(só pra facilitar as coisas) - adicionar um parâmetro em
mother_functionchamadomother_param - fazer
my_functionacessarmother_param(ilustrando com um print)
def mother_function(mother_param): # Passo 4
print(f"> Iniciando mother_function({mother_param})") # Passo 4
def my_function(my_param):
print(f">> Iniciando my_function({my_param})")
print(f">> Tenho acesso a ({mother_param})!") # Passo 4
print(f">> Finalizando my_function({my_param})")
print(f"> Finalizando mother_function({mother_param})") # Passo 4
return my_function
print("[Começando tudo]")
other_name = mother_function("parâmetro materno") # Passo 4
print("[Troca de nome realizada]")
other_name("meu querido parâmetro")
Ou seja: my_function consegue acessar variáveis no escopo de mother_function! Incrível, né??
Executando o código que temos até agora, a saída será:
[Começando tudo]
> Iniciando mother_function(parâmetro materno)
> Finalizando mother_function(parâmetro materno)
[Troca de nome realizada]
>> Iniciando my_function(meu querido parâmetro)
>> Tenho acesso a (parâmetro materno)!
>> Finalizando my_function(meu querido parâmetro)
Passo 5: passar uma nova função como parâmetro para a "mãe"
Agora vem o "pulo-do-gato": vamos tirar proveito do fato que funções são objetos, e passar uma função (ao invés de uma simples string) como parâmetro para mother_function. Dentro de my_function então poderei chamar essa nova função, manipulando (decorando 👀) como eu desejar.
Então agora vou:
- criar uma nova função
final_functione passá-la comomother_param - fazer
my_functionchamarmother_param(que seráfinal_function) passandomy_paramcomo parâmetro - e por fim, exibir o retorno da chamada de
other_function
def mother_function(mother_param):
print(f"> Iniciando mother_function({mother_param})")
def my_function(my_param):
print(f">> Iniciando my_function({my_param})")
res = mother_param(my_param) # Passo 5
print(f">> Finalizando my_function({my_param})")
return res # Passo 5
print(f"> Finalizando mother_function({mother_param})")
return my_function
# Passo 5
def final_function(final_param):
print(f">>> Executando final_function({final_param})")
return "RESULTADO FINAL"
print("[Começando tudo]")
other_name = mother_function(final_function)
print("[Troca de nome realizada]")
print(other_name("meu querido parâmetro")). # Passo 5
Executando esse código, a saída fica assim:
[Começando tudo]
> Iniciando mother_function(<function 'final_function'>)
> Finalizando mother_function(<function 'final_function'>)
[Troca de nome realizada]
>> Iniciando my_function(meu querido parâmetro)
>>> Executando final_function(meu querido parâmetro)
>> Finalizando my_function(meu querido parâmetro)
RESULTADO FINAL
Antes de seguir para o último passo
Nesse momento já temos o comportamento "cru" do decorator: uma função está sendo decorada por outra. 💅
Podemos afirmar isso porque quando fazemos other_name = mother_function(final_function), estamos usando mother_function para decorar final_function! Podemos dizer que other_function é a versão decorada de final_function.
Nesse caso é uma decoração simples (prints informando que a execução está iniciando/finalizando), mas ao final mostrarei um exemplo mais aplicável 😉
Passo 6: simplificando para a sintaxe com @
Agora que temos nosso decorator funcionando, só precisamos usar a famosa sintaxe com @. Nosso código vai ficar assim:
def mother_function(mother_param):
# Nada muda aqui, escondi apenas para ajudar na leitura ;)
...
@mother_function # Passo 6
def final_function(final_param):
print(f">>> Executando final_function({final_param})")
return "RESULTADO FINAL"
print("[Começando tudo]")
print(final_function("meu querido parâmetro")) # Passo 6
Removi o print [Troca de nome realizada] porque esse passo é feito implicitamente. Antes usávamos other_name para chamar a função decorada, mas agora final_function já guarda a versão decorada da função.
Isso significa que uma chamada para final_function na verdade executará my_function, e teremos a seguinte saída:
Executando o novo código, a saída fica assim:
> Iniciando mother_function(<function 'final_function'>)
> Finalizando mother_function(<function 'final_function'>)
[Começando tudo]
>> Iniciando my_function(meu querido parâmetro)
>>> Executando final_function(meu querido parâmetro)
>> Finalizando my_function(meu querido parâmetro)
RESULTADO FINAL
Uma diferença interessante aqui: [Começando tudo] agora aparece depois do print de mother_function, já que ela foi processada antes (quando usamos @mother_function).
Um exemplo mais divertido (e útil)
Para finalizar, vamos ver mais um exemplo para garantir que ficou nítido?! 🤩
Vamos fazer um decorator chamado shhh para suprimir a saída padrão (os "prints") de uma função. Fica mais ou menos assim:
def shhh(func):
def wrapper(*args, **kwargs):
with open(os.devnull, "w") as student_output:
with contextlib.redirect_stdout(student_output):
return func(*args, **kwargs)
return wrapper
@shhh
def say_goodbye_to(name):
print(f"Goodbye, {name}!")
def say_hello_to(name):
print(f"Hello, {name}!")
say_goodbye_to("Jair")
say_hello_to("Luiz")
Executando esse exemplo, a saída será somente:
Hello, Luiz!
E aí, como você pensa em explorar o poder dos decorators?
Top comments (0)