Decoradores em Python

PythonBeginner
Pratique Agora

Introdução

Neste laboratório, você aprenderá sobre Decoradores Python (Python Decorators), um recurso poderoso que fornece uma maneira concisa e reutilizável de estender a funcionalidade de uma função ou classe sem modificá-la permanentemente.

Você começará construindo um decorador simples, depois aprenderá a lidar com parâmetros de função, aplicar as melhores práticas usando functools.wraps, criar decoradores que aceitam seus próprios argumentos e, finalmente, implementar decoradores usando classes para gerenciar estado (state).

Decorador Básico (Estrutura Mínima)

Nesta etapa, você aprenderá a estrutura fundamental de um decorador em Python. Um decorador, em sua forma mais simples, é uma função que recebe outra função como argumento, adiciona alguma funcionalidade e, em seguida, retorna outra função.

Nota: Se você não está familiarizado com o uso do WebIDE (semelhante ao VS Code), aprenda primeiro neste laboratório neste laboratório.

Primeiro, abra o arquivo step1.py no explorador de arquivos à esquerda.

Agora, copie o código a seguir para o arquivo step1.py. Este código define um decorador simples e o aplica a uma função.

## Decorator: a function that takes another function and returns a wrapper
def your_decorator(func):
  def wrapper():
    ## Do stuff before func...
    print("Before func!")
    func()  ## Call the original function
    ## Do stuff after func...
    print("After func!")
  return wrapper  ## Return the wrapper function

## @your_decorator is syntactic sugar for: foo = your_decorator(foo)
@your_decorator
def foo():
  print("Hello World!")

foo()  ## Calls wrapper, which calls foo with extra behavior

Vamos analisar o código:

  • your_decorator(func): Esta é a função decoradora. Ela recebe um argumento, func, que é a função a ser decorada.
  • wrapper(): Esta é uma função interna que "envolve" (wraps) a função original. Ela contém o novo comportamento que você deseja adicionar. Aqui, ela imprime mensagens antes e depois de chamar a função original.
  • @your_decorator: Este é o açúcar sintático (syntactic sugar) para aplicar o decorador. É equivalente a escrever foo = your_decorator(foo) após a definição da função.

Agora, execute o script no terminal para ver o decorador em ação.

python3 step1.py

Você deverá ver a seguinte saída, que mostra que a função wrapper foi executada, adicionando comportamento em torno da função original foo.

Before func!
Hello World!
After func!

Decorador para uma Função com Parâmetros

Nesta etapa, você estenderá seu decorador para funcionar com funções que aceitam argumentos. Uma função do mundo real frequentemente possui parâmetros, e nosso decorador precisa ser capaz de lidar com eles corretamente.

Para fazer isso, você pode usar *args e **kwargs na assinatura da função wrapper. Isso permite que o wrapper aceite qualquer número de argumentos posicionais e de palavra-chave (keyword arguments) e os repasse para a função original.

Abra o arquivo step2.py e adicione o seguinte código:

## Decorator that works with functions that have parameters
def your_decorator(func):
  def wrapper(*args,**kwargs):  ## Accept any arguments
    ## Do stuff before func...
    print("Before func!")
    func(*args,**kwargs)  ## Pass arguments to original function
    ## Do stuff after func...
    print("After func!")
  return wrapper

@your_decorator
def foo(bar):
  print("My name is " + bar)

foo("Jack")  ## Arguments are passed through wrapper

Neste código, a função wrapper é definida com *args e **kwargs. Quando foo("Jack") é chamada, o argumento "Jack" é capturado por *args no wrapper e depois passado para a função original func quando func(*args, **kwargs) é executado.

Execute o script para ver o resultado.

python3 step2.py

A saída mostra que o argumento foi passado com sucesso através do decorador para a função original.

Before func!
My name is Jack
After func!

Template para um Decorador Básico

