Fundamentos

Decoradores em Python: o que são, como funcionam e como criar o seu

Atualizado: · Skillaria.Top

Decorador em Python é uma função que recebe outra função e devolve uma nova, geralmente um invólucro que acrescenta comportamento antes ou depois da chamada: registro de logs, medição de tempo, cache, verificação de permissões. A notação @decorator acima de def é apenas um atalho para func = decorator(func); para que o invólucro mantenha o nome e a docstring da função original, usa-se functools.wraps.

Um decorador em Python é uma função que recebe uma função e devolve outra — em geral um invólucro em torno da original. Assim se acrescenta comportamento, como registro de logs, medição de tempo, cache ou novas tentativas, sem alterar o código da função. Os decoradores são escritos com a notação @nome logo acima da declaração da função e aparecem em todo lugar: no Flask e no FastAPI, no pytest, na biblioteca padrão.

O que é um decorador em linguagem simples

Imagine um presente e o papel de embrulho. O presente (a função) continua o mesmo, mas por fora aparece uma camada que age primeiro. O decorador é exatamente esse embrulho: na chamada, primeiro executa o código do invólucro, que chama a função original e pode fazer algo com o resultado.

Isso funciona porque, em Python, funções são objetos comuns. Elas podem ser passadas a outras funções, devolvidas por funções e declaradas dentro de funções. Os decoradores são construídos sobre essas três propriedades.

Como funciona um decorador: o @ é açúcar sintático

Veja um decorador simples, que imprime uma mensagem antes e depois da chamada:

def log_calls(func):
    def wrapper():
        print(f"Chamando {func.__name__}")
        result = func()
        print(f"{func.__name__} concluída")
        return result
    return wrapper

@log_calls
def hello():
    print("Olá!")

hello()
Chamando hello
Olá!
hello concluída

A notação @log_calls acima do def é totalmente equivalente à linha que vem depois da declaração:

def hello():
    print("Olá!")

hello = log_calls(hello)

Agora o nome hello aponta para o wrapper, e a função original vive dentro do closure — o wrapper guarda a variável func da função externa. Um detalhe importante: o decorador é aplicado uma única vez, no momento em que o def é executado, ou seja, na importação do módulo; já o invólucro é executado a cada chamada.

Closures e decoradores

Closure é uma função interna que guarda as variáveis da função externa mesmo depois de ela ter terminado. Sem closures não existiriam decoradores: é justamente assim que o wrapper tem acesso a func. No closure também é possível guardar estado, por exemplo um contador de chamadas:

def count_calls(func):
    calls = 0
    def wrapper(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"{func.__name__}: chamada nº {calls}")
        return func(*args, **kwargs)
    return wrapper

Como escrever um decorador em Python: modelo universal

O primeiro exemplo tem duas falhas: o invólucro não aceita argumentos e «engole» o nome da função original. Um modelo funcional, com o qual vale começar qualquer decorador, fica assim:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # código antes da chamada
        result = func(*args, **kwargs)
        # código depois da chamada
        return result
    return wrapper

Três regras do modelo:

  1. *args, **kwargs — o invólucro aceita quaisquer argumentos e os repassa sem alterações.
  2. return result — não esquecer de devolver o resultado, senão a função decorada sempre devolverá None.
  3. @functools.wraps(func) — transferir os metadados da função original para o invólucro.

Exemplo de decorador: medição do tempo de execução.

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} levou {elapsed:.3f} s")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)   # slow_sum levou 0.142 s

Para que serve functools.wraps

Sem wraps, a função decorada perde o nome, a docstring e outros atributos — de fora se vê o wrapper:

def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def add(a, b):
    """Soma dois números."""
    return a + b

print(add.__name__, add.__doc__)   # wrapper None

Com @functools.wraps(func) o invólucro recebe __name__, __doc__, __module__, __qualname__ e __annotations__ da função original, e no atributo __wrapped__ fica a referência ao original. Isso é importante para depuração e para os tracebacks, para help(), para frameworks que registram funções pelo nome e para inspect.signature, que mostra a assinatura real.

Decorador com parâmetros (com argumentos)

Às vezes o decorador precisa de configurações: @retry(times=3), @app.route("/"). Para isso acrescenta-se mais um nível de aninhamento. A função externa recebe os parâmetros e devolve o decorador de verdade — esse tipo de construção é chamado de fábrica de decoradores.

import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def ping():
    print("ping")

ping()   # ping, ping, ping — três linhas

Ordem das chamadas: repeat(times=3) devolve decorator, que é aplicado a ping e devolve wrapper. Em outras palavras, @repeat(times=3) equivale a ping = repeat(times=3)(ping).

Um exemplo prático — repetir em caso de erro:

import functools
import time

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if attempt == times:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
    ...

Vários decoradores na mesma função

Decoradores podem ser empilhados. Eles são aplicados de baixo para cima: o mais próximo do def é o primeiro.

@timer
@log_calls
def report():
    ...

# o mesmo que:
report = timer(log_calls(report))

Na chamada, a camada externa (timer) é executada primeiro, por isso ela também medirá o tempo de trabalho de log_calls. A ordem importa, por exemplo, quando um decorador verifica permissões de acesso e outro faz cache do resultado.

Decoradores padrão e embutidos do Python

Muitos decoradores úteis já estão na linguagem e na biblioteca padrão:

