Decorators são funções que modificam o comportamento de outras funções.
Eles usam o símbolo @ antes da definição da função.
Primeiramente, decorators envolvem uma função para estendê-la.
Por exemplo, @timer pode medir o tempo de execução.
Além disso, decorators permitem reutilizar código de forma elegante.
Assim, você evita repetir a mesma lógica em várias funções.
Consequentemente, o código fica mais seco e manutenível.
Quando utilizar decorators? Em logging, cache, autenticação e validação.
Também para controle de acesso e medição de performance.
Por outro lado, evite decorators para lógica muito específica.
Python tem decorators nativos como @staticmethod e @property.
Então, vamos explorar como criar e usar decorators personalizados.
Três subtítulos guiarão você pelo mundo dos decorators.
Portanto, ao final, você adicionará poderosas funcionalidades às suas funções.
Criando decorators básicos
Um decorator é uma função que recebe outra função como argumento.
Ele retorna uma nova função que substitui a original.
Quando usar decorators básicos? Para adicionar comportamento antes/depois.
Por exemplo, logging, timing ou autenticação simples.
Além disso, sempre preserve os metadados com @wraps.
Exemplo de decorators básicos:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
import time from functools import wraps # Decorator para medir tempo de execução def timer(func): """Mede o tempo de execução da função decorada.""" @wraps(func) # Preserva metadados da função original def wrapper(*args, **kwargs): inicio = time.time() resultado = func(*args, **kwargs) fim = time.time() print(f"{func.__name__} levou {fim - inicio:.4f}s") return resultado return wrapper # Decorator para logging def log(func): """Registra chamadas da função.""" @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 # Usando os decorators @timer @log def dormir(segundos): time.sleep(segundos) return f"Dormi por {segundos}s" print("=== Decorators Básicos ===\n") resultado = dormir(1) print(f"Resultado: {resultado}") # Decorator para repetir execução def repetir(vezes=3): """Repete a execução da função N vezes.""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): resultados = [] for i in range(vezes): print(f"Execução {i+1}/{vezes}") resultados.append(func(*args, **kwargs)) return resultados return wrapper return decorator @repetir(vezes=2) def saudacao(nome): return f"Olá, {nome}!" print("\n=== Decorator com Parâmetros ===") print(saudacao("Ana")) # Decorator para depuração def debug(func): """Mostra entrada e saída da função.""" @wraps(func) def wrapper(*args, **kwargs): args_repr = [repr(a) for a in args] kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] assinatura = ", ".join(args_repr + kwargs_repr) print(f"Chamando {func.__name__}({assinatura})") resultado = func(*args, **kwargs) print(f"{func.__name__} retornou {resultado!r}") return resultado return wrapper @debug def soma(a, b, multiplicador=1): return (a + b) * multiplicador print("\n=== Decorator de Depuração ===") soma(3, 5, multiplicador=2) |
Decorators básicos são simples e extremamente úteis. Eles separam preocupações transversais do código principal. Portanto, use-os para funcionalidades repetitivas.
Decorators com argumentos e parâmetros
Decorators podem receber argumentos para serem configuráveis. A estrutura tem três níveis: decorator, wrapper interna e função. Quando usar decorators com argumentos? Em comportamentos parametrizáveis. Por exemplo, limites de tempo, contagem de tentativas ou níveis de log. Assim, você cria decorators reutilizáveis e flexíveis. Exemplo de decorators com argumentos:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
from functools import wraps import time # Decorator com argumento (máximo de tentativas) def retry(max_tentativas=3, delay=1): """Tenta executar a função novamente em caso de erro.""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for tentativa in range(max_tentativas): try: return func(*args, **kwargs) except Exception as e: if tentativa == max_tentativas - 1: raise print(f"Tentativa {tentativa+1} falhou: {e}") time.sleep(delay) return None return wrapper return decorator @retry(max_tentativas=3, delay=0.5) def funcao_instavel(): import random if random.random() < 0.7: raise ValueError("Erro aleatório!") return "Sucesso!" print("=== Decorator com Argumentos (retry) ===") try: print(funcao_instavel()) except ValueError: print("Falha após todas as tentativas") # Decorator para timeout def timeout(segundos): """Cancela a função se exceder o tempo limite.""" import signal def decorator(func): @wraps(func) def wrapper(*args, **kwargs): def handler(signum, frame): raise TimeoutError(f"Função excedeu {segundos}s") signal.signal(signal.SIGALRM, handler) signal.alarm(segundos) try: resultado = func(*args, **kwargs) finally: signal.alarm(0) return resultado return wrapper return decorator @timeout(2) def operacao_lenta(): time.sleep(3) return "Nunca chegará" print("\n=== Decorator com Timeout ===") try: print(operacao_lenta()) except TimeoutError as e: print(f"Timeout: {e}") # Decorator para cache (memorização) def cache(maxsize=128): """Cache simples para resultados de funções.""" def decorator(func): cache_dict = {} @wraps(func) def wrapper(*args, **kwargs): # Cria chave a partir dos argumentos chave = (args, tuple(sorted(kwargs.items()))) if chave in cache_dict: print(f"Cache hit para {func.__name__}{args}") return cache_dict[chave] print(f"Cache miss para {func.__name__}{args}") resultado = func(*args, **kwargs) # Limita tamanho do cache if len(cache_dict) >= maxsize: cache_dict.pop(next(iter(cache_dict))) cache_dict[chave] = resultado return resultado return wrapper return decorator @cache(maxsize=2) def fibonacci(n): if n < 2: return n return fibonacci(n-1) + fibonacci(n-2) print("\n=== Decorator com Cache ===") print(f"fibonacci(10): {fibonacci(10)}") print(f"fibonacci(10) (segunda vez): {fibonacci(10)}") |
Decorators com argumentos oferecem grande flexibilidade. Eles permitem configurar o comportamento dinamicamente. Portanto, use-os para criar ferramentas reutilizáveis.
Decorators práticos: autenticação e validação
Decorators são excelentes para validação e controle de acesso. Eles verificam pré-condições antes de executar a função. Quando usar esses decorators? Em APIs, rotas web e sistemas de permissão. Por exemplo, verificar se usuário está logado ou dados são válidos. Além disso, eles centralizam a lógica de segurança. Exemplo de decorators práticos:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
from functools import wraps # Decorator para validação de tipos def type_check(**tipos): """Valida os tipos dos argumentos.""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): # Combina args e kwargs nomes_args = func.__code__.co_varnames[:len(args)] valores = dict(zip(nomes_args, args)) valores.update(kwargs) for nome, tipo_esperado in tipos.items(): if nome in valores: valor = valores[nome] if not isinstance(valor, tipo_esperado): raise TypeError( f"Argumento '{nome}' deve ser {tipo_esperado.__name__}, " f"recebeu {type(valor).__name__}" ) return func(*args, **kwargs) return wrapper return decorator @type_check(a=int, b=int) def somar_numeros(a, b): return a + b print("=== Decorator de Validação de Tipos ===") print(f"somar_numeros(5, 3): {somar_numeros(5, 3)}") try: print(somar_numeros(5, "3")) except TypeError as e: print(f"Erro: {e}") # Decorator para autenticação (simulada) def requer_login(func): """Verifica se o usuário está autenticado.""" @wraps(func) def wrapper(usuario, *args, **kwargs): if not usuario.get("autenticado", False): raise PermissionError("Usuário não autenticado") return func(usuario, *args, **kwargs) return wrapper def requer_permissao(permissao_necessaria): """Verifica se o usuário tem uma permissão específica.""" def decorator(func): @wraps(func) def wrapper(usuario, *args, **kwargs): if not usuario.get("autenticado", False): raise PermissionError("Usuário não autenticado") if permissao_necessaria not in usuario.get("permissoes", []): raise PermissionError( f"Usuário não tem permissão '{permissao_necessaria}'" ) return func(usuario, *args, **kwargs) return wrapper return decorator # Usuários de exemplo usuario_comum = {"nome": "Ana", "autenticado": True, "permissoes": ["ler"]} usuario_admin = {"nome": "Carlos", "autenticado": True, "permissoes": ["ler", "escrever", "deletar"]} usuario_nao_auth = {"nome": "João", "autenticado": False} @requer_login def perfil(usuario): return f"Perfil de {usuario['nome']}" @requer_permissao("escrever") def criar_post(usuario, titulo): return f"Post '{titulo}' criado por {usuario['nome']}" print("\n=== Decorator de Autenticação ===") print(perfil(usuario_comum)) print(criar_post(usuario_admin, "Meu post")) try: print(criar_post(usuario_comum, "Tentativa")) except PermissionError as e: print(f"Erro: {e}") # Decorator para rate limiting (limite de chamadas) def rate_limit(max_chamadas, janela_segundos): """Limita o número de chamadas por janela de tempo.""" def decorator(func): chamadas = [] @wraps(func) def wrapper(*args, **kwargs): agora = time.time() # Remove chamadas antigas while chamadas and chamadas[0] < agora - janela_segundos: chamadas.pop(0) if len(chamadas) >= max_chamadas: raise RuntimeError( f"Limite de {max_chamadas} chamadas em {janela_segundos}s excedido" ) chamadas.append(agora) return func(*args, **kwargs) return wrapper return decorator @rate_limit(max_chamadas=3, janela_segundos=5) def api_request(url): return f"Requisição para {url}" print("\n=== Decorator de Rate Limit ===") for i in range(5): try: print(api_request(f"url_{i}")) except RuntimeError as e: print(f"Erro: {e}") time.sleep(1) # Empilhando múltiplos decorators @timer @log @retry(max_tentativas=2, delay=0.1) def operacao_critica(): import random if random.random() < 0.5: raise ValueError("Falha aleatória") return "Operação bem-sucedida!" print("\n=== Múltiplos Decorators ===") try: resultado = operacao_critica() print(f"Resultado: {resultado}") except ValueError: print("Operação falhou após tentativas") |
Decorators são ferramentas essenciais no dia a dia do Python. A fórmula de um decorator pode ser expressa assim: \(D(f) = \text{wrapper} \quad \text{onde} \quad \text{wrapper} \approx \text{antes} + f + \text{depois}\) Use decorators para separar preocupações transversais. Seu código será mais limpo, modular e reutilizável. Portanto, comece com decorators simples e avance para os mais complexos. Finalmente, pratique criando seus próprios decorators.