Decorators
Nesta aula, exploramos decorators em Python: o que são, como usar a sintaxe @, como criar decorators com argumentos e casos de uso práticos. A aula inclui exemplos detalhados, exercícios com respostas e referências para aprofundamento.
Decorators são um dos recursos mais poderosos e elegantes do Python. Eles permitem modificar ou estender o comportamento de funções ou métodos sem alterar seu código-fonte. Nesta aula, vamos entender o que são decorators, como usar a sintaxe com @, como criar decorators que aceitam argumentos e explorar casos de uso comuns no dia a dia do desenvolvimento.
Um decorator é essencialmente uma função que recebe outra função como argumento e retorna uma nova função que geralmente adiciona alguma funcionalidade. É uma aplicação do padrão de projeto Decorator, mas de forma nativa na linguagem.
O que são
Em Python, funções são objetos de primeira classe: podem ser passadas como argumentos, retornadas de outras funções e atribuídas a variáveis. Um decorator tira proveito disso. Formalmente, um decorator é uma função que recebe uma função e retorna uma função (geralmente substituindo a original por uma versão modificada).
Por exemplo, suponha que queremos medir o tempo de execução de uma função. Podemos criar um decorator que envolve a função original com código de temporização:
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} levou {end - start:.4f} segundos")
return result
return wrapper
def minha_funcao():
time.sleep(1)
return "Feito!"
minha_funcao = timer(minha_funcao)
print(minha_funcao())Aqui, timer é um decorator. A linha minha_funcao = timer(minha_funcao) substitui a função original pela função wrapper que adiciona a temporização. A partir de então, chamar minha_funcao() executa a versão decorada.
Sintaxe @
Python oferece um açúcar sintático para aplicar decorators: o símbolo @. Em vez de escrever minha_funcao = decorator(minha_funcao), basta colocar @decorator acima da definição da função. O código acima pode ser reescrito como:
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} levou {end - start:.4f} segundos")
return result
return wrapper
@timer
def minha_funcao():
time.sleep(1)
return "Feito!"
print(minha_funcao())O uso de @timer é equivalente a minha_funcao = timer(minha_funcao). A sintaxe @ torna o código mais limpo e legível, especialmente quando múltiplos decorators são aplicados.
É importante notar que decorators podem ser empilhados. Por exemplo:
@decorator1
@decorator2
def funcao():
passÉ equivalente a funcao = decorator1(decorator2(funcao)).
Decorators com argumentos
Às vezes, queremos que o próprio decorator aceite argumentos para configurar seu comportamento. Por exemplo, um decorator que repete a execução de uma função um número de vezes. Para isso, precisamos de uma função que retorne um decorator. Ou seja, criamos uma função que recebe os argumentos e retorna o decorator propriamente dito.
Exemplo: um decorator repeat que repete a função n vezes:
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def diga_oi():
print("Oi!")
diga_oi()Aqui, repeat(3) retorna o decorator decorator, que então é aplicado a diga_oi. A sintaxe @repeat(3) é equivalente a diga_oi = repeat(3)(diga_oi).
Outra abordagem comum é usar functools.wraps para preservar metadados da função original (como __name__, __doc__). Sempre que criar um decorator, é boa prática decorar o wrapper com @functools.wraps(func).
import functools
def meu_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# código antes
result = func(*args, **kwargs)
# código depois
return result
return wrapperCasos de uso
Decorators são amplamente utilizados em Python para diversos fins. Alguns casos comuns:
- Logging: Registrar chamadas de função, argumentos e resultados.
- Temporização: Medir tempo de execução (como vimos).
- Cache: Armazenar resultados de funções caras (ex.:
@functools.lru_cache). - Controle de acesso: Verificar permissões antes de executar uma função.
- Validação de argumentos: Garantir que os argumentos atendam a certas condições.
- Retry: Tentar executar uma função novamente em caso de falha.
- Registro de métricas: Coletar dados de uso para monitoramento.
Exemplo de decorator para logging:
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Chamando {func.__name__} com args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} retornou {result}")
return result
return wrapper
@log
def soma(a, b):
return a + b
soma(2, 3)Saída:
Chamando soma com args=(2, 3) kwargs={}
soma retornou 5Outro exemplo: decorator para controle de acesso simulado:
import functools
def requer_autenticacao(func):
@functools.wraps(func)
def wrapper(usuario, *args, **kwargs):
if not usuario.get("autenticado"):
raise PermissionError("Usuário não autenticado")
return func(usuario, *args, **kwargs)
return wrapper
@requer_autenticacao
def dados_sensiveis(usuario):
return "Dados secretos"
# Teste
usuario_valido = {"nome": "João", "autenticado": True}
print(dados_sensiveis(usuario_valido)) # Funciona
usuario_invalido = {"nome": "Invasor", "autenticado": False}
# print(dados_sensiveis(usuario_invalido)) # Levanta PermissionErrorBoas práticas
Ao criar decorators, lembre-se de:
- Usar
functools.wrapspara preservar metadados da função original. - Usar
*argse**kwargspara tornar o decorator genérico. - Documentar o decorator e seu propósito.
- Evitar decorators muito complexos; se necessário, divida em funções menores.
- Lembrar que decorators são aplicados em tempo de definição, não em tempo de chamada.
Referências
- Glossário Python: Decorator
- functools.wraps - Documentação oficial
- Real Python: Primer on Python Decorators
- PEP 318 – Decorators for Functions and Methods
- GeeksforGeeks: Decorators in Python
Exercícios
Crie um decorator chamado
maiusculasque converte o resultado de uma função em letras maiúsculas. Aplique-o a uma função que retorna uma string qualquer.✓ Resposta:import functools def maiusculas(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result.upper() return wrapper @maiusculas def saudacao(): return "olá, mundo!" print(saudacao()) # "OLÁ, MUNDO!"Crie um decorator com argumento
prefixoque adiciona um prefixo ao resultado de uma função. Exemplo:@prefixo("Resultado: ")aplicado a uma função que retorna um número.✓ Resposta:import functools def prefixo(pre): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) return f"{pre}{result}" return wrapper return decorator @prefixo("Resultado: ") def obter_numero(): return 42 print(obter_numero()) # "Resultado: 42"Implemente um decorator
contadorque conta quantas vezes uma função foi chamada. Dica: use um atributo no wrapper.✓ Resposta:import functools def contador(func): @functools.wraps(func) def wrapper(*args, **kwargs): wrapper.chamadas += 1 print(f"Chamada número {wrapper.chamadas}") return func(*args, **kwargs) wrapper.chamadas = 0 return wrapper @contador def diga_oi(): print("Oi!") diga_oi() # Chamada número 1 diga_oi() # Chamada número 2Crie um decorator que valide se o primeiro argumento de uma função é um inteiro positivo. Se não for, levante uma exceção
ValueError.✓ Resposta:import functools def valida_positivo(func): @functools.wraps(func) def wrapper(*args, **kwargs): if args and (not isinstance(args[0], int) or args[0] <= 0): raise ValueError("O primeiro argumento deve ser um inteiro positivo") return func(*args, **kwargs) return wrapper @valida_positivo def raiz_quadrada(n): return n ** 0.5 print(raiz_quadrada(9)) # 3.0 # print(raiz_quadrada(-1)) # ValueErrorUse o decorator
@lru_cachedo módulofunctoolspara memoizar uma função que calcula o n-ésimo número de Fibonacci recursivamente. Compare o desempenho com e sem o decorator chamandofib(35).✓ Resposta:import functools import time @functools.lru_cache(maxsize=None) def fib(n): if n < 2: return n return fib(n-1) + fib(n-2) start = time.time() print(fib(35)) # 9227465 print(f"Tempo com cache: {time.time() - start:.4f}s") # Sem cache (remova o decorator para testar) # start = time.time() # print(fib(35)) # print(f"Tempo sem cache: {time.time() - start:.4f}s")