Ir para o conteúdo

Como criar decoradores com parâmetros?

Os decoradores (decorators) são muito poderosos e práticos. Eles permitem modificar ou melhorar uma função sem alterar seu código internamente. Isso permite que você mantenha a sua função original intacta e, ao mesmo tempo, adicione novos comportamentos a ela.

Se você vem de outras linguagens pode ser que já tenha visto os decoradores com outro nome, como anotações ou wrappers.

Casos de uso#

Existem muitos casos de uso para decoradores. Alguns exemplos são:

  • Autenticação
  • Cache
  • Log
  • Validação
  • Tempo de execução

Criando um simples decorador#

Para criar um decorador, você precisa de uma função que receba outra função como parâmetro e retorne uma função. Vamos ver um exemplo:

def decorador(funcao):
    def wrapper():
        print('Antes da função')
        saida = funcao()
        print('Depois da função')
        return saida
    return wrapper

Agora vamos criar uma função que utilize o decorador:

main.py
@decorador
def exemplo():
    print('Função')

Nota

Observe que para utilizar o decorador, basta colocar o arroba e nome dele acima da função que você quer decorar.

O código completo fica assim:

main.py
def decorador(funcao):
    def wrapper():
        print('Antes da função')
        saida = funcao()
        print('Depois da função')
        return saida
    return wrapper

@decorador
def exemplo():
    print('Função')


if __name__ == '__main__':
    exemplo()

Ao executar este arquivo você verá o seguinte resultado:

$ python3 main.py
Antes da função
Função
Depois da função

Nota

Observe que a função decorador recebe uma função como parâmetro, essa função é a que vem logo abaixo do @decorador.

Decorando uma função com parâmetros#

Nos exemplos anteriores, nós decoramos uma função bem simples, mas você pode querer decorar uma função que recebe parâmetros. Para isso, basta adicionar os parâmetros na função wrapper:

main2.py
def decorador(funcao):
    def wrapper(*args, **kwargs):
        print('Antes da função')
        saida = funcao(*args, **kwargs)
        print('Depois da função')
        return saida

    return wrapper

@decorador
def exemplo(nome):
    print(f'Função {nome}')

if __name__ == '__main__':
    exemplo('Lorem')

Ao executar este arquivo você verá o seguinte resultado:

$ python3 main2.py
Antes da função
Função Lorem
Depois da função

Criando um decorador com parâmetros#

Agora chegamos na parte mais interessante deste artigo, criar um decorador com parâmetros. Para isso, vamos criar uma função que recebe os parâmetros do decorador e retorna uma função que recebe a função que será decorada:

def decorador(parametro1, parametro2):
    def decorador_antigo(funcao):
        def wrapper(*args, **kwargs):
            print(f'Antes da função {parametro1} {parametro2}')
            saida = funcao(*args, **kwargs)
            print(f'Depois da função {parametro1} {parametro2}')
            return saida
        return wrapper
    return decorador_antigo

Observe que a função decorador_antigo é basicamente a mesma coisa da função decorador dos exemplos anteriores, a única diferença é que agora ela está dentro de outra função (decorador).

Você pode estar vendo este exemplo e se perguntando em como isso seria útil, veremos isso mais a frente.

Exemplo 1#

Suponha que você deseja saber quanto tempo uma função demora para ser executada. Para isso, vamos criar um decorador para não ter que modificar a função e além disso o decorador irá receber a unidade de tempo que será exibida.

A ideia é que utilizemos o decorador da seguinte forma:

@timeit(unidade='ms')
def exemplo():
    time.sleep(30)

if __name__ == '__main__':
    exemplo()

Para isso o decorador ficará assim:

import time

def timeit(unidade):
    def decorador_antigo(funcao):
        def wrapper(*args, **kwargs):
            tempo_inicial = time.time()
            saida = funcao(*args, **kwargs)
            tempo_final = time.time()

            duracao = tempo_final - tempo_inicial

            if unidade == 'ms':
                print(f'Tempo de execução: {round(duracao * 1000, 2)} ms')
            elif unidade == 's':
                print(f'Tempo de execução: {round(duracao, 2)} s')
            elif unidade == 'min':
                print(f'Tempo de execução: {round(duracao / 60, 2)} min')
            return saida
        return wrapper
    return decorador_antigo

O código completo ficará assim:

exemplo1.py
import time

def timeit(unidade):
    def decorador_antigo(funcao):
        def wrapper(*args, **kwargs):
            tempo_inicial = time.time()
            saida = funcao(*args, **kwargs)
            tempo_final = time.time()

            duracao = tempo_final - tempo_inicial

            if unidade == 'ms':
                print(f'Tempo de execução: {round(duracao * 1000, 2)} ms')
            elif unidade == 's':
                print(f'Tempo de execução: {round(duracao, 2)} s')
            elif unidade == 'min':
                print(f'Tempo de execução: {round(duracao / 60, 2)} min')
            return saida
        return wrapper
    return decorador_antigo

@timeit(unidade='ms')
def exemplo():
    time.sleep(30)

if __name__ == '__main__':
    exemplo()

Ao executar este arquivo você verá o seguinte resultado:

$ python3 main3.py
Tempo de execução: 0.50 min

Nota

Observe que o decorador timeit recebe um parâmetro unidade, que é utilizado para definir a unidade de tempo que será exibida no final da execução da função decorada.

Exemplo 2#

Um outro caso e que é comum é em APIs são decoradores para roteamento. Vamos criar um decorador que recebe o caminho da rota e o método HTTP que será utilizado.

A ideia é que utilizemos o decorador da seguinte forma:

@route('/usuarios', metodos=['GET', 'POST'])
def minha_rota():
    pass

Para isso o decorador ficará assim:

def route(caminho, metodos):
    def decorador_antigo(funcao):
        def wrapper(*args, **kwargs):
            print(f'Executando rota {caminho} com o método {metodos}')
            saida = funcao(*args, **kwargs)
            return saida
        return wrapper
    return decorador_antigo

O código completo ficará assim:

exemplo2.py
def route(caminho, metodos):
    def decorador_antigo(funcao):
        def wrapper(*args, **kwargs):
            print(f'Executando rota {caminho} com o método {metodos}')
            saida = funcao(*args, **kwargs)
            return saida
        return wrapper
    return decorador_antigo

@route('/usuarios', metodos=['GET', 'POST'])
def minha_rota():
    pass

Veja que acabamos de criar um decorador bem parecido com o que o Flask utiliza para criar rotas. Nós não fizemos nada demais, mas serve de inspiração para você criar seus próprios decoradores.