Python & AI Tutorials Logo
Programação Python

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:

python
# 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: 60

Essa abordagem tem vários problemas:

  1. Duplicação de código: As linhas de logging se repetem em toda função
  2. Mistura de responsabilidades: Código de logging fica misturado com a lógica de negócio
  3. Difícil de manter: Se você quiser mudar o formato do logging, precisa atualizar todas as funções
  4. 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:

python
# 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: 60

A 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:

python
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:

python
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:

python
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: 50

A 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:

python
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: greeting

Vamos rastrear o que acontece:

  1. simple_wrapper recebe say_hello como original_func
  2. Ele cria uma nova função wrapper que:
    • Imprime "Before calling the function"
    • Chama original_func() (que é say_hello)
    • Imprime "After calling the function"
    • Retorna o resultado
  3. simple_wrapper retorna a função wrapper
  4. Quando chamamos wrapped_hello(), na verdade estamos chamando wrapper, que chama a say_hello original 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:

python
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:

  • *args coleta todos os argumentos posicionais em uma tupla
  • **kwargs coleta 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:

python
greet = flexible_wrapper(greet)

Vamos usar a sintaxe com @:

python
@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:

python
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.0

A sintaxe @log_call é exatamente equivalente a escrever:

python
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:

python
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: 30

Quando decorators são empilhados, eles são aplicados de baixo para cima (o mais próximo da função primeiro):

python
@timer          # Aplicado em segundo (camada mais externa)
@log_call       # Aplicado em primeiro (mais próximo da função)
def process_data(items):
    pass

Isso é equivalente a:

python
process_data = timer(log_call(process_data))

Ordem de aplicação (de baixo para cima):

  1. @log_call envolve a função original primeiro
  2. @timer envolve o resultado (envolve a função já envolvida)

Ordem de execução (de cima para baixo, da camada mais externa para a mais interna):

  1. O wrapper de timer começa (mais externo, executa primeiro)
  2. O wrapper de log_call começa (wrapper interno)
  3. A função original executa
  4. O wrapper de log_call termina
  5. O wrapper de timer termina (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:

Função Original
process_data

Passo 1: @log_call(decorator de baixo)

log_call envolve a original

Passo 2: @timer(decorator de cima)

timer envolve o wrapper de log_call

Final: timer envolve log_call envolve a original

Fluxo de Execução:

Chamar process_data

1. wrapper de timer começa
2. wrapper de log_call começa
3. Função original executa
4. wrapper de log_call termina
5. wrapper de timer termina

Retorna o resultado

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:

python
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 ID

Esse 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 raise sem argumento)
  • Usa um bloco try/except para 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:

python
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 digits

Esse 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:

python
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 -10

Esse 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:

python
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:

  1. Avalia @log_call → Resultado: log_call em si (o objeto função)
  2. Aplica a greet: greet = log_call(greet)

B) Fábrica de Decorators:

Com base neste exemplo:

python
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:

  1. Avalia @repeat(3) → Resultado: repeat(3) é chamada, retorna uma função decorator
  2. 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:

python
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 decorator

Ní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):

python
# O que você escreve:
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

Passo 1: O Python avalia repeat(3)

python
decorator = repeat(3)  # A fábrica retorna um decorator (times=3 é capturado)

Passo 2: O Python aplica o decorator a greet

python
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

python
greet("Alice")  # Na verdade chama wrapper("Alice")
# wrapper usa 'times' e 'func' capturados

Por Que Três Níveis?

Cada nível captura informações diferentes por meio de closures:

python
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:

python
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.0

Repare 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:

python
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 0

Quando 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.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai