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.
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.
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çucaradoAlgumas 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.
@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
wrapperem 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_cachee ferramentas similares usam a identidade da função para cacheamento — semwraps, você pode ter colisões de cache surpreendentes.
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.' ← preservadofunctools.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:
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.
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.
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:
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:
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 ValueErrorUm 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:
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.
@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# 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.@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.