DEV Community

Cover image for Entendendo @decorators no Python em 6 passos
Vitor Buxbaum Orlandi
Vitor Buxbaum Orlandi

Posted on • Updated on

Entendendo @decorators no Python em 6 passos

Decorators: um resumo

Quando falamos de decorators no Python, nos referimos à seguinte sintaxe:

@meu_decorator
def minha_funcao(param):
    ...
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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 de get_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 para mother_function (só pra facilitar as coisas)
  • adicionar um parâmetro em mother_function chamado mother_param
  • fazer my_function acessar mother_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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 como mother_param
  • fazer my_function chamar mother_param (que será final_function) passando my_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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Executando esse exemplo, a saída será somente:

Hello, Luiz!
Enter fullscreen mode Exit fullscreen mode

E aí, como você pensa em explorar o poder dos decorators?

Top comments (0)