Entendendo @decorators no Python em 6 passos
Tags: braziliandevs, python
Published: 2023-11-14 21:58:02

## Decorators: um resumo

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

```python
@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 😉

```python
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:

```python
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:

```python
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`.

```python
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 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_)

```python
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 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`

```python
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:

```python
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:

```python
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?