Decorators em Python: o guia prático com functools.wraps

Entenda decorators em Python de verdade: por que funções são objetos, como o @ funciona por dentro, o wrapper com args e kwargs, functools.wraps e um decorator útil de medir tempo.

Revisado pela equipe editorial ValorFinalDocumentação oficial do Python (functools) e Python.org
Calcule agora: JSON FormatterResultado na hora, de graça e sem cadastro.

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 para funcao = nome(funcao).
  • Use *args, **kwargs no wrapper para aceitar qualquer função.
  • Sempre aplique @functools.wraps para 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, Bruno

Repare 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
# 5

Trê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 numeros

Com 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, Ana

Leia 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:

DecoratorVem deO que faz
@propertybuiltinsTransforma um método em atributo lido sem parênteses
@staticmethodbuiltinsMétodo que não recebe self nem cls, uma função presa à classe
@classmethodbuiltinsMétodo que recebe a classe (cls) em vez da instância
@functools.lru_cachefunctoolsGuarda em cache o retorno para os mesmos argumentos
@functools.wrapsfunctoolsCopia nome e documentação da função original para o wrapper
@dataclassdataclassesGera __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 recursivo

Casos 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 usoO que o decorator fazNome de exemplo
Log e rastreioRegistra quando a função entra e sai@registrar
CronômetroMede o tempo de execução@cronometrar
CacheReaproveita resultados já calculados@lru_cache
AutenticaçãoBarra a chamada sem permissão@exige_login
RetryRepete a chamada se der erro@tentar(3)
ValidaçãoConfere 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 positivo

Decorar 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

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

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.

Calculadoras deste guia

Leia também

Fontes oficiais

Links externos para os documentos oficiais consultados na construção desta página. O conteúdo deles pode mudar sem aviso; em caso de divergência, vale sempre a fonte oficial.

Como validamos os cálculos

Os valores citados neste guia são estimativos e baseados em fontes oficiais (Documentação oficial do Python (functools) e Python.org). Eles podem variar conforme convenção coletiva, situação individual e atualizações da legislação. Entenda nossa metodologia em como validamos os cálculos.

Perguntas frequentes

O que é um decorator em Python?
É uma função que recebe outra função e devolve uma versão dela com algo a mais, sem mexer no código original. Você escreve @nome em cima da função e, a partir daí, toda chamada passa antes pelo decorator. É o jeito de acrescentar comportamento repetido (medir tempo, registrar log, checar permissão) a várias funções sem copiar esse código em cada uma.
Para que serve o @ em cima de uma função?
O @nome é um atalho. Escrever @meu_decorator antes de def tarefa() é o mesmo que escrever tarefa = meu_decorator(tarefa) depois da definição. Os dois fazem a mesma coisa: substituem a função pela versão embrulhada pelo decorator. O @ só deixa isso mais legível e coloca a intenção logo acima da função.
Por que usar functools.wraps num decorator?
Quando você embrulha uma função, a de dentro perde o nome e a documentação originais: passam a ser os do wrapper. functools.wraps copia esses dados de volta (__name__, __doc__, __module__, __qualname__ e as anotações) e ainda cria __wrapped__ apontando para a função real. Sem ele, o help(), o debug e a introspecção mostram wrapper no lugar do nome certo. É uma linha que evita dor de cabeça, então use sempre.
Qual a diferença entre decorator e função normal?
Toda função normal recebe dados e devolve um resultado. O decorator é uma função que trabalha com outra função: recebe a função como argumento e devolve uma nova função. Isso é possível porque, em Python, funções são objetos que podem ser passados e retornados como qualquer valor. O decorator só aproveita esse fato.
Como criar um decorator que aceita argumentos, tipo @repetir(3)?
Você precisa de três funções aninhadas. A de fora recebe o argumento (o 3) e devolve o decorator de verdade; o decorator recebe a função e devolve o wrapper; o wrapper faz o trabalho. Quando você escreve @repetir(3), o Python primeiro chama repetir(3), que devolve um decorator, e só então esse decorator embrulha a função. Por isso o @repetir(3) tem parênteses e o @repetir simples não teria.
Em que ordem os decorators empilhados rodam?
De baixo para cima na aplicação. Se você tem @a e @b em cima de uma função f, o Python monta f = a(b(f)): o de baixo (b) embrulha primeiro, o de cima (a) embrulha por último. Na hora de chamar, o wrapper de cima roda primeiro e por dentro chama o de baixo. Trocar a ordem pode mudar o resultado, então empilhe com atenção.
Decorator deixa a função mais lenta?
Um pouco, porque toda chamada passa por uma função a mais (o wrapper). Na prática esse custo é minúsculo perto do trabalho da função em si e você nem percebe. A exceção é chamar uma função trivial milhões de vezes num laço apertado: aí a camada extra soma. Decorators de cache, como @lru_cache, quase sempre deixam o código mais rápido, não mais lento.
Dá para usar decorator em métodos de classe?
Sim. @property, @staticmethod e @classmethod são decorators embutidos feitos justamente para isso, e você pode escrever os seus. Um detalhe: em métodos, o primeiro argumento é o self, então o wrapper com *args, **kwargs já pega o self naturalmente sem tratamento especial.
O que é um decorator de classe?
É um decorator aplicado a uma classe inteira em vez de a uma função. O @ fica acima do def... perdão, acima do class, recebe a classe como argumento e devolve a classe (às vezes modificada). O @dataclass é o exemplo mais famoso: ele lê os atributos da classe e gera __init__, __repr__ e __eq__ para você. Isso está previsto na PEP 3129.
Qual o erro mais comum de quem começa com decorators?
Esquecer de retornar o wrapper no fim do decorator. Sem esse return, o decorator devolve None e a função decorada vira None, quebrando na primeira chamada. Os outros clássicos são não repassar o resultado da função original (perde o retorno) e esquecer o functools.wraps.
Preciso saber decorators para trabalhar com Python?
Para escrever scripts simples, não. Mas você vai encontrar decorators cedo ao usar frameworks: @app.route no Flask, @property em classes, @staticmethod, @pytest.fixture nos testes. Entender como funcionam por dentro faz esses símbolos deixarem de ser mágica e virarem ferramenta que você lê e escreve com confiança.
Onde pratico decorators sem instalar Python?
No navegador. O curso de Python avançado do ValorFinal traz um playground que roda o código na própria página, então você escreve decorators, testa com functools.wraps e vê a saída na hora, sem configurar ambiente. Escrever e quebrar o exemplo de propósito é a forma mais rápida de entender o mecanismo.