DecoradorO que faz
@propertytransforma um método em atributo somente de leitura; com @x.setter — em atributo controlado
@staticmethodmétodo de classe sem self, função comum dentro do namespace da classe
@classmethodo método recebe a classe cls em vez da instância — assim se escrevem construtores alternativos
@functools.cache, @functools.lru_cacheguardam em cache os resultados por argumentos
@functools.wrapscopia os metadados da função para o invólucro
@dataclasses.dataclassgera __init__, __repr__ e __eq__ para a classe
@contextlib.contextmanagertransforma um gerador em gerenciador de contexto para o with

Cache: functools.cache e lru_cache

O decorador de cache memoriza o resultado para cada conjunto de argumentos e, numa nova chamada, devolve-o sem recalcular. O exemplo clássico são os números de Fibonacci recursivos, que sem cache demoram um tempo exponencial:

import functools

@functools.cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(100))   # 354224848179261915075

functools.cache (surgiu no Python 3.9) guarda todos os resultados sem limite. Já @functools.lru_cache(maxsize=128) mantém apenas os registros mais recentes e descarta os antigos — é mais seguro para serviços que funcionam por muito tempo. As estatísticas são vistas com fib.cache_info(), e o cache é limpo com fib.cache_clear(). Os argumentos dessas funções precisam ser hasheáveis: uma lista como argumento gera TypeError: unhashable type: 'list'. E cachear vale apenas para funções puras — aquelas que, com os mesmos argumentos, sempre devolvem o mesmo resultado e não têm efeitos colaterais.

O que faz o decorador property

@property permite acessar um método como se fosse atributo, sem parênteses, e, se necessário, controlar a atribuição de valores:

class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("O saldo não pode ser negativo")
        self._balance = value

acc = Account(100)
print(acc.balance)   # 100
acc.balance = -5     # ValueError

staticmethod e classmethod

class Money:
    def __init__(self, cents):
        self.cents = cents

    @classmethod
    def from_reais(cls, reais):
        return cls(round(reais * 100))

    @staticmethod
    def is_valid(value):
        return value >= 0

m = Money.from_reais(12.5)
print(m.cents, Money.is_valid(-1))   # 1250 False

É o @classmethod que permite ao método trabalhar com a própria classe, e não com as instâncias: from_reais cria corretamente também um objeto de uma subclasse.

Decoradores de classes e classes como decoradores

Um decorador pode ser aplicado a uma classe: ele recebe a classe e devolve a classe (a mesma ou uma alterada). É assim que funciona o @dataclass. Um exemplo simples — um registro de classes:

registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class CsvExporter:
    pass

print(registry)   # {'CsvExporter': <class '__main__.CsvExporter'>}

O caso inverso é uma classe usada como decorador. Serve qualquer objeto com o método __call__; o estado é convenientemente guardado nos atributos da instância:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

@CountCalls
def tick():
    pass

tick(); tick()
print(tick.calls)   # 2

Decorador assíncrono

Para funções async def o invólucro também deve ser uma corrotina e aguardar o resultado com await; caso contrário, será devolvido um objeto de corrotina ainda não executado:

import functools

def log_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"Início de {func.__name__}")
        result = await func(*args, **kwargs)
        print(f"Concluído {func.__name__}")
        return result
    return wrapper

Para que servem os decoradores em Python

  • Lógica transversal — logs, métricas e medição de tempo sem copiar código para cada função.
  • Cache — @functools.cache para cálculos caros e puros.
  • Verificações — permissões, validação de argumentos, autorização em handlers web.
  • Registro — rotas no Flask e no FastAPI (@app.get("/")), fixtures do pytest.
  • Confiabilidade — novas tentativas, timeouts, tratamento de erros.

A principal vantagem é a separação de responsabilidades: a função cuida da sua tarefa, e o comportamento auxiliar é acoplado com uma linha e removido com a mesma facilidade.

Erros típicos

  • O invólucro não devolve o resultado — a função depois de decorada retorna None.
  • Sem *args, **kwargs — TypeError ao chamar com argumentos.
  • Esqueceram o wraps — __name__ e docstring errados, confusão nos logs.
  • Parênteses na fábrica — @repeat em vez de @repeat(3): a função cai no parâmetro times.
  • Código pesado no corpo do decorador — ele roda na importação do módulo, e não na chamada.

Resumo

Decoradores em Python são funções de ordem superior: recebem uma função, devolvem um invólucro, e o @ é apenas uma forma curta de atribuição. Comece pelo modelo com *args, **kwargs, return result e functools.wraps, e para configurações acrescente um nível de fábrica. Funções, closures e anotações, sobre os quais se apoiam os decoradores, são praticados no curso de Python.

Perguntas frequentes

Dá para remover um decorador de uma função?

Se o decorador usa functools.wraps, o original fica disponível pelo atributo __wrapped__: func.__wrapped__(...) chama a função sem o invólucro. Caso contrário, resta apenas tirar a linha com @ do código.

Quando o código do decorador é executado?

O corpo do próprio decorador roda uma vez, na declaração da função, geralmente durante a importação do módulo. O código dentro de wrapper roda a cada chamada da função decorada.

Dá para decorar métodos de classe?

Sim, um decorador comum com *args, **kwargs funciona também com métodos: o self simplesmente será o primeiro elemento de args.

Em que um decorador difere da herança?

A herança altera o comportamento de uma classe por meio de uma hierarquia, enquanto o decorador envolve de fora uma função ou classe específica e combina-se facilmente com outros decoradores, sem mudar o código.

Aprenda este tema em vídeo

Este tema é abordado em profundidade, com prática, no curso “Python 3.15: от новичка до профи”.

Ir para o curso