Nesta etapa, você descobrirá o template de melhores práticas para criar decoradores robustos. Embora os decoradores anteriores funcionassem, eles tinham um efeito colateral: eles perdiam os metadados da função original (como seu nome __name__ e docstring __doc__).

Para resolver isso, o módulo functools do Python fornece um decorador auxiliar chamado wraps. Além disso, um bom decorador deve ser capaz de lidar com funções que retornam valores.

Este template é útil para a maioria dos casos de uso de decoradores. É válido para funções com ou sem parâmetros, e com ou sem valor de retorno.

Abra o arquivo step3.py e adicione o seguinte código.

import functools

## Best practice decorator template: preserves function metadata and return value
def your_decorator(func):
  @functools.wraps(func)  ## Preserves function name, docstring, etc.
  def wrapper(*args,**kwargs):
    print("Before function execution...")
    result = func(*args,**kwargs)  ## Call function and capture return value
    print("After function execution...")
    return result  ## Return the original function's return value
  return wrapper

@your_decorator
def add(a, b):
  """This function adds two numbers."""
  return a + b

sum_result = add(5, 3)
print(f"Function name: {add.__name__}")
print(f"Docstring: {add.__doc__}")
print(f"Result: {sum_result}")

Melhorias chave neste template:

  • import functools: Importa o módulo necessário.
  • @functools.wraps(func): Este é um decorador aplicado à função wrapper. Ele copia os metadados da func original (como __name__ e __doc__) para a função wrapper.
  • result = func(*args, **kwargs): O valor de retorno da função original é capturado na variável result.
  • return result: A função wrapper retorna o resultado capturado, para que a função decorada se comporte como esperado.

Execute o script:

python3 step3.py

A saída demonstra que o nome e a docstring da função são preservados, e o valor de retorno é tratado corretamente.

Before function execution...
After function execution...
Function name: add
Docstring: This function adds two numbers.
Result: 8

Decorador com Parâmetros

Nesta etapa, você aprenderá como criar decoradores que podem ser personalizados com seus próprios parâmetros. Isso permite um uso de decorador mais flexível e configurável, como @your_decorator(arg='value').

Para conseguir isso, você precisa criar uma "fábrica de decoradores" (decorator factory)—uma função que aceita argumentos e retorna um decorador. Isso adiciona uma camada extra de aninhamento à nossa estrutura.

Vamos criar um decorador que aceita uma string prefix e a imprime antes e depois da chamada da função. Abra step4.py e adicione o seguinte código:

import functools

## Decorator factory: returns a decorator based on parameters
def prefix_decorator(prefix):
  def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
      ## Use the 'prefix' argument from the factory
      print(f"{prefix} Before function call")
      result = func(*args, **kwargs)
      print(f"{prefix} After function call")
      return result
    return wrapper
  return decorator  ## Return the actual decorator function

## Using the decorator with a parameter
@prefix_decorator(prefix="LOG:")
def say_hello(name):
  print(f"Hello, {name}!")

say_hello("Alice")

Veja como funciona:

  1. prefix_decorator(prefix="LOG:") é chamado primeiro. Esta é a função de fábrica.
  2. Ela retorna a função decorator, que agora tem acesso à variável prefix ("LOG:") através de um closure.
  3. Este decorator retornado é então aplicado a say_hello, decorando-a.

Execute o script para ver como o parâmetro é usado.

python3 step4.py

A saída mostra o prefixo "LOG:", que foi passado como um argumento para o decorador.

LOG: Before function call
Hello, Alice!
LOG: After function call

Decoradores Baseados em Classes

Nesta etapa, você explorará como implementar decoradores usando classes. Essa abordagem é particularmente útil para manter o estado (state) entre as chamadas de função. Abordaremos dois padrões: decorar um método dentro de uma classe e definir um decorador como uma classe em si.

Abra o arquivo step5.py para começar.

Padrão 1: Decorando um Método de Classe

