Voltar ao Glossario
Glossario Python

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