Entendendo @decorators no Python em 6 passos
Vitor Buxbaum Orlandi
Posted on November 14, 2023
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?
Posted on November 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 16, 2023