Compreender Decorators em Python

PythonBeginner
Pratique Agora

Introdução

Neste laboratório, você obterá uma compreensão abrangente dos decorators em Python, um recurso poderoso para modificar ou aprimorar funções e métodos. Começaremos introduzindo o conceito fundamental de decorators e explorando seu uso básico através de exemplos práticos.

Com base nesta fundação, você aprenderá a usar functools.wraps de forma eficaz para preservar metadados importantes da função decorada. Em seguida, aprofundaremos em decorators específicos, como o decorator property, entendendo seu papel no gerenciamento do acesso a atributos. Por fim, o laboratório esclarecerá as distinções entre métodos de instância (instance methods), métodos de classe (class methods) e métodos estáticos (static methods), demonstrando como os decorators são usados nesses contextos para controlar o comportamento do método dentro das classes.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível iniciante com uma taxa de conclusão de 93%. Recebeu uma taxa de avaliações positivas de 100% dos estudantes.

Compreendendo Decorators Básicos

Nesta etapa, introduziremos o conceito de decorators e seu uso básico. Um decorator é uma função que aceita outra função como argumento, adiciona alguma funcionalidade e retorna outra função, tudo isso sem alterar o código-fonte da função original.

Primeiro, localize o arquivo decorator_basics.py no explorador de arquivos no lado esquerdo do WebIDE. Dê um duplo clique para abri-lo. Escreveremos nosso primeiro decorator neste arquivo.

Copie e cole o seguinte código em decorator_basics.py:

import datetime

def log_activity(func):
    """A simple decorator to log function calls."""
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_activity
def greet(name):
    """A simple function to greet someone."""
    print(f"Hello, {name}!")

## Call the decorated function
greet("Alice")

## Let's inspect the function's metadata
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")

Vamos analisar este código:

  • Definimos uma função decorator log_activity que aceita uma função func como seu argumento.
  • Dentro de log_activity, definimos uma função aninhada wrapper. Esta função conterá o novo comportamento. Ela imprime uma mensagem de log, chama a função original func e, em seguida, imprime outra mensagem de log.
  • A função log_activity retorna a função wrapper.
  • A sintaxe @log_activity acima da função greet é um atalho para greet = log_activity(greet). Ela aplica nosso decorator à função greet.

Agora, salve o arquivo (você pode usar Ctrl+S ou Cmd+S). Para executar o script, abra o terminal integrado na parte inferior do WebIDE e execute o seguinte comando:

python ~/project/decorator_basics.py

Você verá a seguinte saída. Observe que a data e hora variarão.

Calling function 'greet' at 2023-10-27 10:30:00.123456
Hello, Alice!
Function 'greet' finished.

Function name: wrapper
Function docstring: None

Note duas coisas na saída. Primeiro, nossa função greet agora está envolvida com as mensagens de log. Segundo, o nome e a docstring da função foram substituídos pelos da função wrapper. Isso pode ser problemático para depuração (debugging) e introspecção. Na próxima etapa, aprenderemos como corrigir isso.

Preservando Metadados da Função com functools.wraps

Na etapa anterior, observamos que decorar uma função substitui seus metadados originais (como __name__ e __doc__) pelos metadados da função wrapper. O módulo functools do Python fornece uma solução para isso: o decorator wraps.

O decorator wraps é usado dentro do seu próprio decorator para copiar os metadados da função original para a função wrapper.

Vamos modificar nosso código em decorator_basics.py. Abra o arquivo no WebIDE e atualize-o para usar functools.wraps.

import datetime
from functools import wraps

