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.
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_activityque aceita uma funçãofunccomo seu argumento. - Dentro de
log_activity, definimos uma função aninhadawrapper. Esta função conterá o novo comportamento. Ela imprime uma mensagem de log, chama a função originalfunce, em seguida, imprime outra mensagem de log. - A função
log_activityretorna a funçãowrapper. - A sintaxe
@log_activityacima da funçãogreeté um atalho paragreet = log_activity(greet). Ela aplica nosso decorator à funçãogreet.
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:
- Importamos
wrapsdo módulofunctools. - Adicionamos
@wraps(func)logo acima da definição da nossa funçãowrapper.
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:
@propertyno métodoradiusdefine um "getter". Ele é chamado quando você acessac.radius.@radius.setterdefine um "setter" para a propriedaderadius. Ele é chamado quando você atribui um valor, comoc.radius = 10. Adicionamos validação aqui para prevenir valores negativos.- O método
areatambé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 nomeadocls. 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.



