Se você usou Flask ou FastAPI por mais de uma semana, já usou decoradores — @app.route, @login_required, @pytest.mark.parametrize. Eles parecem mágica na primeira vez, e então alguém explica o que realmente está acontecendo e clica imediatamente. Um decorador é apenas uma função que envolve outra função. A sintaxe @ é açúcar sintático puro — @my_decorator acima de uma definição de função é exatamente equivalente a escrever func = my_decorator(func) depois dela. Esse é o segredo completo. O resto são apenas padrões construídos sobre essa única ideia. Este artigo constrói o modelo mental do zero, depois percorre os padrões que você usará de verdade: @functools.wraps, decoradores com argumentos, decoradores baseados em classe, e um punhado de exemplos do mundo real incluindo @lru_cache, @dataclass, e um @retry adequado com backoff exponencial.

Funções São Objetos de Primeira Classe

Antes que decoradores façam sentido, você precisa ter solidez em um fato do Python: funções são objetos. Você pode atribuí-las a variáveis, passá-las como argumentos para outras funções, retorná-las de funções, e armazená-las em listas ou dicts. Nada especial acontece só porque algo é uma função.

python
def greet(name: str) -> str:
    return f"Hello, {name}"

# Atribuir a uma variável — sem () significa que não estamos chamando, apenas referenciando
say_hello = greet
print(say_hello("Alice"))   # 'Hello, Alice'

# Passar uma função como argumento
def run_twice(fn, value):
    return fn(value), fn(value)

run_twice(greet, "Bob")     # ('Hello, Bob', 'Hello, Bob')

# Retornar uma função de outra função — isso é uma "fábrica"
def make_prefixer(prefix: str):
    def prefixed_greet(name: str) -> str:
        return f"{prefix}, {name}"
    return prefixed_greet

morning_hello = make_prefixer("Good morning")
morning_hello("Carol")      # 'Good morning, Carol'

A função interna prefixed_greet "fecha sobre" a variável prefix do escopo delimitador — mesmo depois que make_prefixer retornou, a função interna ainda tem acesso a prefix. Isso é um closure, e é o mecanismo que faz decoradores funcionarem. A documentação do Python sobre regras de escopo explica isso em detalhe se você quiser o quadro completo.

Construindo um Decorador do Zero

Um decorador é uma função que recebe uma função e retorna uma função (geralmente modificada). O exemplo clássico primeiro é um decorador de temporização — ele envolve qualquer função e registra quanto tempo levou para executar.

python
import time
import functools

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

@timer
def fetch_user_records(db, user_id: int):
    """Fetch all records for a given user from the database."""
    return db.query("SELECT * FROM records WHERE user_id = ?", user_id)

# Chamar fetch_user_records é agora: timer(fetch_user_records)(db, 42)
# Que é exatamente para o que @timer é desaçucarado

Algumas coisas para notar: o wrapper usa *args, **kwargs para poder encaminhar qualquer combinação de argumentos para a função original sem conhecer sua assinatura. Ele captura o valor de retorno em result e o retorna, então a função envolvida ainda se comporta de forma idêntica do ponto de vista do chamador — ela só tem um efeito colateral extra de print. Remova a temporização e você tem o esqueleto de quase todo decorador que você vai escrever.

A regra de desaçucaramento: Sempre que você vê @some_decorator acima de uma definição de função, substitua mentalmente por fn = some_decorator(fn) escrito imediatamente após o bloco def. Os dois são exatamente equivalentes. Não há mágica — é uma chamada de função.

Por Que Você Deve Usar @functools.wraps

No exemplo acima, há uma linha @functools.wraps(fn) no wrapper. Isso não é opcional. Sem ele, sua função decorada perde sua identidade — seus atributos __name__, __doc__ e __qualname__ são todos substituídos pelos da função wrapper interna. Isso causa quebras sutis em algumas situações reais:

  • Docstrings desaparecem. help(fetch_user_records) mostra a docstring vazia do wrapper em vez de "Fetch all records for a given user...".
  • Stack traces mentem. Quando uma exceção é lançada dentro da função envolvida, o traceback mostra wrapper em vez do nome real da função — difícil de depurar.
  • Introspecção quebra. Ferramentas como pytest, o sistema de roteamento do Flask e inspect.signature() dependem de __name__ e __wrapped__. O roteador do Flask lançará erro se duas rotas compartilharem o mesmo nome (wrapper).
  • functools.lru_cache e ferramentas similares usam a identidade da função para cacheamento — sem wraps, você pode ter colisões de cache surpreendentes.
