38. Decorators: Adicionando Comportamento a Funções
Decorators são um dos recursos mais poderosos do Python para escrever código limpo e reutilizável. Eles permitem que você modifique ou melhore o comportamento de funções sem mudar o código delas de fato. Neste capítulo, vamos construir em cima do seu entendimento de funções de primeira classe e closures do Capítulo 23 para explorar como decorators funcionam e como usá-los de forma eficaz.
38.1) O que Decorators São e Por Que Eles São Úteis
Um decorator é uma função que recebe outra função como entrada e retorna uma versão modificada dessa função. Isso é possível porque, como vimos no Capítulo 23, funções em Python são objetos de primeira classe — elas podem ser passadas como argumentos e retornadas por outras funções. Decorators permitem que você "envolva" (wrap) um comportamento adicional em torno de funções existentes, facilitando adicionar funcionalidades comuns como logging, timing, validação ou controle de acesso sem bagunçar sua lógica principal.
Por Que Decorators Importam
Imagine que você tem várias funções no seu programa e quer registrar (log) quando cada uma é chamada. Sem decorators, você poderia escrever algo assim:
# Sem decorators - código de logging duplicado
def calculate_total(prices):
print("Calling calculate_total")
result = sum(prices)
print(f"calculate_total returned: {result}")
return result
def find_average(numbers):
print("Calling find_average")
result = sum(numbers) / len(numbers)
print(f"find_average returned: {result}")
return result
def process_order(order_id):
print("Calling process_order")
result = f"Order {order_id} processed"
print(f"process_order returned: {result}")
return result
# Usando as funções
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60Essa abordagem tem vários problemas:
- Duplicação de código: As linhas de logging se repetem em toda função
- Mistura de responsabilidades: Código de logging fica misturado com a lógica de negócio
- Difícil de manter: Se você quiser mudar o formato do logging, precisa atualizar todas as funções
- Fácil de esquecer: Funções novas podem não incluir logging
Decorators resolvem esses problemas permitindo que você separe o comportamento de logging das suas funções principais:
# Com decorators - limpo e fácil de manter
# (Vamos aprender como criar @log_calls neste capítulo)
@log_calls
def calculate_total(prices):
return sum(prices)
@log_calls
def find_average(numbers):
return sum(numbers) / len(numbers)
@log_calls
def process_order(order_id):
return f"Order {order_id} processed"
# Usar as funções produz a mesma saída
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60A diferença? O comportamento de logging é definido uma vez no decorator @log_calls e reutilizado em todo lugar. Suas funções principais continuam limpas e focadas no propósito primário delas.
Casos de Uso Comuns para Decorators
Decorators são particularmente úteis para:
- Logging: Registrar quando funções são chamadas e o que elas retornam
- Timing: Medir quanto tempo as funções levam para executar
- Validação: Verificar se argumentos de função atendem a certos requisitos
- Caching: Armazenar resultados de chamadas de função custosas para reutilização
- Controle de acesso: Checar permissões antes de permitir a execução de uma função
- Lógica de retentativa (retry): Tentar novamente automaticamente operações que falharam
- Checagem de tipos: Validar tipos de argumentos e de retorno
A principal vantagem é que você escreve o decorator uma vez e pode aplicá-lo a muitas funções com uma única linha de código.
38.2) Funções como Objetos: A Base dos Decorators
Antes de entendermos decorators, precisamos revisar e expandir o conceito de que funções são objetos de primeira classe em Python. Como vimos no Capítulo 23, isso significa que funções podem ser atribuídas a variáveis, passadas como argumentos e retornadas por outras funções.
Funções Podem Ser Atribuídas a Variáveis
Quando você define uma função, o Python cria um objeto função e o vincula a um nome:
def greet(name):
return f"Hello, {name}!"
# O objeto função pode ser atribuído a outra variável
say_hello = greet
# Ambos os nomes se referem ao mesmo objeto função
print(greet("Alice")) # Output: Hello, Alice!
print(say_hello("Bob")) # Output: Hello, Bob!Os nomes greet e say_hello ambos se referem ao mesmo objeto função. Isso é fundamental para como decorators funcionam.
Funções Podem Ser Passadas como Argumentos
Você pode passar funções para outras funções como qualquer outro valor:
def apply_twice(func, value):
"""Aplicar uma função a um valor duas vezes."""
result = func(value)
result = func(result)
return result
def add_five(x):
return x + 5
result = apply_twice(add_five, 10)
print(result) # Output: 20 (10 + 5 = 15, then 15 + 5 = 20)Aqui, apply_twice recebe a função add_five como argumento e a chama duas vezes.
Funções Podem Retornar Outras Funções
Uma função pode criar e retornar uma nova função:
def make_multiplier(factor):
"""Criar uma função que multiplica por um fator específico."""
def multiply(x):
return x * factor
return multiply
times_three = make_multiplier(3)
times_five = make_multiplier(5)
print(times_three(10)) # Output: 30
print(times_five(10)) # Output: 50A função make_multiplier retorna uma nova função que "lembra" o valor factor por meio de closure (como vimos no Capítulo 23).
Envolvendo Funções: O Padrão Central de Decorator
O padrão de decorator combina esses conceitos: uma função que recebe uma função como entrada, cria uma função wrapper que adiciona comportamento e retorna o wrapper:
def simple_wrapper(original_func):
"""Envolver uma função com comportamento adicional."""
def wrapper():
print("Before calling the function")
result = original_func()
print("After calling the function")
return result
return wrapper
def say_hello():
print("Hello!")
return "greeting"
# Envolver manualmente a função
wrapped_hello = simple_wrapper(say_hello)
return_value = wrapped_hello()
# Output:
# Before calling the function
# Hello!
# After calling the function
print(f"Returned: {return_value}")
# Output: Returned: greetingVamos rastrear o que acontece:
simple_wrapperrecebesay_hellocomooriginal_func- Ele cria uma nova função
wrapperque:- Imprime "Before calling the function"
- Chama
original_func()(que ésay_hello) - Imprime "After calling the function"
- Retorna o resultado
simple_wrapperretorna a funçãowrapper- Quando chamamos
wrapped_hello(), na verdade estamos chamandowrapper, que chama asay_hellooriginal por dentro
Esse é o padrão central por trás de todos os decorators.
Lidando com Funções com Argumentos
O wrapper acima só funciona com funções que não recebem argumentos. Para fazê-lo funcionar com qualquer função, precisamos de *args e **kwargs:
def flexible_wrapper(original_func):
"""Envolver uma função que pode aceitar quaisquer argumentos."""
def wrapper(*args, **kwargs):
# *args captura argumentos posicionais
# **kwargs captura argumentos nomeados
print("Before calling the function")
result = original_func(*args, **kwargs)
print("After calling the function")
return result
return wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
# Envolver manualmente a função
greet = flexible_wrapper(greet)
result = greet("Alice")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hello, Alice!
result = greet("Bob", greeting="Hi")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hi, Bob!Como *args e **kwargs funcionam:
Como vimos no Capítulo 20, *args e **kwargs permitem que funções aceitem um número variável de argumentos:
*argscoleta todos os argumentos posicionais em uma tupla**kwargscoleta todos os argumentos nomeados em um dicionário- Quando chamamos
original_func(*args, **kwargs), nós os desempacotamos de volta como argumentos para a função original
Esse padrão permite que nosso wrapper funcione com qualquer função, independentemente de quantos argumentos ela receba.
Indo para uma Sintaxe Mais Limpa
Esse padrão é a base de decorators. A sintaxe de decorator que vamos aprender a seguir é apenas uma forma mais limpa de aplicar esse padrão. Em vez de escrever:
greet = flexible_wrapper(greet)Vamos usar a sintaxe com @:
@flexible_wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"Ambos fazem exatamente a mesma coisa — a sintaxe @ é apenas açúcar sintático (syntactic sugar) que deixa o código mais limpo e legível.
38.3) A Sintaxe @decorator: Aplicação Mais Limpa
Escrever function_name = decorator(function_name) funciona, mas é verboso e fácil de esquecer. O Python fornece a sintaxe @decorator como uma forma mais limpa de aplicar decorators.
Usando o Símbolo @
Em vez de envolver manualmente uma função, você pode colocar @decorator_name na linha imediatamente antes da definição da função:
def log_call(func):
"""Decorator que registra chamadas de função."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def calculate_total(prices):
return sum(prices)
@log_call
def find_average(numbers):
return sum(numbers) / len(numbers)
# Usar as funções decoradas
total = calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = find_average([10, 20, 30])
# Output:
# Calling find_average
# find_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0A sintaxe @log_call é exatamente equivalente a escrever:
def calculate_total(prices):
return sum(prices)
calculate_total = log_call(calculate_total)Mas a sintaxe @ é muito mais limpa e deixa imediatamente óbvio que a função está decorada.
Empilhando Múltiplos Decorators
Você pode aplicar múltiplos decorators à mesma função, empilhando-os:
import time
def log_call(func):
"""Decorator que registra chamadas de função."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
def timer(func):
"""Decorator que mede o tempo de execução da função."""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start_time
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timer
@log_call
def process_data(items):
total = sum(items)
return total * 2
result = process_data([1, 2, 3, 4, 5])
# Output:
# Calling process_data
# process_data returned: 30
# process_data took 0.0001 seconds
print(f"Final result: {result}")
# Output: Final result: 30Quando decorators são empilhados, eles são aplicados de baixo para cima (o mais próximo da função primeiro):
@timer # Aplicado em segundo (camada mais externa)
@log_call # Aplicado em primeiro (mais próximo da função)
def process_data(items):
passIsso é equivalente a:
process_data = timer(log_call(process_data))Ordem de aplicação (de baixo para cima):
@log_callenvolve a função original primeiro@timerenvolve o resultado (envolve a função já envolvida)
Ordem de execução (de cima para baixo, da camada mais externa para a mais interna):
- O wrapper de
timercomeça (mais externo, executa primeiro) - O wrapper de
log_callcomeça (wrapper interno) - A função original executa
- O wrapper de
log_calltermina - O wrapper de
timertermina (mais externo, termina por último)
Pense em decorators como camadas de papel de presente — você os aplica de dentro para fora, mas quando desembrulha (executa), você vai de fora para dentro.
Aplicação de Decorator:
Fluxo de Execução:
38.4) Exemplos Práticos de Decorators (Logging, Timing, Validação)
Agora vamos explorar vários decorators práticos que você pode usar em programas reais. Esses exemplos demonstram padrões comuns e mostram como decorators resolvem problemas do mundo real.
Exemplo 1: Decorator de Logging Melhorado
Um decorator de logging mais sofisticado que inclui timestamps e trata exceções:
import time
def log_with_timestamp(func):
"""Decorator que registra chamadas de função com timestamps."""
def wrapper(*args, **kwargs):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] Calling {func.__name__}")
try:
result = func(*args, **kwargs)
print(f"[{timestamp}] {func.__name__} completed successfully")
return result
except Exception as e:
print(f"[{timestamp}] {func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
@log_with_timestamp
def divide(a, b):
return a / b
@log_with_timestamp
def process_user(user_id):
# Simular processamento
if user_id < 0:
raise ValueError("User ID must be positive")
return f"Processed user {user_id}"
# Testar execução bem-sucedida
result = divide(10, 2)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide completed successfully
print(f"Result: {result}")
# Output: Result: 5.0
# Testar execução bem-sucedida com validação
user = process_user(42)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user completed successfully
print(user)
# Output: Processed user 42
# Testar tratamento de exceção
try:
divide(10, 0)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide raised ZeroDivisionError: division by zero
except ZeroDivisionError:
print("Handled division by zero")
# Output: Handled division by zero
try:
process_user(-5)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user raised ValueError: User ID must be positive
except ValueError:
print("Handled invalid user ID")
# Output: Handled invalid user IDEsse decorator:
- Adiciona timestamps a todas as mensagens de log
- Registra tanto conclusões bem-sucedidas quanto exceções
- Relança exceções depois de registrá-las (usando
raisesem argumento) - Usa um bloco
try/exceptpara capturar e registrar qualquer exceção
Exemplo 2: Decorator de Timing de Performance
Um decorator que mede e relata o tempo de execução da função:
import time
def measure_time(func):
"""Decorator que mede e relata o tempo de execução."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
# Formatar o tempo de forma apropriada
if elapsed < 0.001:
time_str = f"{elapsed * 1000000:.2f} microseconds"
elif elapsed < 1:
time_str = f"{elapsed * 1000:.2f} milliseconds"
else:
time_str = f"{elapsed:.2f} seconds"
print(f"{func.__name__} executed in {time_str}")
return result
return wrapper
@measure_time
def find_primes(limit):
"""Encontrar todos os números primos até limit."""
primes = []
for num in range(2, limit):
is_prime = True
for divisor in range(2, int(num ** 0.5) + 1):
if num % divisor == 0:
is_prime = False
break
if is_prime:
primes.append(num)
return primes
@measure_time
def calculate_factorial(n):
"""Calcular o fatorial de n."""
result = 1
for i in range(1, n + 1):
result *= i
return result
# Testar as funções decoradas
primes = find_primes(1000)
# Output: find_primes executed in 15.23 milliseconds
print(f"Found {len(primes)} primes")
# Output: Found 168 primes
factorial = calculate_factorial(100)
# Output: calculate_factorial executed in 45.67 microseconds
print(f"Factorial has {len(str(factorial))} digits")
# Output: Factorial has 158 digitsEsse decorator formata automaticamente a medição de tempo de forma apropriada (microseconds, milliseconds ou seconds) com base na duração.
Exemplo 3: Decorator de Validação de Entrada
Um decorator que valida argumentos de função antes da execução:
def validate_positive(func):
"""Decorator que garante que todos os argumentos numéricos sejam positivos."""
def wrapper(*args, **kwargs):
# Verificar argumentos posicionais
for i, arg in enumerate(args):
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(
f"Argument {i} to {func.__name__} must be positive, got {arg}"
)
# Verificar argumentos nomeados
for key, value in kwargs.items():
if isinstance(value, (int, float)) and value <= 0:
raise ValueError(
f"Argument '{key}' to {func.__name__} must be positive, got {value}"
)
return func(*args, **kwargs)
return wrapper
@validate_positive
def calculate_area(width, height):
"""Calcular a área de um retângulo."""
return width * height
@validate_positive
def calculate_discount(price, discount_percent):
"""Calcular preço com desconto."""
discount = price * (discount_percent / 100)
return price - discount
# Testar entradas válidas
area = calculate_area(10, 5)
print(f"Area: {area}")
# Output: Area: 50
discounted = calculate_discount(100, 20)
print(f"Discounted price: ${discounted:.2f}")
# Output: Discounted price: $80.00
# Testar entradas inválidas
try:
calculate_area(-5, 10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 0 to calculate_area must be positive, got -5
try:
calculate_discount(100, discount_percent=-10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 'discount_percent' to calculate_discount must be positive, got -10Esse decorator:
- Verifica todos os argumentos numéricos (tanto posicionais quanto nomeados)
- Levanta um erro descritivo se algum não for positivo
- Fornece mensagens de erro claras indicando qual argumento falhou na validação
38.5) (Opcional) Decorators com Argumentos
Até agora, todos os nossos decorators foram funções simples que recebem uma função como entrada. Mas e se você quiser configurar o comportamento de um decorator? Por exemplo, você pode querer um decorator de retry em que dá para especificar o número de tentativas, ou um decorator de logging em que dá para especificar o nível de log.
Decorators com argumentos exigem um nível extra de aninhamento de funções. Em vez de um decorator ser uma função que recebe uma função, ele vira uma função que recebe argumentos e retorna um decorator.
O Padrão: Fábricas de Decorators
Um decorator com argumentos é, na verdade, uma fábrica de decorators (decorator factory) — uma função que cria e retorna um decorator. A chave para entender isso é saber o que o Python faz com o símbolo @.
O Princípio-Chave: O Python Avalia @ Primeiro
O Python sempre avalia primeiro o que vem depois de @ e, então, usa o resultado para decorar sua função.
Vamos comparar:
A) Decorator Básico:
Com base neste exemplo:
def log_call(func):
"""Decorator que registra chamadas de função."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def greet(name):
return f"Hello, {name}!"O que o Python faz:
- Avalia
@log_call→ Resultado:log_callem si (o objeto função) - Aplica a
greet:greet = log_call(greet)
B) Fábrica de Decorators:
Com base neste exemplo:
def repeat(times):
"""Nível 1: Fábrica - recebe configuração"""
def decorator(func):
"""Nível 2: Decorator - recebe a função a decorar"""
def wrapper(*args, **kwargs):
"""Nível 3: Wrapper - executa quando a função decorada é chamada"""
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!O que o Python faz:
- Avalia
@repeat(3)→ Resultado:repeat(3)é chamada, retorna uma função decorator - Aplica esse decorator a
greet:greet = decorator(greet)
A diferença: @log_call dá a própria função, mas @repeat(3) chama uma função (repeat) que retorna um decorator.
Entendendo os Três Níveis
Uma fábrica de decorators tem três funções aninhadas, cada uma com um papel específico:
def repeat(times): # Nível 1: Fábrica
def decorator(func): # Nível 2: Decorator
def wrapper(*args, **kwargs): # Nível 3: Wrapper
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decoratorNível 1 - Fábrica (repeat):
- Recebe: Configuração (
times) - Retorna: Uma função decorator
- É chamada: Quando o Python avalia
@repeat(3)
Nível 2 - Decorator (decorator):
- Recebe: A função a decorar (
func) - Retorna: Uma função wrapper
- É chamada: Imediatamente após o Nível 1, como parte da sintaxe com @
Nível 3 - Wrapper (wrapper):
- Recebe: Os argumentos da função quando ela é chamada (
*args, **kwargs) - Retorna: O resultado
- É chamada: Toda vez que você chama a função decorada
Execução Passo a Passo
Vamos rastrear o que acontece com @repeat(3):
# O que você escreve:
@repeat(3)
def greet(name):
print(f"Hello, {name}!")Passo 1: O Python avalia repeat(3)
decorator = repeat(3) # A fábrica retorna um decorator (times=3 é capturado)Passo 2: O Python aplica o decorator a greet
def greet(name):
print(f"Hello, {name}!")
greet = decorator(greet) # O decorator retorna um wrapper (func=greet é capturado)Observação: Neste ponto, greet agora se refere à função wrapper. A greet original é capturada em func.
Passo 3: Quando você chama greet("Alice"), o wrapper executa
greet("Alice") # Na verdade chama wrapper("Alice")
# wrapper usa 'times' e 'func' capturadosPor Que Três Níveis?
Cada nível captura informações diferentes por meio de closures:
def repeat(times): # Captura: times
def decorator(func): # Captura: func (e lembra de times)
def wrapper(*args, **kwargs): # Captura: times, func, e recebe args
for i in range(times): # Usa o 'times' capturado
result = func(*args, **kwargs) # Usa 'func' e 'args' capturados
return result
return wrapper
return decorator- Nível 1 captura a configuração (
times) - Nível 2 captura a função a decorar (
func) - Nível 3 recebe os argumentos quando a função é chamada (
args,kwargs)
Sem os três níveis, não conseguiríamos ter um decorator configurável que lembra tanto as configurações quanto a função que ele está decorando.
Exemplo 1: Um Decorator de Logging Configurável
Aqui vai um exemplo prático de um decorator de logging que aceita configuração:
def log_with_prefix(prefix="LOG"):
"""Fábrica de decorators que cria um decorator de logging com um prefixo personalizado."""
def decorator(func):
def wrapper(*args, **kwargs):
print(f"[{prefix}] Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"[{prefix}] {func.__name__} returned: {result}")
return result
return wrapper
return decorator
@log_with_prefix(prefix="INFO")
def calculate_total(prices):
return sum(prices)
@log_with_prefix() # Usar prefixo padrão
def get_average(numbers):
return sum(numbers) / len(numbers)
# Testar as funções decoradas
total = calculate_total([10, 20, 30])
# Output:
# [INFO] Calling calculate_total
# [INFO] calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = get_average([10, 20, 30])
# Output:
# [LOG] Calling get_average
# [LOG] get_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0Repare que:
@log_with_prefix(prefix="INFO")usa um prefixo personalizado@log_with_prefix()usa o prefixo padrão "LOG"- Você precisa incluir parênteses mesmo quando usa os padrões
Exemplo 2: Um Decorator com Múltiplos Argumentos
Aqui vai um decorator que valida intervalos numéricos:
def validate_range(min_value=None, max_value=None):
"""
Fábrica de decorators que valida se argumentos numéricos estão dentro de um intervalo.
Args:
min_value: Valor mínimo permitido (inclusivo)
max_value: Valor máximo permitido (inclusivo)
"""
def decorator(func):
def wrapper(*args, **kwargs):
# Verificar todos os argumentos numéricos
all_args = list(args) + list(kwargs.values())
for arg in all_args:
if isinstance(arg, (int, float)):
if min_value is not None and arg < min_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is below minimum {min_value}"
)
if max_value is not None and arg > max_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is above maximum {max_value}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_range(min_value=0, max_value=100)
def calculate_percentage(value, total):
"""Calcular porcentagem."""
return (value / total) * 100
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
"""Calcular idade a partir do ano de nascimento."""
return current_year - birth_year
# Testar entradas válidas
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")
# Output: Percentage: 25.0%
age = calculate_age(1990, 2025)
print(f"Age: {age}")
# Output: Age: 35
# Testar entradas inválidas
try:
calculate_percentage(150, 100)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_percentage received 150, which is above maximum 100
try:
calculate_age(-5, 2025)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_age received -5, which is below minimum 0Quando Usar Decorators com Argumentos
Use decorators com argumentos quando:
- Você precisa configurar o comportamento do decorator
- O mesmo decorator deve se comportar de forma diferente em contextos diferentes
- Você quer tornar decorators mais reutilizáveis e flexíveis
Exemplos comuns incluem:
- Decorators de retry com tentativas e atrasos configuráveis
- Decorators de logging com níveis ou formatos de log configuráveis
- Decorators de validação com regras configuráveis
- Decorators de caching com tamanhos de cache ou tempos de expiração configuráveis
- Limitação de taxa (rate limiting) com limites configuráveis
Uma Observação sobre Complexidade
Decorators com argumentos adicionam um nível extra de complexidade. Ao escrevê-los:
- Use nomes de parâmetros claros e descritivos
- Forneça valores padrão sensatos
- Inclua docstrings explicando os parâmetros
- Considere se a flexibilidade adicional vale a complexidade
Para casos simples, um decorator sem argumentos costuma ser mais claro e mais fácil de entender.
Decorators são uma ferramenta poderosa para escrever código Python limpo e fácil de manter. Eles permitem que você separe preocupações transversais (como logging, timing e validação) da sua lógica de negócio principal, deixando seu código mais fácil de ler, testar e modificar. À medida que você continuar programando em Python, vai encontrar decorators sendo usados extensivamente em frameworks e bibliotecas, e vai descobrir muitas oportunidades de escrever seus próprios decorators para resolver problemas comuns de forma elegante.