Se você escreve Python há algum tempo, já usou decorators sem perceber. O @app.route do Flask, o
@pytest.mark.parametrize, o @dataclass da stdlib, o @property nativo da linguagem — todos são decorators. Eles
aparecem em todo framework relevante do ecossistema, mas a maioria dos recursos disponíveis explica como usar sem
explicar por que funciona.
Este artigo corrige isso.
A ideia aqui não é ensinar a sintaxe do @. É mostrar o mecanismo embaixo: o que Python faz quando encontra esse
símbolo, como construir um decorator do zero com segurança e como evitar as armadilhas que só aparecem em produção.
1. Pré-requisito: Funções São Objetos de Primeira Classe
Antes de entender decorators, é preciso internalizar um conceito que Python respeita de forma consistente: funções são objetos como qualquer outro.
Isso significa que uma função pode ser atribuída a uma variável, passada como argumento para outra função, e retornada como resultado de uma chamada. Se isso parece óbvio, bem. Mas as consequências disso são o alicerce inteiro do decorator pattern.
Funções podem ser atribuídas a variáveis:
| |
Funções podem ser passadas como argumento:
| |
Funções podem ser retornadas por outras funções:
| |
Esse último exemplo tem um nome técnico: closure. A função interna mensagem “fecha sobre” a variável nome do
escopo externo e a mantém viva mesmo depois que criar_saudacao terminou de executar. O Python preserva esse contexto
enquanto houver uma referência à função interna.
Closures são o mecanismo que permite decorators funcionarem. Quando você entende closures, a mecânica do @ deixa de
ser mágica e passa a ser consequência natural.
2. A Mecânica do Decorator — Desvendando o @
Com closures claras, o decorator se torna trivial de entender: @decorator é açúcar sintático para uma atribuição.
| |
O Python transforma isso exatamente em:
| |
É tudo. Não existe nenhuma magia adicional. O símbolo @ instrui o interpretador a passar a função definida logo abaixo
como argumento para meu_decorator e a reatribuir o resultado de volta ao mesmo nome.
Para deixar isso concreto, veja o primeiro decorator possível — sem usar @, para tornar o mecanismo explícito:
| |
Agora a mesma coisa com a sintaxe @ — o resultado é idêntico:
| |
O @ é apenas uma forma mais limpa de escrever processar = logar(processar). Reconhecer isso é o que permite
raciocinar sobre qualquer decorator, não importa o quão complexo ele pareça.
3. Anatomia de um Decorator Bem Formado
O exemplo acima funciona, mas tem um problema: só aceita funções sem argumentos. Em produção, os decorators precisam ser transparentes — funcionar com qualquer assinatura de função, independentemente de quantos parâmetros ela receba.
Este é o template canônico:
| |
Cada ponto merece atenção:
(a) @functools.wraps(func) preserva os metadados da função original no wrapper. O motivo completo merece uma seção
própria — e vai ter uma logo adiante.
(b) *args, **kwargs garante que o wrapper aceita qualquer combinação de argumentos posicionais e nomeados,
repassando-os intactos para a função original. Sem isso, o decorator só funciona com funções de assinatura idêntica à do
wrapper.
(c) e (e) São os pontos onde a lógica do decorator vive: logging, validação, timing, cache — tudo entra aqui.
(d) func(*args, **kwargs) chama a função original com os mesmos argumentos recebidos. Note que a variável func
vem do escopo externo — isso é a closure em ação.
(f) return resultado é crítico. Um decorator que não retorna o valor da função original “engole” o retorno
silenciosamente. Se processar_pedido retorna uma lista de itens e o decorator não faz return resultado, o chamador
recebe None.
(g) return wrapper está fora do corpo do wrapper. O decorator retorna a função wrapper — não a chama. É essa
distinção que faz o mecanismo funcionar: ao escrever @meu_decorator, Python substitui o nome da função pelo wrapper
retornado aqui.
Exemplo completo com timer:
| |
time.perf_counter() é preferível a time.time() para medições de performance: tem resolução mais alta e não sofre
ajustes de relógio do sistema.
4. O Problema da Identidade — Por que functools.wraps É Obrigatório
Há um detalhe sutil que cobra um preço alto quando ignorado. Observe:
| |
Após a decoração, calcular_total aponta para o objeto wrapper. Sem nenhum cuidado adicional, __name__, __doc__,
__annotations__ e outros atributos são os do wrapper — não os da função original. O nome que aparece em stack traces,
em ferramentas de documentação automática como Sphinx, em pytest markers e no help() interativo é wrapper.
Em um projeto com dezenas de funções decoradas, todo stack trace em produção vai apontar para wrapper em vez de
indicar a função real com problema. O custo de debugging aumenta desnecessariamente.
functools.wraps resolve isso. Ele é um decorator aplicado ao wrapper que copia os atributos relevantes da função
original:
| |
Internamente, functools.wraps é um atalho para functools.update_wrapper(wrapper, func). Os atributos transferidos
são:
| Atributo | O que representa |
|---|---|
__name__ | Nome da função — aparece em stack traces e repr |
__qualname__ | Nome qualificado — inclui classe e módulo, para contexto exato |
__doc__ | Docstring — essencial para help(), Sphinx e IDEs |
__module__ | Módulo de origem — identifica onde a função foi definida |
__annotations__ | Type hints — necessário para mypy e ferramentas de análise estática |
__dict__ | Atributos customizados — preserva metadados adicionados à função |
__wrapped__ | Referência direta à função original — adicionado pelo wraps |
O atributo __wrapped__ merece destaque: ele permite “desembrulhar” a cadeia de decorators e acessar a função original
diretamente, o que é útil em testes e introspecção.
| |
A regra é simples: todo decorator deve usar @functools.wraps(func) no wrapper interno, sem exceção. O custo é
zero, o benefício é real.
5. Decorators com Argumentos — A Fábrica de Decorators
Até aqui, os decorators recebem apenas a função como argumento. Mas muitos dos decorators mais úteis precisam de
configuração: @retry(max_tentativas=3), @cache(ttl=60), @permissao_requerida("admin").
Ao adicionar parênteses ao decorator, o comportamento muda completamente — e é aqui que a maioria dos tutoriais perde o leitor.
A confusão vem do seguinte: @repetir(vezes=3) não está chamando um decorator. Está chamando uma
fábrica de decorator — uma função que, ao ser chamada com os argumentos de configuração, retorna o decorator de
verdade.
A estrutura tem três camadas:
| |
Para entender o que acontece passo a passo, expanda a sintaxe @:
| |
A variável vezes fica capturada pela closure do wrapper, que a usa em cada chamada de notificar.
A regra para identificar quantas camadas um decorator precisa é direta: decorator sem parênteses = uma função que
recebe func; decorator com parênteses = uma função que recebe os argumentos e retorna uma função que recebe func.
6. Stacking — Empilhando Decorators e a Ordem de Execução
Python permite empilhar múltiplos decorators sobre uma mesma função. A ordem em que eles aparecem determina o comportamento — e errar essa ordem pode introduzir bugs silenciosos que só aparecem em produção.
| |
A regra de ouro: a aplicação é de baixo para cima (o decorator mais próximo da função é aplicado primeiro), mas a execução em runtime é de cima para baixo (o decorator mais externo executa primeiro).
Para tornar isso concreto, considere dois decorators em um endpoint de API:
| |
Cenário A — @logar acima de @autenticar:
| |
O log registra a tentativa de acesso mesmo quando o usuário não tem permissão. Em alguns sistemas, isso é o comportamento correto — registrar toda tentativa, incluindo as negadas.
Cenário B — @autenticar acima de @logar:
| |
Aqui, a autenticação bloqueia antes do log registrar qualquer coisa. Apenas chamadas autenticadas chegam ao log.
Ambos os comportamentos podem ser desejados, dependendo do requisito. O ponto é que a ordem define o comportamento, e
não há nada no código que sinalize a diferença visualmente além da posição do @. É uma decisão arquitetural que
precisa ser documentada.
7. Decorators Baseados em Classe — Quando o Estado Importa
Até agora, todos os decorators foram funções. Mas Python permite usar classes como decorators também — e elas se tornam a escolha certa quando o decorator precisa manter estado entre chamadas.
Um contador de invocações é o exemplo mais direto:
| |
O @Contador sobre buscar_usuario é equivalente a buscar_usuario = Contador(buscar_usuario). O construtor
__init__ recebe a função, __call__ é executado cada vez que a função decorada é chamada, e o estado
(self.chamadas) persiste no objeto.
O que update_wrapper realmente faz numa instância de classe
Aqui vale parar e ser preciso, porque há uma nuance importante que a maioria dos tutoriais ignora.
functools.update_wrapper(self, func) copia atributos como __name__, __qualname__, __doc__ e __annotations__
da função original para o objeto instância — não para a classe Contador. Isso significa que a introspecção
programática funciona corretamente:
| |
Porém, o __repr__ padrão de um objeto em Python é gerado pela classe, não pela instância. E a classe Contador
não sabe nada sobre __name__ — ela simplesmente herda o __repr__ de object, que produz:
| |
Não <function buscar_usuario at 0x...>, como seria com um decorator de função. O update_wrapper não tem como alterar
isso: atributos de instância não têm efeito sobre o __repr__ padrão da classe.
Para fins práticos do dia a dia — pytest, mypy, Sphinx, logging, stack traces — isso raramente é problema: todas essas
ferramentas usam __name__ e __qualname__ diretamente, e esses atributos estão corretos. O __repr__ entra em cena
principalmente no REPL interativo e em sessões de debug — exatamente onde um repr que “mente” pode confundir mais do que
ajudar.
A solução correta: __repr__ que comunica a realidade
O caminho certo não é imitar o repr de uma função — é comunicar a natureza real do objeto, incluindo o estado que só um decorator de classe pode ter:
| |
Isso honra os dois requisitos ao mesmo tempo: __name__ e __qualname__ continuam disponíveis para introspecção
programática via update_wrapper, e o repr comunica o que o objeto realmente é — um decorator com estado — em vez de
fingir ser uma função simples.
A distinção importa especialmente quando o decorator carrega estado observável. Um repr que oculta chamadas, cache,
ou qualquer outro estado interno priva o desenvolvedor de informação útil no momento em que ele mais precisa dela:
durante o debug.
Quando usar cada abordagem:
| Situação | Escolha |
|---|---|
| Comportamento puro sem estado (log, timer, validação) | Decorator de função |
| Estado entre chamadas (contador, cache, rate limiter) | Decorator de classe com __repr__ explícito |
| Lógica configurável via argumentos | Fábrica de decorators |
8. Padrões de Produção — Exemplos Prontos para Usar
Com a mecânica compreendida, esta seção apresenta três decorators que resolvem problemas reais e podem ser adaptados diretamente em projetos.
8.1 Retry Automático com Backoff
Chamadas a serviços externos falham. Redes instáveis, timeouts, rate limiting — são situações normais em produção. Um decorator de retry encapsula a lógica de re-tentativa sem poluir o código de negócio:
| |
O parâmetro excecoes permite especificar quais exceções devem acionar o retry. Erros de programação como ValueError
ou TypeError não devem ser re-tentados — por isso o padrão não é Exception para tudo.
8.2 Cache por Memoização
Funções que recebem os mesmos argumentos e produzem sempre o mesmo resultado são candidatas à memoização. O decorator abaixo ilustra a lógica antes de introduzir a solução da stdlib:
| |
Em projetos reais, use @functools.lru_cache(maxsize=128) ou @functools.cache (Python 3.9+) — são implementações da
stdlib com controle de tamanho, thread safety e suporte a kwargs. O decorator manual acima serve para compreender o
mecanismo antes de usar a versão pronta.
8.3 Validação de Argumentos
Validações de entrada que se repetem em múltiplas funções são candidatas a serem extraídas para um decorator. Isso reduz duplicação e, como consequência direta, reduz a complexidade ciclomática de cada função — o que já discutimos no artigo sobre Radon.
| |
Cada função de negócio ficou com uma única responsabilidade — o decorator cuidou da guarda de entrada.
9. Armadilhas Comuns — O que Costuma Dar Errado
Engolir o retorno da função original
| |
O Python não avisa sobre isso. A função executa normalmente, mas o valor retornado some. Sempre use
return func(*args, **kwargs) ou armazene em variável antes de retornar.
Esquecer functools.wraps
Já detalhado na seção 4. O custo de depurar stack traces cheios de wrapper em produção é muito maior do que adicionar
uma linha ao decorator.
Decorar métodos de instância sem considerar self
Decorators que inspecionam o primeiro argumento precisam de atenção ao ser aplicados a métodos:
| |
O self entra como args[0], empurrando os argumentos reais para args[1] em diante. Decorators de função que assumem
args[0] como primeiro argumento do usuário quebram silenciosamente ao serem aplicados a métodos.
Stacking na ordem errada: Como demonstrado na seção 6, inverter a posição de @autenticar e @logar produz
comportamentos diferentes. Sem um comentário que documente a intenção, a ordem parece arbitrária para quem lê o código
depois.
10. Checklist de Boas Práticas
| # | Prática | Por quê |
|---|---|---|
| 1 | Sempre use @functools.wraps(func) no wrapper interno | Preserva identidade da função em stack traces, docs e ferramentas |
| 2 | Use *args, **kwargs no wrapper | Garante compatibilidade com qualquer assinatura de função |
| 3 | Sempre retorne resultado = func(...) / return resultado | Evita engolir retornos silenciosamente |
| 4 | Prefira decorator de função para comportamento puro | Mais simples, sem overhead de classe |
| 5 | Use decorator de classe quando precisar de estado entre chamadas | self é o lugar natural para manter estado |
| 6 | Documente o decorator com docstring | Descreva o que ele adiciona, não o que a função faz |
| 7 | Em stacking, coloque o decorator mais específico mais próximo da função | Torna a cadeia de transformações previsível |
| 8 | Especifique as exceções no retry, não use Exception para tudo | Evita re-tentativas em erros de programação |
| 9 | Em decorators de classe, use functools.update_wrapper(self, func) | Equivalente ao @wraps para instâncias |
| 10 | Documente a ordem em stacking quando ela for semanticamente relevante | Quem lê o código não deve ter que raciocinar sobre a ordem |
11. Conclusão
Decorators não são mágica. São closures com açúcar sintático — e a sintaxe @ é apenas uma forma elegante de escrever funcao = decorator(funcao).
Entender isso abre um caminho direto para duas habilidades práticas: saber ler qualquer decorator existente em frameworks e bibliotecas, e saber construir os seus com a estrutura correta desde o início.
Há uma conexão direta com outros princípios já explorados aqui no blog. O functools.wraps é a materialização do princípio de nomear pelo propósito — sem ele, __name__ mente para toda ferramenta que depende do nome da função. E decorators que extraem lógica transversal — retry, log, validação, cache — reduzem a complexidade ciclomática das funções de negócio, exatamente o que o radon mediria como melhoria no artigo sobre CC.
Um decorator bem escrito é invisível: a função de negócio comunica sua intenção, e o comportamento adicional está encapsulado, testável e reutilizável. É código que se explica por si só — e isso é poder puro na engenharia de software.
Se este artigo te fez repensar como você aplica comportamento transversal no seu código, compartilhe o decorator mais criativo que já escreveu: @riverfount@bolha.us