Decorators em Python: O que É e Como Funciona | Python Brasil
Entenda decorators em Python: functools.wraps, decorators de classe, stacking, lru_cache, retry, rate limiting e padrões reais. Guia completo com exemplos.
O que são Decorators?
Decorators são uma forma elegante de modificar ou estender o comportamento de funções e classes sem alterar seu código-fonte. Eles usam o símbolo @ e são um dos recursos mais poderosos e utilizados do Python.
Se você já usou Flask ou Django, com certeza já viu decorators como @app.route() ou @login_required. Em código de produção eles aparecem em logging, autenticação, cache, retry, validação e muito mais.
Como funcionam internamente
Um decorator é basicamente uma função que recebe outra função como argumento e retorna uma nova função. A sintaxe @decorator é apenas açúcar sintático para func = decorator(func):
import time
import functools
def medir_tempo(func):
@functools.wraps(func) # Preserva metadados da função original
def wrapper(*args, **kwargs):
inicio = time.perf_counter()
resultado = func(*args, **kwargs)
fim = time.perf_counter()
print(f"{func.__name__} levou {fim - inicio:.4f} segundos")
return resultado
return wrapper
@medir_tempo
def processar_dados(n):
"""Processa n itens."""
return sum(range(n))
resultado = processar_dados(1_000_000)
# processar_dados levou 0.0231 segundos
# Sem @medir_tempo:
# processar_dados = medir_tempo(processar_dados)
A importância de functools.wraps
Sem @functools.wraps, o decorator “mascara” a identidade da função original, quebrando documentação, debugging e inspeção:
def decorator_ruim(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def decorator_correto(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator_ruim
def minha_funcao():
"""Documentação importante."""
pass
@decorator_correto
def outra_funcao():
"""Documentação importante."""
pass
print(minha_funcao.__name__) # wrapper — ERRADO!
print(minha_funcao.__doc__) # None — documentação perdida!
print(outra_funcao.__name__) # outra_funcao — correto
print(outra_funcao.__doc__) # Documentação importante. — correto
functools.wraps copia __name__, __doc__, __annotations__, __qualname__ e __dict__ da função original para o wrapper. Use sempre ao criar decorators.
Decorators com parâmetros
Para decorators que aceitam argumentos, adicione uma camada extra de função:
import functools
def repetir(vezes=1, delay=0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
resultado = None
for i in range(vezes):
if delay > 0 and i > 0:
time.sleep(delay)
resultado = func(*args, **kwargs)
return resultado
return wrapper
return decorator
@repetir(vezes=3, delay=0.1)
def saudar(nome):
print(f"Olá, {nome}!")
saudar("Maria")
# Olá, Maria! (impresso 3 vezes, com 0.1s entre cada)
Decorators baseados em classe
Classes com o método __call__ podem ser usadas como decorators, o que facilita o armazenamento de estado:
import functools
class Retry:
"""Decorator de classe que implementa lógica de retry com estado."""
def __init__(self, max_tentativas=3, excecoes=(Exception,), delay=1.0):
self.max_tentativas = max_tentativas
self.excecoes = excecoes
self.delay = delay
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
ultima_excecao = None
for tentativa in range(1, self.max_tentativas + 1):
try:
return func(*args, **kwargs)
except self.excecoes as e:
ultima_excecao = e
print(
f"[Retry] Tentativa {tentativa}/{self.max_tentativas} "
f"falhou: {e}"
)
if tentativa < self.max_tentativas:
time.sleep(self.delay)
raise ultima_excecao
return wrapper
@Retry(max_tentativas=3, excecoes=(ConnectionError, TimeoutError), delay=2.0)
def chamar_api(url):
import random
if random.random() < 0.7:
raise ConnectionError("Falha na conexão")
return f"Dados de {url}"
Decorando classes inteiras
Decorators também podem ser aplicados a classes, não apenas funções:
def singleton(cls):
"""Garante que apenas uma instância da classe exista."""
instancias = {}
@functools.wraps(cls)
def get_instancia(*args, **kwargs):
if cls not in instancias:
instancias[cls] = cls(*args, **kwargs)
return instancias[cls]
return get_instancia
@singleton
class Configuracao:
def __init__(self):
self.debug = False
self.db_url = "postgresql://localhost/mydb"
cfg1 = Configuracao()
cfg2 = Configuracao()
print(cfg1 is cfg2) # True — mesma instância
def adicionar_repr(cls):
"""Adiciona __repr__ automático baseado em __dict__."""
def __repr__(self):
attrs = ', '.join(f'{k}={v!r}' for k, v in self.__dict__.items())
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
@adicionar_repr
class Ponto:
def __init__(self, x, y):
self.x = x
self.y = y
print(Ponto(1, 2)) # Ponto(x=1, y=2)
Empilhando decorators
Múltiplos decorators são aplicados de baixo para cima:
def negrito(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italico(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
def maiusculo(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@negrito
@italico
@maiusculo
def saudacao(nome):
return f"olá, {nome}"
# Equivale a: negrito(italico(maiusculo(saudacao)))
print(saudacao("mundo")) # <b><i>OLÁ, MUNDO</i></b>
Decorators nativos do Python
O Python inclui vários decorators poderosos na biblioteca padrão:
import functools
# @property — transforma método em atributo
class Circulo:
def __init__(self, raio):
self.raio = raio
@property
def area(self):
return 3.14159 * self.raio ** 2
# @functools.lru_cache — memoização automática
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # Instantâneo graças ao cache
print(fibonacci.cache_info()) # hits=48, misses=51, maxsize=128, currsize=51
# @functools.cached_property — property calculada uma vez e cacheada
class Dataset:
def __init__(self, dados):
self.dados = dados
@functools.cached_property
def estatisticas(self):
print("Calculando estatísticas...") # Executado apenas uma vez
return {
'media': sum(self.dados) / len(self.dados),
'minimo': min(self.dados),
'maximo': max(self.dados),
}
d = Dataset(list(range(1000)))
print(d.estatisticas) # Calculando...
print(d.estatisticas) # Sem recalcular!
Padrões reais: rate limiting e memoização
import time
import functools
from collections import deque
def rate_limit(chamadas_por_segundo):
"""Limita a taxa de execução de uma função."""
intervalo = 1.0 / chamadas_por_segundo
historico = deque()
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
agora = time.monotonic()
# Remove registros antigos
while historico and agora - historico[0] > 1.0:
historico.popleft()
if len(historico) >= chamadas_por_segundo:
tempo_espera = 1.0 - (agora - historico[0])
if tempo_espera > 0:
time.sleep(tempo_espera)
historico.append(time.monotonic())
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(chamadas_por_segundo=2)
def chamar_api_externa(endpoint):
print(f"Chamando {endpoint} em {time.monotonic():.2f}")
return f"Resposta de {endpoint}"
def memoize(func):
"""Memoização personalizada com suporte a argumentos não-hashable."""
cache = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Cria chave a partir dos argumentos
try:
chave = (args, tuple(sorted(kwargs.items())))
except TypeError:
return func(*args, **kwargs) # Argumento não-hashable
if chave not in cache:
cache[chave] = func(*args, **kwargs)
return cache[chave]
wrapper.cache_clear = lambda: cache.clear()
wrapper.cache_size = lambda: len(cache)
return wrapper
Decorator thread-safe
Em ambientes multi-thread, use locks para proteger estado compartilhado dentro do decorator:
import threading
import functools
def sincronizado(func):
"""Garante execução thread-safe da função."""
lock = threading.Lock()
@functools.wraps(func)
def wrapper(*args, **kwargs):
with lock:
return func(*args, **kwargs)
return wrapper
class Contador:
def __init__(self):
self._valor = 0
@sincronizado
def incrementar(self):
self._valor += 1
return self._valor
Erros comuns
O erro mais frequente é esquecer de usar @functools.wraps, perdendo metadados. Outro problema comum é criar decorators com estado mutável compartilhado entre chamadas sem sincronização. Também tome cuidado com decorators que capturam variáveis de loop — use argumentos padrão ou closures corretas.
# ERRADO — todas as funções capturam o mesmo 'i'
funcoes = []
for i in range(3):
def f():
return i
funcoes.append(f)
print([f() for f in funcoes]) # [2, 2, 2] — bug!
# CORRETO — captura o valor atual via argumento padrão
funcoes = []
for i in range(3):
def f(x=i):
return x
funcoes.append(f)
print([f() for f in funcoes]) # [0, 1, 2] — correto
Quando usar decorators?
Use decorators quando você precisa de comportamento transversal (cross-cutting concerns) que se aplica a múltiplas funções: logging, autenticação, cache, validação, retry, rate limiting, timing. Evite decorators quando a lógica é específica de uma única função — nesse caso, o código inline é mais claro.
Termos Relacionados
- Classe - Decorators são muito usados com classes
- Flask - Framework que usa decorators para rotas
- Context Manager - Outro padrão de design Python
- Lambda - Funções anônimas em Python