python
import functools

# Sem @functools.wraps — quebrado
def bad_timer(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@bad_timer
def process_batch(batch_id: int):
    """Process a single batch job."""
    pass

print(process_batch.__name__)   # 'wrapper'   ← errado
print(process_batch.__doc__)    # None         ← docstring perdida

# Com @functools.wraps — correto
def good_timer(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@good_timer
def process_batch(batch_id: int):
    """Process a single batch job."""
    pass

print(process_batch.__name__)   # 'process_batch'  ← correto
print(process_batch.__doc__)    # 'Process a single batch job.'  ← preservado

functools.wraps é ele mesmo um decorador — ele copia __module__, __name__, __qualname__, __annotations__, __doc__, e define __wrapped__ para a função original. Use-o em cada função wrapper, ponto final. Não há razão para não fazê-lo.

Decoradores com Argumentos

Um decorador simples recebe uma função e retorna uma função. Um decorador com argumentos precisa de mais um nível: uma função que recebe os argumentos e retorna um decorador. São três níveis de aninhamento, e confunde quase todos na primeira vez que veem. Aqui está um decorador @retry que repete uma função até max_attempts vezes em exceção:

python
import functools
import time

def retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0):
    """
    Retry a function on exception with exponential backoff.

    Usage:
        @retry(max_attempts=5, delay=0.5, backoff=2.0)
        def call_external_api(endpoint: str) -> dict:
            ...
    """
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            current_delay = delay
            last_exception = None

            for attempt in range(1, max_attempts + 1):
                try:
                    return fn(*args, **kwargs)
                except Exception as exc:
                    last_exception = exc
                    if attempt < max_attempts:
                        print(
                            f"{fn.__name__}: attempt {attempt}/{max_attempts} failed "
                            f"({exc!r}), retrying in {current_delay:.1f}s..."
                        )
                        time.sleep(current_delay)
                        current_delay *= backoff
                    else:
                        print(f"{fn.__name__}: all {max_attempts} attempts failed.")

            raise last_exception

        return wrapper
    return decorator

@retry(max_attempts=4, delay=0.5, backoff=2.0)
def fetch_price_data(ticker: str) -> dict:
    """Fetch stock price data from external API."""
    response = requests.get(f"https://api.example.com/prices/{ticker}", timeout=5)
    response.raise_for_status()
    return response.json()

A cadeia de chamadas quando Python processa @retry(max_attempts=4, delay=0.5, backoff=2.0): primeiro retry(max_attempts=4, delay=0.5, backoff=2.0) é chamado e retorna decorator. Então decorator(fetch_price_data) é chamado e retorna wrapper. Finalmente fetch_price_data é religado a wrapper. Então @retry(...) é fetch_price_data = retry(...)(fetch_price_data) — três chamadas, dois níveis de envolvimento. Uma vez que você vê esse padrão, fábricas de decoradores param de ser confusas.

Decoradores Baseados em Classe

Você também pode implementar um decorador como uma classe definindo __call__. Isso é útil quando o decorador precisa manter estado entre chamadas — um contador de chamadas, um cache, pools de conexão — porque variáveis de instância são um lar mais natural para esse estado do que variáveis de closure.

python
import functools
import time

class RateLimiter:
    """
    Decorator that limits how often a function can be called.
    Raises RuntimeError if the function is called within `min_interval` seconds
    of the previous call.
    """

    def __init__(self, min_interval: float):
        self.min_interval = min_interval
        self._last_called: float = 0.0

    def __call__(self, fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            now = time.monotonic()
            since_last = now - self._last_called
            if since_last < self.min_interval:
                wait = self.min_interval - since_last
                raise RuntimeError(
                    f"{fn.__name__} called too soon — "
                    f"wait {wait:.2f}s before calling again."
                )
            self._last_called = now
            return fn(*args, **kwargs)
        return wrapper

# Uso — uma chamada a cada 2 segundos
@RateLimiter(min_interval=2.0)
def send_sms_alert(phone: str, message: str) -> None:
    """Send an SMS alert via the gateway API."""
    sms_gateway.send(phone, message)

@RateLimiter(min_interval=2.0) funciona exatamente como uma fábrica de decoradores: RateLimiter(2.0) constrói uma instância, então essa instância é chamada com send_sms_alert porque tem __call__. A instância armazena _last_called como um atributo — sem necessidade de malabarismo com variáveis de closure. Veja PEP 318 (a proposta original de decorador) para a justificativa de design por trás da sintaxe @, e PEP 614 para a gramática de decorador relaxada que chegou no Python 3.9.

Decoradores do Mundo Real da Biblioteca Padrão

Antes de escrever o seu próprio, verifique se a stdlib já tem o que você precisa. Três decoradores em functools aparecem constantemente em código Python de produção.

python
from functools import lru_cache, cache
import requests

# @cache — memoização ilimitada (Python 3.9+)
# Cacheia cada conjunto único de argumentos para sempre.
# Bom para funções puras com um domínio pequeno de entradas.
@cache
def get_country_name(country_code: str) -> str:
    """Look up a country name from ISO 3166 code. Cached after first call."""
    response = requests.get(f"https://restcountries.com/v3.1/alpha/{country_code}")
    return response.json()[0]["name"]["common"]

get_country_name("DE")   # atinge a API
get_country_name("DE")   # servido do cache, sem chamada de rede

# @lru_cache(maxsize=N) — cache LRU limitado
# Despeja entradas menos recentemente usadas quando o cache atinge `maxsize`.
# Melhor quando o domínio de entrada é grande e a memória é uma preocupação.
@lru_cache(maxsize=256)
def compute_discount(base_price: float, tier: str) -> float:
    """Computação pesada — preço varia por tier. Cacheia as 256 combinações principais."""
    discount_table = load_discount_table()          # chamada de DB cara
    rate = discount_table.get(tier, 0.0)
    return round(base_price * (1 - rate), 2)

# Inspecionar desempenho do cache
print(compute_discount.cache_info())
# CacheInfo(hits=142, misses=14, maxsize=256, currsize=14)

@dataclass está em uma categoria diferente — é um decorador de classe que auto-gera __init__, __repr__ e __eq__ a partir das suas anotações de campo. Ele elimina uma quantidade significativa de boilerplate para classes que armazenam dados:

python
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime

@dataclass
class WebhookEvent:
    event_type: str
    source_id: int
    payload: dict
    received_at: datetime = field(default_factory=datetime.utcnow)
    retry_count: int = 0
    error_message: Optional[str] = None

# @dataclass gera tudo isso de graça:
# - __init__(self, event_type, source_id, payload, received_at=..., retry_count=0, error_message=None)
# - __repr__ que mostra todos os campos
# - __eq__ que compara campo a campo

event = WebhookEvent(event_type="order.created", source_id=9912, payload={"order_id": 44501})
print(event)
# WebhookEvent(event_type='order.created', source_id=9912, payload={...}, retry_count=0, ...)

O decorador @property transforma um método em um acessor de estilo atributo — os chamadores leem user.display_name em vez de user.get_display_name(). Combine com @property.setter para validar na escrita:

python
class UserProfile:
    def __init__(self, first_name: str, last_name: str, email: str):
        self._first_name = first_name
        self._last_name = last_name
        self._email = email.strip().lower()

    @property
    def display_name(self) -> str:
        return f"{self._first_name} {self._last_name}"

    @property
    def email(self) -> str:
        return self._email

    @email.setter
    def email(self, value: str) -> None:
        if "@" not in value:
            raise ValueError(f"Invalid email address: {value!r}")
        self._email = value.strip().lower()

profile = UserProfile("Alice", "Smith", "  [email protected]  ")
print(profile.display_name)   # 'Alice Smith'
print(profile.email)          # '[email protected]'

profile.email = "[email protected]"    # setter executa validação
profile.email = "not-an-email"           # lança ValueError

Um Decorador @require_auth para Handlers de Rota

Verificações de autenticação em frameworks web são um caso de uso clássico de decorador. Em vez de duplicar a verificação "o usuário está logado?" no topo de cada handler de rota, você a escreve uma vez como decorador e aplica onde necessário. Aqui está o padrão, escrito para funcionar com Flask mas transferível para qualquer framework:

python
import functools
from flask import request, jsonify, g

def require_auth(fn):
    """
    Decorator that validates a Bearer token before the route handler runs.
    Sets g.current_user on success; returns 401 JSON on failure.
    """
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        auth_header = request.headers.get("Authorization", "")
        if not auth_header.startswith("Bearer "):
            return jsonify({"error": "Missing or malformed Authorization header"}), 401

        token = auth_header[len("Bearer "):]
        user = verify_token(token)          # sua lógica de validação de token
        if user is None:
            return jsonify({"error": "Invalid or expired token"}), 401

        g.current_user = user               # disponível para o handler de rota
        return fn(*args, **kwargs)
    return wrapper

# Uso — a verificação de auth roda antes do corpo do handler
@app.route("/api/v1/reports/<int:report_id>")
@require_auth
def get_report(report_id: int):
    report = Report.query.get_or_404(report_id)
    if report.owner_id != g.current_user.id:
        return jsonify({"error": "Forbidden"}), 403
    return jsonify(report.to_dict())

Note a ordem: @app.route vai primeiro (mais externo), @require_auth vai segundo. Isso importa — veja a próxima seção. O padrão se estende naturalmente: você poderia adicionar um decorador fábrica @require_role("admin") que verifica g.current_user.role depois que @require_auth já verificou que o usuário existe.

Empilhando Decoradores — A Ordem Importa

Quando você empilha múltiplos decoradores, eles se aplicam de baixo para cima (o decorador mais próximo da função se aplica primeiro), mas eles executam de cima para baixo quando a função é chamada. Isso pega as pessoas de surpresa.

python
@decorator_a
@decorator_b
@decorator_c
def my_function():
    pass

# Isso é exatamente equivalente a:
my_function = decorator_a(decorator_b(decorator_c(my_function)))

# Quando my_function() é chamada:
# 1. wrapper do decorator_a roda primeiro (mais externo)
# 2. wrapper do decorator_b roda segundo
# 3. wrapper do decorator_c roda terceiro (mais interno, mais próximo da função real)
# 4. O corpo real de my_function roda
# 5. Desenrolando: decorator_c → decorator_b → decorator_a
python
# Exemplo prático: a ordem importa para @timer e @retry
# Se timer é mais externo, ele mede o tempo total incluindo períodos de espera de retry.
# Se retry é mais externo, ele mede apenas a chamada final bem-sucedida.

@timer          # mede: tempo total em todas as tentativas de retry + sleep
@retry(max_attempts=3, delay=1.0)
def sync_with_partner_api(partner_id: int) -> dict:
    ...

# vs.

@retry(max_attempts=3, delay=1.0)  # mede: apenas a chamada final bem-sucedida
@timer
def sync_with_partner_api(partner_id: int) -> dict:
    ...

# Geralmente você quer @timer mais externo — ele diz o custo real de wall-clock da operação.
# Pense no que "chamar esta função" significa para o chamador, então envolva nessa ordem.
Dica de stack trace: Quando uma função decorada lança uma exceção, o traceback mostrará cada wrapper na pilha de chamadas. Se você usou @functools.wraps corretamente, os nomes refletirão as funções originais. Se você vir um mar de frames wrapper, alguém esqueceu @functools.wraps. O primer de decoradores do Real Python tem um bom tutorial sobre como depurar decoradores empilhados.

Conclusão

O modelo mental a carregar adiante: @decorator é fn = decorator(fn). Todo o resto — decoradores com argumentos, decoradores baseados em classe, decoradores empilhados — é uma variação em essa única substituição. Use @functools.wraps em todo wrapper interno, sempre encaminhe *args, **kwargs para a função envolvida, e retorne seu resultado. Decoradores com argumentos precisam de três níveis de aninhamento: a função de argumento, o decorador e o wrapper. Decoradores baseados em classe são a escolha certa quando seu decorador precisa manter estado entre chamadas.

Para leitura adicional: a entrada do glossário Python sobre decoradores é breve mas precisa. A proposta original de decorador, PEP 318, vale a leitura para contexto sobre por que a sintaxe @ foi escolhida em vez de alternativas. Se você está usando decoradores em uma codebase que também faz muito processamento de dados — lendo arquivos, transformando registros — os padrões aqui se pareiam naturalmente com o que é coberto em Python File Handling. E se você está usando decoradores para transformar coleções ou construir estruturas de lookup, Python List Comprehensions cobre o lado de transformação de dados desse quadro. Se suas funções decoradas retornam saída JSON e você quer inspecioná-la rapidamente, o JSON Formatter neste site é bastante útil.