Decorator em Python do básico até com parâmetros
1. Visão geral
Decorator é um dos recursos mais “mágicos” do Python, mas também fácil de causar confusão se você apenas copiar-colar sem entender a essência. Este artigo vai desde o conceito de função como first-class citizen, passando por decorator básico, decorator que preserva metadata com functools.wraps, até decorator com parâmetros e decorator escrito em classe. Após ler, você será capaz de escrever seus próprios decorator de logging, timing ou retry para seu projeto sem “perder o nome da função original”.
2. Função como First-Class Citizen
Antes de aprender sobre decorator, você precisa entender um fato importante: em Python, funções também são objetos. Você pode atribuir uma função a uma variável, passar uma função como parâmetro, ou retornar uma função de outra função.
def greet(name: str) -> str:
return f"Hello, {name}!"
say_hello = greet
print(say_hello("Alice"))
Hello, Alice!
say_hello é apenas outro nome apontando para o mesmo objeto de função greet. Graças a essa característica, Python permite escrever funções que aceitam funções como entrada e retornam funções como saída — isso é exatamente a base do decorator.
3. Decorator Básico: Função Envolvendo Função
Um decorator é simplesmente uma função que aceita outra função, retornando uma nova função que “envolve” (wrap) o comportamento da função original.
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
def add(a: int, b: int) -> int:
return a + b
add = log_call(add)
print(add(2, 3))
Calling add with args=(2, 3), kwargs={}
add returned 5
5
log_callrecebefunc(a função originaladd) e definewrapper— uma função interna que chamafuncnovamente mas com log antes/depois.log_call(add)retornawrapper, e atribuímos de volta aadd. A partir daí, chamaradd(...)é na verdade chamarwrapper(...).
3.1. Sintaxe @ — Açúcar Sintático
Escrever func = decorator(func) manualmente é bem verboso. Python fornece a sintaxe @decorator para fazer exatamente isso, de forma mais concisa:
@log_call
def multiply(a: int, b: int) -> int:
return a * b
print(multiply(4, 5))
Calling multiply with args=(4, 5), kwargs={}
multiply returned 20
20
@log_call colocado direto acima de def multiply é equivalente a multiply = log_call(multiply). Isso é apenas açúcar sintático — não há nenhuma magia escondida.
4. Problema de Perder Metadata e functools.wraps
Quando o decorator “substitui” a função original por wrapper, você perde atributos como __name__, __doc__ da função original.
@log_call
def subtract(a: int, b: int) -> int:
"""Subtrai dois números."""
return a - b
print(subtract.__name__)
print(subtract.__doc__)
wrapper
None
Esse é um erro comum ao debugar ou usar ferramentas que geram documentação — você acha que está chamando subtract mas Python relata que o nome é wrapper. Corrija isso com functools.wraps:
import functools
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def subtract(a: int, b: int) -> int:
"""Subtrai dois números."""
return a - b
print(subtract.__name__)
print(subtract.__doc__)
subtract
Subtrai dois números.
functools.wraps(func) é um decorator de decorator: copia __name__, __doc__, __module__… da função func para wrapper.
Nota: Sempre use
functools.wrapsem decorator reais. Omiti-lo causará dificuldade em debug e quebrará introspection (por exemplo,help(), frameworks que dependem do nome da função).
5. Decorator com Parâmetros
Muitas vezes você quer que seu decorator seja customizável, por exemplo @retry(times=3). Nesse caso você precisa de uma camada extra de função: uma função externa que aceita parâmetros e retorna o decorator real, que então aceita func.
import functools
import time
def retry(times: int = 3, delay: float = 1.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as exc:
last_exception = exc
print(f"Attempt {attempt} failed: {exc}")
time.sleep(delay)
raise last_exception
return wrapper
return decorator
@retry(times=2, delay=0)
def call_payment_api(order_id: int) -> str:
raise ConnectionError(f"Timeout for order {order_id}")
try:
call_payment_api(101)
except ConnectionError as exc:
print(f"Giving up: {exc}")
Attempt 1 failed: Timeout for order 101
Attempt 2 failed: Timeout for order 101
Giving up: Timeout for order 101
A estrutura de três camadas a lembrar:
retry(times, delay)— função factory, aceita parâmetros de configuração, retornadecorator.decorator(func)— a função decorator real, aceita a função original.wrapper(*args, **kwargs)— a função executada quando chamamos a função decorada.
@retry(times=2, delay=0) na verdade chama primeiro retry(times=2, delay=0) (retorna decorator), depois aplica decorator a call_payment_api.
5.1. Decorator que Funciona Com e Sem Parâmetros
Às vezes você quer que @retry funcione tanto sem parâmetros (@retry em vez de @retry()) quanto com. Uma forma limpa é usar functools.partial combinado com verificação de callable:
import functools
def retry(func=None, *, times: int = 3):
if func is None:
return functools.partial(retry, times=times)
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as exc:
print(f"Attempt {attempt} failed: {exc}")
raise RuntimeError("All attempts failed")
return wrapper
@retry
def task_a():
return "ok"
@retry(times=5)
def task_b():
return "ok"
Quando você usa @retry (sem chamar a função), func recebe diretamente a função task_a, a condição if func is None não executa. Quando você usa @retry(times=5), func é None inicialmente, a função retorna functools.partial(retry, times=times) — uma função esperando receber func no próximo passo, que é task_b.
6. Decorator Escrito em Classe
Em vez de usar funções aninhadas, você pode escrever um decorator como uma classe, implementando __call__. Essa abordagem é apropriada quando o decorator precisa manter estado complexo.
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"{self.func.__name__} called {self.call_count} time(s)")
return self.func(*args, **kwargs)
@CountCalls
def process_order(order_id: int) -> None:
print(f"Processing order {order_id}")
process_order(1)
process_order(2)
print(process_order.call_count)
process_order called 1 time(s)
Processing order 1
process_order called 2 time(s)
Processing order 2
2
__init__armazena a função original e inicializacall_count = 0.functools.update_wrapper(self, func)— equivalente afunctools.wrapsmas para class instance, copia metadata para o objetoself.__call__faz a instânciaprocess_order(que agora é um objetoCountCalls, não mais uma função) ser chamável como uma função comum.
Nota: Decorator baseado em classe não funciona bem quando decora métodos em outras classes (pois
selfda classe original e do decorator podem entrar em conflito). Com métodos, prefira funções aninhadas oufunctools.singledispatchmethodse precisar de dispatch.
7. Empilhando Múltiplos Decorator ao Mesmo Tempo (Stacking)
Você pode empilhar múltiplos decorator em uma função. A ordem de aplicação é de baixo para cima (o decorator mais próximo da função executa primeiro).
@log_call
@retry(times=2, delay=0)
def charge_card(amount: float) -> str:
if amount <= 0:
raise ValueError("Amount must be positive")
return f"Charged {amount}"
print(charge_card(10))
Calling wrapper
Charged 10
Aqui retry(times=2, delay=0) é aplicado primeiro, envolvendo charge_card em uma função com capacidade de retry; depois log_call envolve novamente por fora. Se você não usar functools.wraps corretamente em retry, verá o log imprimindo wrapper em vez de charge_card — mais uma prova da importância da seção 4.
8. Testando Decorator
Decorator ainda é uma função Python normal, então você pode testá-lo com pytest como qualquer outra lógica. Instale a biblioteca se ainda não tiver:
pip install pytest
Testando o decorator retry:
import pytest
def test_retry_raises_after_exhausting_attempts():
call_log = []
@retry(times=3, delay=0)
def always_fail():
call_log.append("called")
raise ValueError("boom")
with pytest.raises(ValueError):
always_fail()
assert len(call_log) == 3
def test_retry_succeeds_on_second_attempt():
attempts = {"count": 0}
@retry(times=3, delay=0)
def fail_once_then_succeed():
attempts["count"] += 1
if attempts["count"] < 2:
raise ValueError("boom")
return "ok"
result = fail_once_then_succeed()
assert result == "ok"
assert attempts["count"] == 2
Execute os testes:
pytest test_retry.py -v
test_retry.py::test_retry_raises_after_exhausting_attempts PASSED
test_retry.py::test_retry_succeeds_on_second_attempt PASSED
call_log e attempts fazem o papel de spy para verificar quantas vezes a função original foi chamada — exatamente como esperado para o comportamento retry, sem precisar mockar bibliotecas externas.
9. Erros Comuns
- Esquecer
functools.wraps: a função perde nome/docstring, causando dificuldade em debug e erros ao usar com framework que dependa de__name__(por exemplo, roteamento em Flask). - Usar argumento padrão mutável em wrapper: similar ao erro comum em Python, evite ter
args=[]como padrão. - Decorator com side-effect pesado no momento de import: se o decorator realizar I/O (chamar API, ler arquivo) direto em
decorator(func)em vez de dentro dewrapper, executará toda vez que o módulo for importado, não cada vez que a função for chamada. - Abuso de decorator para lógica complexa: se o decorator contiver muitas ramificações, o código fica mais difícil de ler do que uma função que chama diretamente. Quando a lógica é complexa, considere context manager ou separar em uma classe específica em vez de colocar tudo dentro do decorator.
Quando NÃO usar decorator: se o comportamento de “envolvimento” é usado apenas uma vez, não é reutilizado, escrever um decorator só torna o código mais difícil de rastrear do que chamar a função diretamente. Decorator brilha quando aplicam lógica transversal (cross-cutting concern) como logging, caching, retry, autenticação a múltiplas funções diferentes.
10. Conclusão
Decorator em Python não tem nada de misterioso — é apenas uma função que recebe uma função, retorna uma função, baseado no fato de funções serem first-class citizen. De um decorator básico, você adiciona functools.wraps para preservar metadata, adiciona uma camada extra de função para aceitar parâmetros de configuração, ou muda para baseado em classe quando precisa manter estado complexo. Dominando esses padrões, você será capaz de escrever seus próprios decorator de logging, caching, retry ou validação, limpo e fácil de testar para seus projetos reais.