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_function
diretamente pelo escopo global -
my_function
compartilha 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_function
paramother_function
(só pra facilitar as coisas) - adicionar um parâmetro em
mother_function
chamadomother_param
- fazer
my_function
acessarmother_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_function
e passá-la comomother_param
- fazer
my_function
chamarmother_param
(que seráfinal_function
) passandomy_param
como 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)