def log_activity(func):
    """A simple decorator to log function calls."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_activity
def greet(name):
    """A simple function to greet someone."""
    print(f"Hello, {name}!")

## Call the decorated function
greet("Alice")

## Let's inspect the function's metadata again
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")

As únicas alterações são:

  1. Importamos wraps do módulo functools.
  2. Adicionamos @wraps(func) logo acima da definição da nossa função wrapper.

Salve o arquivo e execute-o novamente no terminal:

python ~/project/decorator_basics.py

Agora, a saída será diferente:

Calling function 'greet' at 2023-10-27 10:35:00.543210
Hello, Alice!
Function 'greet' finished.

Function name: greet
Function docstring: A simple function to greet someone.

Como você pode ver, o nome da função é relatado corretamente como greet, e sua docstring original é preservada. Usar functools.wraps é uma boa prática que torna seus decorators mais robustos e profissionais.

Implementando Atributos Gerenciados com @property

Python fornece vários decorators nativos (built-in). Um dos mais úteis é o @property, que permite transformar um método de classe em um "atributo gerenciado" (managed attribute). Isso é ideal para adicionar lógica, como validação ou computação, ao acesso de atributos sem alterar a forma como os usuários interagem com sua classe.

Vamos explorar isso criando uma classe Circle. Abra o arquivo property_decorator.py no explorador de arquivos.

Copie e cole o seguinte código em property_decorator.py:

import math

class Circle:
    def __init__(self, radius):
        ## The actual value is stored in a "private" attribute
        self._radius = radius

    @property
    def radius(self):
        """The radius property."""
        print("Getting radius...")
        return self._radius

    @radius.setter
    def radius(self, value):
        """The radius setter with validation."""
        print(f"Setting radius to {value}...")
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        """A read-only computed property for the area."""
        print("Calculating area...")
        return math.pi * self._radius ** 2

## --- Let's test our Circle class ---
c = Circle(5)

## Access the radius like a normal attribute (triggers the getter)
print(f"Initial radius: {c.radius}\n")

## Change the radius (triggers the setter)
c.radius = 10
print(f"New radius: {c.radius}\n")

## Access the computed area property
print(f"Circle area: {c.area:.2f}\n")

## Try to set an invalid radius (triggers the setter's validation)
try:
    c.radius = -2
except ValueError as e:
    print(f"Error: {e}")

Neste código:

  • @property no método radius define um "getter". Ele é chamado quando você acessa c.radius.
  • @radius.setter define um "setter" para a propriedade radius. Ele é chamado quando você atribui um valor, como c.radius = 10. Adicionamos validação aqui para prevenir valores negativos.
  • O método area também usa @property, mas não possui um setter, tornando-o um atributo somente leitura (read-only). Seu valor é calculado toda vez que é acessado.

Salve o arquivo e execute-o a partir do terminal:

python ~/project/property_decorator.py

Você deverá ver a seguinte saída, demonstrando como a lógica do getter, setter e validação são invocadas automaticamente:

Getting radius...
Initial radius: 5

Setting radius to 10...
Getting radius...
New radius: 10

Calculating area...
Circle area: 314.16

Setting radius to -2...
Error: Radius cannot be negative

Diferenciando Métodos de Instância, Classe e Estáticos

Nas classes Python, os métodos podem estar vinculados a uma instância, à classe ou não estarem vinculados de forma alguma. Decorators são usados para definir esses diferentes tipos de métodos.

  • Métodos de Instância (Instance Methods): O tipo padrão. Eles recebem a instância como o primeiro argumento, convencionalmente nomeado self. Eles operam em dados específicos da instância.
  • Métodos de Classe (Class Methods): Marcados com @classmethod. Eles recebem a classe como o primeiro argumento, convencionalmente nomeado cls. Eles operam em dados de nível de classe e são frequentemente usados como construtores alternativos.
  • Métodos Estáticos (Static Methods): Marcados com @staticmethod. Eles não recebem nenhum primeiro argumento especial. São essencialmente funções regulares nomeadas dentro de uma classe e não podem acessar o estado da instância ou da classe.

Vamos ver os três em ação. Abra o arquivo class_methods.py no explorador de arquivos.

Copie e cole o seguinte código em class_methods.py:

class MyClass:
    class_variable = "I am a class variable"

    def __init__(self, instance_variable):
        self.instance_variable = instance_variable

    ## 1. Instance Method
    def instance_method(self):
        print("\n--- Calling Instance Method ---")
        print(f"Can access instance data: self.instance_variable = '{self.instance_variable}'")
        print(f"Can access class data: self.class_variable = '{self.class_variable}'")

    ## 2. Class Method
    @classmethod
    def class_method(cls):
        print("\n--- Calling Class Method ---")
        print(f"Can access class data: cls.class_variable = '{cls.class_variable}'")
        ## Note: Cannot access instance_variable without an instance
        print("Cannot access instance data directly.")

    ## 3. Static Method
    @staticmethod
    def static_method(a, b):
        print("\n--- Calling Static Method ---")
        print("Cannot access instance or class data directly.")
        print(f"Just a utility function: {a} + {b} = {a + b}")

## --- Let's test the methods ---
## Create an instance of the class
my_instance = MyClass("I am an instance variable")

## Call the instance method (requires an instance)
my_instance.instance_method()

## Call the class method (can be called on the class or an instance)
MyClass.class_method()
my_instance.class_method() ## Also works

## Call the static method (can be called on the class or an instance)
MyClass.static_method(10, 5)
my_instance.static_method(20, 8) ## Also works

Salve o arquivo e execute-o a partir do terminal:

python ~/project/class_methods.py

Examine a saída com atenção. Ela demonstra claramente as capacidades e limitações de cada tipo de método.

--- Calling Instance Method ---
Can access instance data: self.instance_variable = 'I am an instance variable'
Can access class data: self.class_variable = 'I am a class variable'

--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.

--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.

--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 10 + 5 = 15

--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 20 + 8 = 28

Este exemplo fornece uma referência clara sobre quando usar cada tipo de método, com base na necessidade de acesso ao estado da instância, ao estado da classe ou a nenhum deles.

Resumo

Neste laboratório, você adquiriu uma compreensão prática de decorators em Python. Você começou aprendendo como criar e aplicar um decorator básico para adicionar funcionalidade a uma função. Em seguida, você viu a importância de usar functools.wraps para preservar os metadados da função original, uma prática recomendada crucial para escrever decorators limpos e de fácil manutenção.

Além disso, você explorou decorators nativos (built-in) poderosos. Você aprendeu a usar o decorator @property para criar atributos gerenciados com lógica personalizada de getter e setter, possibilitando recursos como validação de entrada. Por fim, você diferenciou entre métodos de instância, métodos de classe (@classmethod) e métodos estáticos (@staticmethod), entendendo como cada um serve a um propósito diferente dentro da estrutura de uma classe com base em seu acesso ao estado da instância e da classe.