Decorator é uma função que embrulha outra função para acrescentar comportamento sem tocar no código original. É um dos recursos que mais aparece em Python de verdade (frameworks web, testes, classes) e que mais assusta quem vê o símbolo @ pela primeira vez. Este guia mostra o mecanismo por dentro, do porquê funções serem objetos até decorators com argumentos, empilhamento, decorators de classe e os embutidos da linguagem, sempre com código que roda. Para praticar com playground no navegador, exercícios e certificado, use o curso de Python avançado, gratuito.
Resposta rápida
- Um decorator recebe uma função e devolve outra com algo a mais.
@nomeé atalho parafuncao = nome(funcao).- Use
*args, **kwargsno wrapper para aceitar qualquer função. - Sempre aplique
@functools.wrapspara preservar nome e documentação.
Antes de tudo: funções são objetos de primeira classe
O decorator só faz sentido depois que você aceita uma ideia: em Python, função é um valor como qualquer outro. Dá para guardar uma função numa variável, passar como argumento e retornar de dentro de outra função. Isso é o que se chama de função de primeira classe, e é o alicerce de todo o resto deste guia.
def saudacao(nome):
return "Ola, " + nome
f = saudacao # guardando a funcao numa variavel
print(f("Ana")) # Ola, Ana
def executar(funcao, valor):
return funcao(valor) # recebendo uma funcao como argumento
print(executar(saudacao, "Bruno")) # Ola, BrunoRepare que f = saudacao não tem parênteses: você guarda a função, não o resultado dela. Com parênteses (saudacao("Ana")) você chama e recebe o retorno. Essa diferença entre a função e a chamada da função é o detalhe que faz decorator funcionar. Se função pode ser passada e retornada, então dá para escrever uma função que fabrica outra função. É exatamente isso que um decorator faz. Se você ainda está montando essa base, vale começar pelo guia de como aprender Python do zero.
O que é um decorator: a intuição de embrulhar
Imagine que você tem dez funções e quer que todas escrevam uma mensagem no log antes e depois de rodar. Você poderia copiar duas linhas de log dentro de cada uma, mas aí o mesmo código fica espalhado em dez lugares e qualquer ajuste vira dez edições. O decorator resolve isso de outro jeito: você escreve o comportamento extra uma vez, numa função que embrulha a original, e aplica esse embrulho onde quiser com @nome.
A palavra embrulhar (em inglês, wrap) é literal. O decorator devolve uma função nova que, por dentro, chama a sua função original e ainda faz algo antes, depois ou em volta dela. Quem usa a função nem percebe: o nome continua o mesmo, os argumentos continuam os mesmos. O que mudou foi o que acontece nos bastidores de cada chamada.
O primeiro decorator, passo a passo
Vamos criar um decorator que avisa antes e depois de a função rodar. Ele recebe a função original, define um wrapper por dentro que acrescenta as mensagens, e devolve esse wrapper:
def registrar(funcao):
def wrapper(*args, **kwargs):
print("Comecando", funcao.__name__)
resultado = funcao(*args, **kwargs)
print("Terminou", funcao.__name__)
return resultado
return wrapper
@registrar
def somar(a, b):
return a + b
print(somar(2, 3))
# Comecando somar
# Terminou somar
# 5Três coisas acontecem aí. Primeiro, registrar recebe a função somar. Segundo, ele define um wrapper que imprime as mensagens e, no meio, chama a função real e guarda o resultado. Terceiro, registrar devolve esse wrapper (sem parênteses: devolve a função, não o resultado). O @registrar em cima de somar é um atalho: é o mesmo que escrever somar = registrar(somar). A partir daí, chamar somar(2, 3) passa pelo wrapper.
Por que *args e **kwargs no wrapper
O wrapper precisa aceitar qualquer função, e cada função tem uma assinatura diferente: uma recebe dois números, outra recebe um texto, outra não recebe nada. Se você escrevesse def wrapper(a, b), o decorator só serviria para funções de exatamente dois argumentos. Os *args e **kwargs resolvem isso: o *args captura qualquer quantidade de argumentos posicionais numa tupla, e o **kwargs captura os argumentos nomeados num dicionário. Ao repassar funcao(*args, **kwargs), o wrapper entrega tudo intacto para a função original.
A regra prática é curta: todo wrapper de decorator genérico usa *args, **kwargs na definição e repassa *args, **kwargs na chamada. Fixar argumentos ali dentro é um dos erros mais comuns, porque limita a quais funções o decorator serve.
Preservando a identidade com functools.wraps
Há um efeito colateral no primeiro exemplo: depois de embrulhada, somar.__name__ vira "wrapper", porque a função visível agora é o wrapper. Isso confunde o help(), o debug e ferramentas de teste, que passam a mostrar wrapper no lugar do nome real. A biblioteca padrão resolve com functools.wraps:
import functools
def registrar(funcao):
@functools.wraps(funcao)
def wrapper(*args, **kwargs):
resultado = funcao(*args, **kwargs)
return resultado
return wrapper
@registrar
def somar(a, b):
"Soma dois numeros"
return a + b
print(somar.__name__) # somar (sem wraps, seria: wrapper)
print(somar.__doc__) # Soma dois numerosCom essa linha, o wrapper copia da função original o nome (__name__), a documentação (__doc__), o módulo, o nome qualificado e as anotações, e ainda cria um atributo __wrapped__ apontando para a função de dentro, útil para introspecção. É boa prática fixa: todo decorator que você escrever deve usar functools.wraps. Sem ele, o nome da função some no debug e você perde tempo procurando de onde vem o tal wrapper.
Um decorator útil: medir o tempo
Um caso real e comum é medir quanto uma função demora, sem espalhar código de cronômetro por todo lado:
import functools
import time
def cronometrar(funcao):
@functools.wraps(funcao)
def wrapper(*args, **kwargs):
inicio = time.perf_counter()
resultado = funcao(*args, **kwargs)
fim = time.perf_counter()
print(f"{funcao.__name__} levou {fim - inicio:.4f}s")
return resultado
return wrapper
@cronometrar
def somar_ate(n):
return sum(range(n))
somar_ate(1_000_000)Coloque @cronometrar em cima de qualquer função e ela passa a relatar o próprio tempo. Nenhuma linha da lógica original mudou. Esse é o ponto forte do decorator: separar o comportamento extra da regra de negócio. O time.perf_counter() é o relógio recomendado para medir intervalos, porque tem resolução alta e não sofre com ajustes de horário do sistema.
Decorators com argumentos: a fábrica de três níveis
Às vezes você quer configurar o decorator, como @repetir(3) para rodar a função três vezes. Aí precisa de uma função a mais por fora, porque o @repetir(3) é, na verdade, uma chamada: o Python executa repetir(3) primeiro e usa o que essa chamada devolve como decorator. Isso resulta em três funções aninhadas:
import functools
def repetir(vezes):
def decorator(funcao):
@functools.wraps(funcao)
def wrapper(*args, **kwargs):
resultado = None
for _ in range(vezes):
resultado = funcao(*args, **kwargs)
return resultado
return wrapper
return decorator
@repetir(3)
def cumprimentar(nome):
print("Ola,", nome)
cumprimentar("Ana")
# Ola, Ana
# Ola, Ana
# Ola, AnaLeia de fora para dentro. repetir(vezes) guarda o número e devolve o decorator. O decorator recebe a função e devolve o wrapper. O wrapper faz o laço. Cada nível tem um papel: o de fora carrega a configuração, o do meio é o decorator de verdade, o de dentro é o comportamento. É esse padrão de três níveis que aparece em @app.route("/home") do Flask ou em @pytest.fixture(scope="module").
Empilhando decorators: a ordem de aplicação
Você pode colocar mais de um decorator na mesma função. A regra é que a aplicação vai de baixo para cima: o decorator mais próximo do def embrulha primeiro.
import functools
def negrito(funcao):
@functools.wraps(funcao)
def wrapper(*args, **kwargs):
return "<b>" + funcao(*args, **kwargs) + "</b>"
return wrapper
def italico(funcao):
@functools.wraps(funcao)
def wrapper(*args, **kwargs):
return "<i>" + funcao(*args, **kwargs) + "</i>"
return wrapper
@negrito
@italico
def texto():
return "Ola"
print(texto()) # <b><i>Ola</i></b>O Python monta isso como texto = negrito(italico(texto)). O italico (de baixo) embrulha primeiro; o negrito (de cima) embrulha por último e fica por fora. Na hora de chamar, o de fora roda primeiro: o wrapper do negrito começa, chama o do italico, que chama o texto. Por isso a tag <b> fica por fora e a <i> por dentro. Trocar a ordem para @italico em cima muda o resultado para <i><b>Ola</b></i>, então empilhe com atenção.
Decorators de classe
O @ não serve só para funções. A PEP 3129 estendeu a sintaxe para classes: um decorator de classe recebe a classe como argumento e devolve uma classe, geralmente a mesma com algum ajuste. O uso clássico é registrar ou completar a classe:
registrados = {}
def registrar_classe(cls):
registrados[cls.__name__] = cls
return cls
@registrar_classe
class Usuario:
pass
print(registrados) # {'Usuario': <class '__main__.Usuario'>}O exemplo mais usado no dia a dia é o @dataclass, da biblioteca padrão. Ele lê os atributos que você declarou na classe e gera automaticamente o __init__, o __repr__ e o __eq__, poupando código repetitivo. Se você quer entender classes a fundo antes dos decorators de classe, o guia de programação orientada a objetos em Python cobre classe, objeto, herança e polimorfismo.
Os decorators embutidos do Python
Antes de escrever os seus, vale conhecer os que já vêm com a linguagem. Você usa a maioria deles dentro de classes:
| Decorator | Vem de | O que faz |
|---|---|---|
@property | builtins | Transforma um método em atributo lido sem parênteses |
@staticmethod | builtins | Método que não recebe self nem cls, uma função presa à classe |
@classmethod | builtins | Método que recebe a classe (cls) em vez da instância |
@functools.lru_cache | functools | Guarda em cache o retorno para os mesmos argumentos |
@functools.wraps | functools | Copia nome e documentação da função original para o wrapper |
@dataclass | dataclasses | Gera __init__, __repr__ e __eq__ a partir dos atributos |
O @property e o @lru_cache merecem um exemplo rápido. O primeiro deixa um cálculo parecer atributo; o segundo faz a função lembrar resultados que já calculou, o que acelera muito funções recursivas:
import functools
class Circulo:
def __init__(self, raio):
self.raio = raio
@property
def area(self):
return 3.14159 * self.raio ** 2
c = Circulo(2)
print(c.area) # 12.56636 (lido sem parenteses)
@functools.lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # instantaneo, mesmo sendo recursivoCasos de uso reais
Decorators aparecem sempre que um mesmo comportamento precisa envolver várias funções. A tabela reúne os usos mais frequentes:
| Caso de uso | O que o decorator faz | Nome de exemplo |
|---|---|---|
| Log e rastreio | Registra quando a função entra e sai | @registrar |
| Cronômetro | Mede o tempo de execução | @cronometrar |
| Cache | Reaproveita resultados já calculados | @lru_cache |
| Autenticação | Barra a chamada sem permissão | @exige_login |
| Retry | Repete a chamada se der erro | @tentar(3) |
| Validação | Confere os argumentos antes de rodar | @validar_positivo |
Vale ver dois deles em código. O retry usa a fábrica de três níveis para aceitar o número de tentativas, e a validação para na entrada se o argumento não fizer sentido:
import functools
import time
def tentar(vezes, espera=1):
def decorator(funcao):
@functools.wraps(funcao)
def wrapper(*args, **kwargs):
for tentativa in range(1, vezes + 1):
try:
return funcao(*args, **kwargs)
except Exception as erro:
if tentativa == vezes:
raise
print(f"Tentativa {tentativa} falhou: {erro}")
time.sleep(espera)
return wrapper
return decorator
def validar_positivo(funcao):
@functools.wraps(funcao)
def wrapper(numero):
if numero < 0:
raise ValueError("O numero precisa ser positivo")
return funcao(numero)
return wrapper
@validar_positivo
def raiz(numero):
return numero ** 0.5
print(raiz(9)) # 3.0
print(raiz(-1)) # ValueError: O numero precisa ser positivoDecorar não é a mesma coisa que chamar
Uma confusão comum é achar que @registrar e registrar(somar) num lugar qualquer fazem coisas diferentes. Fazem o mesmo: os dois aplicam o decorator. A diferença está em outro par. Um decorator sem argumentos usa @registrar (sem parênteses), porque o Python já vai passar a função para ele. Um decorator com argumentos usa @repetir(3) (com parênteses), porque aí você chama repetir(3) primeiro e o retorno é que vira o decorator.
Trocar isso é fonte garantida de erro. Escrever @repetir sem os parênteses passa a própria função como se fosse o número de repetições; escrever @registrar() com parênteses tenta chamar registrar sem argumento e quebra. A pergunta a fazer é simples: esse decorator precisa de configuração? Se sim, parênteses; se não, sem parênteses.
Erros comuns com decorators
- Esquecer de retornar o
wrapperno fim do decorator, o que faz a função decorada virarNonee quebrar na primeira chamada. - Não repassar o retorno da função original (esquecer o
returnantes defuncao(*args, **kwargs)), perdendo o resultado da chamada. - Fixar argumentos no wrapper em vez de usar
*args, **kwargs, o que limita a quais funções o decorator serve. - Deixar de usar
functools.wrapse depois estranhar por que o nome da função sumiu no debug e nohelp(). - Empilhar decorators na ordem errada, esquecendo que o de baixo embrulha primeiro e o de cima roda primeiro.
- Misturar decorator com e sem argumento, escrevendo
@repetirquando devia ser@repetir(3)e vice-versa.
Performance: o custo de um decorator
Todo decorator adiciona uma camada: cada chamada da função decorada passa antes pelo wrapper. Esse custo existe, mas é pequeno, da ordem de nanossegundos por chamada, e some no ruído perto de qualquer trabalho útil que a função faça. Você só nota o overhead num caso extremo: uma função trivial chamada milhões de vezes dentro de um laço apertado, onde a camada extra passa a pesar em relação ao trabalho quase nulo da função.
Na outra ponta, decorators de cache como @lru_cache costumam deixar o programa mais rápido, não mais lento, porque evitam recalcular o que já foi calculado. A conta quase sempre fecha a favor do decorator: você troca um custo minúsculo por código mais limpo e sem repetição. Se desconfiar de gargalo, meça antes com o próprio @cronometrar deste guia, em vez de otimizar no escuro.
Limitações
Decorator não é bala de prata. Ele torna o fluxo menos óbvio: quem lê somar(2, 3) não vê que existe um wrapper no meio, então empilhar muitos decorators pode esconder o que realmente acontece. Sem functools.wraps, a introspecção fica confusa. E decorator que muda a assinatura da função (que aceita argumentos diferentes dos que repassa) costuma trazer mais dor do que ganho. Use quando o mesmo comportamento se repete em várias funções; para uma função só e um caso único, um if ou uma função auxiliar direta é mais claro.
Fontes oficiais
- Documentação oficial do Python: glossário (decorator): a definição de referência da linguagem.
- Módulo functools (functools.wraps e lru_cache): referência da biblioteca padrão usada aqui.
- PEP 318: Decorators for Functions and Methods: a proposta que introduziu a sintaxe @ (com a PEP 3129 estendendo para classes).
O curso de Python avançado aprofunda decorators, geradores, context managers e mais, com playground no navegador, exercícios e certificado, sem custo. Se ainda faltam as bases de classes e coleções, o curso de Python intermediário vem antes na trilha, e o curso de Python do zero abre a sequência.
Conclusão
Decorator é uma função que recebe outra e devolve uma versão com algo a mais. O @ é só um atalho para funcao = decorator(funcao), o *args, **kwargs deixa o wrapper servir a qualquer função, o functools.wraps preserva o nome original e a fábrica de três níveis dá conta dos decorators com argumentos. Empilhados, eles aplicam de baixo para cima; aplicados a classes, geram código como o @dataclass. Depois de escrever um cronômetro, um retry e um cache como decorator, os @ dos frameworks deixam de ser mistério. Para continuar, veja o guia de listas em Python, o guia de como aprender programação do zero e a categoria de guias e ferramentas de Tecnologia. Para praticar com playground, exercícios e certificado gratuitos, use o curso de Python avançado e conheça todos os cursos gratuitos do ValorFinal. Veja também como validamos os cálculos e o conteúdo do portal.