Decorators
Esta aula explora decorators em Python, desde o conceito básico até casos de uso avançados. Você aprenderá a sintaxe com @, como criar decorators que aceitam argumentos e aplicações práticas como logging, timing e validação.
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, você entenderá o que são decorators, como funcionam por baixo dos panos e como aplicá-los em situações reais.
Um decorator é essencialmente uma função que recebe outra função como argumento e retorna uma nova função que geralmente adiciona alguma funcionalidade. A sintaxe com o símbolo @ torna isso ainda mais limpo e legível.
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 é uma função que recebe uma função e retorna uma função modificada. Isso permite que você "envolva" uma função com código adicional que é executado antes, depois ou ao redor da função original.
Por exemplo, suponha que você queira medir o tempo de execução de uma função. Sem decorators, você precisaria adicionar código de temporização dentro de cada função. Com decorators, você cria uma função genérica que pode ser aplicada a qualquer função.
def meu_decorator(func):
def wrapper():
print("Antes da função")
func()
print("Depois da função")
return wrapper
def dizer_oi():
print("Oi!")
dizer_oi = meu_decorator(dizer_oi)
dizer_oi()
# Saída:
# Antes da função
# Oi!
# Depois da funçãoNo exemplo acima, meu_decorator é um decorator que envolve a função dizer_oi com ações extras. Note que a função original é substituída pela função wrapper.
Sintaxe @
Python fornece um açúcar sintático para aplicar decorators: o símbolo @. Em vez de escrever dizer_oi = meu_decorator(dizer_oi), você pode simplesmente colocar @meu_decorator acima da definição da função.
@meu_decorator
def dizer_oi():
print("Oi!")
dizer_oi()
# Saída:
# Antes da função
# Oi!
# Depois da funçãoIsso é equivalente à atribuição manual. A sintaxe @ torna o código mais limpo e explícito. É importante notar que o decorator é aplicado no momento da definição da função, não na chamada.
Decorators com argumentos
Às vezes, você precisa passar argumentos para o decorator. Por exemplo, um decorator que repete a execução de uma função um número específico de vezes. Para isso, você precisa de uma função que retorne um decorator.
def repetir(vezes):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(vezes):
func(*args, **kwargs)
return wrapper
return decorator
@repetir(3)
def cumprimentar(nome):
print(f"Olá, {nome}!")
cumprimentar("João")
# Saída:
# Olá, João!
# Olá, João!
# Olá, João!Neste exemplo, repetir é uma função que recebe o argumento vezes e retorna um decorator. O decorator, por sua vez, recebe a função original e retorna a função wrapper que executa a repetição. A sintaxe @repetir(3) primeiro chama repetir(3) para obter o decorator e depois aplica-o à função.
É comum usar *args e **kwargs no wrapper para que o decorator funcione com funções de qualquer assinatura.
Casos de uso
Decorators são amplamente utilizados na prática. Alguns exemplos comuns incluem:
- Logging: registrar chamadas de função, argumentos e resultados.
- Timing: medir o tempo de execução de funções.
- Validação: verificar argumentos ou permissões antes de executar a função.
- Cache: armazenar resultados de funções caras para evitar recálculo.
- Autenticação: em frameworks web, verificar se o usuário está logado.
Vamos ver um 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}")
resultado = func(*args, **kwargs)
print(f"{func.__name__} retornou {resultado}")
return resultado
return wrapper
@log
def somar(a, b):
return a + b
somar(3, 5)
# Saída:
# Chamando somar com args=(3, 5), kwargs={}
# somar retornou 8Note o uso de functools.wraps para preservar os metadados da função original (como __name__ e __doc__). Isso é uma boa prática ao criar decorators.
Outro caso útil é um decorator que mede o tempo de execução:
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
inicio = time.time()
resultado = func(*args, **kwargs)
fim = time.time()
print(f"{func.__name__} levou {fim - inicio:.4f} segundos")
return resultado
return wrapper
@timer
def tarefa_pesada():
time.sleep(2)
return "Pronto"
tarefa_pesada()
# Saída: tarefa_pesada levou 2.0001 segundosDecorators também são a base de muitos frameworks, como Flask (rotas) e Django (permissões). Eles permitem separar preocupações transversais do código principal, promovendo a reutilização e a clareza.
Boas práticas
- Sempre use
functools.wrapspara preservar os metadados da função original. - Mantenha os decorators simples e focados em uma única responsabilidade.
- Documente o comportamento do decorator, especialmente se ele modificar a assinatura da função.
- Considere usar classes como decorators quando precisar de estado ou configuração mais complexa.
Referências
- Python Glossary: Decorator
- functools.wraps documentation
- Real Python: Primer on Python Decorators
- PEP 318 – Decorators for Functions and Methods
- GeeksforGeeks: Decorators in Python
Exercícios
Crie um decorator simples chamado
maiusculasque converte o resultado de uma função em letras maiúsculas. A função decorada deve retornar uma string.✓ Resposta:def maiusculas(func): def wrapper(*args, **kwargs): resultado = func(*args, **kwargs) return resultado.upper() return wrapper @maiusculas def saudacao(nome): return f"Olá, {nome}!" print(saudacao("Maria")) # SAÍDA: OLÁ, MARIA!Crie um decorator que mede o tempo de execução de uma função e exibe o tempo em segundos com duas casas decimais. Use
functools.wraps.✓ Resposta:import time import functools def timer(func): @functools.wraps(func) def wrapper(*args, **kwargs): inicio = time.time() resultado = func(*args, **kwargs) fim = time.time() print(f"{func.__name__} levou {fim - inicio:.2f} segundos") return resultado return wrapper @timer def dormir(): time.sleep(1.5) return "Acordei" print(dormir()) # Exibe o tempo e retorna "Acordei"Crie um decorator com argumento chamado
validar_positivoque verifica se todos os argumentos numéricos passados para a função são positivos. Se algum não for, levanta uma exceçãoValueError.✓ Resposta:def validar_positivo(func): def wrapper(*args, **kwargs): for arg in args: if isinstance(arg, (int, float)) and arg <= 0: raise ValueError(f"Argumento {arg} não é positivo") for key, value in kwargs.items(): if isinstance(value, (int, float)) and value <= 0: raise ValueError(f"Argumento {key}={value} não é positivo") return func(*args, **kwargs) return wrapper @validar_positivo def multiplicar(a, b): return a * b print(multiplicar(3, 5)) # 15 # print(multiplicar(-1, 5)) # ValueErrorCrie um decorator que repete a execução de uma função um número de vezes definido por um argumento do decorator. O decorator deve ser chamado como
@repetir(3).✓ Resposta:def repetir(vezes): def decorator(func): def wrapper(*args, **kwargs): for _ in range(vezes): func(*args, **kwargs) return wrapper return decorator @repetir(3) def diga_oi(): print("Oi!") diga_oi() # Imprime "Oi!" três vezesCrie um decorator que registre em um arquivo de log as chamadas de função, incluindo timestamp, nome da função e argumentos. Use a biblioteca
logging.✓ Resposta:import logging import functools logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s') def log_chamada(func): @functools.wraps(func) def wrapper(*args, **kwargs): logging.info(f"Chamando {func.__name__} com args={args}, kwargs={kwargs}") resultado = func(*args, **kwargs) logging.info(f"{func.__name__} retornou {resultado}") return resultado return wrapper @log_chamada def somar(a, b): return a + b somar(2, 3) # Registra no log