Módulo 5 - Decoradores avançados

Casos reais: log, cronômetro, cache, validação e retry

11 min de leitura · por Cesar Gargiulo, revisado pela equipe ValorFinal e GuardiaSec · Atualizado em 01/07/2026

O que você vai aprender

  • Escrever um decorador de log e um de cronômetro do zero.
  • Usar o functools.lru_cache para memoização pronta da biblioteca padrão.
  • Criar um decorador de validação que rejeita argumentos inválidos.
  • Montar um decorador de retry configurável para operações que podem falhar.

Log e cronômetro, os dois clássicos

Os dois decoradores mais comuns de todos são o de log e o de cronômetro, e não é por acaso: ambos acrescentam observação sem mudar o que a função faz. O de log registra que a função foi chamada e com quais argumentos, útil para acompanhar um sistema em funcionamento. O de cronômetro mede o tempo decorrido, ponto de partida de qualquer investigação de performance. Os dois seguem o mesmo esqueleto que você já domina, com wraps no lugar.

import functools
import time

def log(funcao):
    @functools.wraps(funcao)
    def wrapper(*args, **kwargs):
        print(f"-> {funcao.__name__}{args}")
        resultado = funcao(*args, **kwargs)
        print(f"<- {funcao.__name__} devolveu {resultado!r}")
        return resultado
    return wrapper

def cronometrar(funcao):
    @functools.wraps(funcao)
    def wrapper(*args, **kwargs):
        inicio = time.perf_counter()
        resultado = funcao(*args, **kwargs)
        duracao = time.perf_counter() - inicio
        print(f"{funcao.__name__} levou {duracao:.4f}s")
        return resultado
    return wrapper

@cronometrar
@log
def somar(a, b):
    return a + b

somar(2, 3)

Log e cronômetro empilhados: o cronômetro por fora mede tudo, o log por dentro registra.

Repare em dois detalhes profissionais. O time.perf_counter é o relógio recomendado para medir intervalos curtos, mais preciso que o time.time para essa finalidade. E o !r dentro da f-string chama o repr do valor, mostrando o resultado de um jeito sem ambiguidade nos logs, com aspas em textos, por exemplo. São pequenos cuidados que separam um decorador de exemplo de um decorador de produção.

Cache pronto com lru_cache

Para cache, a regra de ouro é não reinventar a roda. A biblioteca padrão já traz o functools.lru_cache, um decorador que memoiza os resultados: na primeira vez com certos argumentos ele calcula e guarda; nas próximas com os mesmos argumentos, devolve o valor guardado na hora. O exemplo clássico é a sequência de Fibonacci recursiva, que sem cache recalcula os mesmos valores milhões de vezes e com cache fica instantânea. Basta uma linha por cima da função.

import functools

@functools.lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(35))  # 9227465, praticamente instantaneo com cache
print(fib.cache_info())  # estatisticas de acertos e faltas do cache

Uma linha, @functools.lru_cache, transforma a recursão lenta em instantânea.

Validação e retry, decoradores configuráveis

Os dois últimos casos usam a fábrica de decoradores da aula 2, porque precisam de configuração. Um decorador de validação verifica os argumentos antes de deixar a função rodar e levanta um erro claro se algo estiver fora do combinado, centralizando uma checagem que se repetiria em muitas funções. Um decorador de retry tenta de novo quando a função falha, útil para operações instáveis por natureza, e recebe quantas tentativas fazer. Ambos reúnem tudo do módulo: wraps, fábrica e tratamento de erro.

import functools
import time

def tentar_de_novo(tentativas=3, espera=0.5):
    def decorador(funcao):
        @functools.wraps(funcao)
        def wrapper(*args, **kwargs):
            ultimo_erro = None
            for tentativa in range(1, tentativas + 1):
                try:
                    return funcao(*args, **kwargs)
                except Exception as erro:
                    ultimo_erro = erro
                    print(f"tentativa {tentativa} falhou: {erro}")
                    time.sleep(espera)
            raise ultimo_erro
        return wrapper
    return decorador

@tentar_de_novo(tentativas=3, espera=1.0)
def operacao_instavel():
    raise ConnectionError("servidor fora do ar")

# Tenta 3 vezes, espera 1s entre elas, e por fim relanca o erro

Retry como fábrica de decoradores: configura tentativas e espera, relança o erro no fim.

Note o cuidado no retry: ele guarda o último erro e, se todas as tentativas falharem, relança esse erro em vez de engolir a falha em silêncio. Esconder um erro é pior do que deixá-lo aparecer. Esse é o tipo de decisão que o Módulo 12, sobre exceções, aprofunda. Por ora, o que importa é reconhecer que esses cinco decoradores, log, cronômetro, cache, validação e retry, cobrem uma fatia enorme das necessidades reais, e todos saem das técnicas deste módulo.

Teste rápido

Qual é a forma recomendada de adicionar cache (memoização) a uma função pura em Python?

Perguntas frequentes

Por que usar time.perf_counter em vez de time.time no cronômetro?
O perf_counter é um relógio de alta resolução feito para medir intervalos, com mais precisão e sem sofrer com ajustes do relógio do sistema. O time.time devolve a hora do relógio de parede, que pode até andar para trás com uma sincronização. Para cronometrar trechos de código, perf_counter é a escolha certa.
O lru_cache serve para qualquer função?
Não. Ele exige que os argumentos sejam hasháveis, ou seja, números, textos e tuplas servem, mas listas e dicionários não. E a função precisa ser pura: para os mesmos argumentos, sempre o mesmo resultado, sem efeitos colaterais. Memoizar uma função que lê o horário atual ou grava em disco daria resultados errados.
O que o decorador de retry deve fazer depois de esgotar as tentativas?
Relançar o último erro, não escondê-lo. Se a operação falhou em todas as tentativas, quem chamou precisa saber para tratar a situação. Engolir a exceção em silêncio cria bugs difíceis de rastrear. O padrão é guardar o último erro capturado e, ao fim do laço, executar raise para propagá-lo.
Vale a pena escrever meu próprio decorador de cache?
Só em casos especiais, como precisar de uma política de expiração por tempo que o lru_cache não oferece. Para memoização comum, o lru_cache da biblioteca padrão é testado, rápido e vem com estatísticas via cache_info(). A regra é preferir a ferramenta pronta e só partir para a sua quando houver um requisito que ela não cobre.
Posso empilhar esses decoradores reais entre si?
Pode, e é comum. Cronometrar por fora e logar por dentro, por exemplo, é uma combinação frequente. Só lembre da ordem de aplicação: o de cima envolve o resultado do de baixo. Pense no que precisa medir ou registrar em cada camada e monte a pilha de propósito, como você viu na aula sobre empilhamento.

Fontes

Seu progresso fica salvo neste aparelho. Assinantes sincronizam entre os aparelhos.