Você pode definir um decorador como um método dentro de uma classe e aplicá-lo a outros métodos dessa mesma classe. Isso é útil para criar comportamentos específicos atrelados ao contexto da classe.

Adicione o seguinte código ao step5.py. Este exemplo define um decorador para lidar com exceções para um método de classe.

## Pattern 1: Decorating a method within a class
class DecorateMyMethod:

  ## Static method decorator for methods with only 'self' parameter
  def decorator_for_class_method_with_no_args(method):
    def wrapper_for_class_method(self):  ## Only takes self
      try:
        return method(self)  ## Call original method
      except Exception as e:
        print(f"\nWARNING: An exception occurred: {e}\n")
    return wrapper_for_class_method

  def __init__(self,succeed:bool):
    self.succeed = succeed

  @decorator_for_class_method_with_no_args
  def class_action(self):
    if self.succeed:
      print("You succeeded by choice.")
    else:
      raise Exception("Epic fail of your own creation.")

print("--- Testing Pattern 1 ---")
test_succeed = DecorateMyMethod(True)
test_succeed.class_action()

test_fail = DecorateMyMethod(False)
test_fail.class_action()
print("-" * 25)

Padrão 2: Decorador como uma Classe

Um padrão mais comum e poderoso é definir o próprio decorador como uma classe. Isso é ideal para quando o decorador precisa manter um estado. Ao implementar os métodos __init__() e __call__(), você pode fazer com que uma instância de uma classe se comporte como uma função.

Vamos criar um decorador que conta quantas vezes uma função foi chamada. Adicione o seguinte código ao final de step5.py.

## Pattern 2: Class-based decorator for maintaining state
class CountCallNumber:

  def __init__(self, func):
    self.func = func  ## Store the function to decorate
    self.call_number = 0  ## Initialize call counter

  def __call__(self, *args, **kwargs):  ## Makes instance callable
    self.call_number += 1  ## Increment counter
    print("This is execution number " + str(self.call_number))
    return self.func(*args, **kwargs)  ## Call original function

@CountCallNumber
def say_hi(name):
  print("Hi! My name is " + name)

print("\n--- Testing Pattern 2 ---")
say_hi("Jack")
say_hi("James")
  • __init__(self, func): Este é o construtor. Ele é chamado quando o decorador é aplicado (@CountCallNumber). Ele recebe a função a ser decorada (say_hi) e a armazena.
  • __call__(self, *args, **kwargs): Este método torna a instância da classe invocável (callable). Quando você chama a função decorada (say_hi("Jack")), você está, na verdade, invocando este método __call__. Ele incrementa o contador e, em seguida, chama a função original armazenada.

Agora, execute o script inteiro.

python3 step5.py

Você verá a saída para ambos os padrões. Observe como o contador de chamadas é incrementado na segunda parte.

--- Testing Pattern 1 ---
You succeeded by choice.

WARNING: An exception occurred: Epic fail of your own creation.

-------------------------

--- Testing Pattern 2 ---
This is execution number 1
Hi! My name is Jack
This is execution number 2
Hi! My name is James

Resumo

Parabéns por completar o laboratório! Você adquiriu uma compreensão sólida sobre decoradores em Python e suas várias formas.

Neste laboratório, você aprendeu a:

  • Criar um decorador básico e entender a sintaxe @.
  • Projetar decoradores que funcionam com funções que aceitam qualquer número de argumentos usando *args e **kwargs.
  • Usar o template de melhores práticas com functools.wraps para preservar os metadados da função e lidar com valores de retorno.
  • Construir fábricas de decoradores (decorator factories) que permitem personalizar seus decoradores com parâmetros.
  • Implementar decoradores como classes para manter o estado (state) entre as chamadas de função, como em um contador de chamadas.

Decoradores são um recurso fundamental para escrever código Python limpo, reutilizável e extensível. Você pode agora aplicar esses padrões para aprimorar seus próprios projetos.