Introdução
Neste laboratório, você aprenderá como encapsular os detalhes internos de objetos usando atributos privados e implementar decoradores de propriedade para controlar o acesso a atributos. Essas técnicas são essenciais para manter a integridade de seus objetos e garantir o tratamento adequado dos dados.
Você também entenderá como restringir a criação de atributos usando __slots__. Modificaremos o arquivo stock.py ao longo deste laboratório para aplicar esses conceitos.
Implementando Atributos Privados
Em Python, usamos uma convenção de nomenclatura para indicar que um atributo se destina ao uso interno dentro de uma classe. Prefixamos esses atributos com um sublinhado (_). Isso sinaliza a outros desenvolvedores que esses atributos não fazem parte da API pública e não devem ser acessados diretamente de fora da classe.
Vamos analisar a classe Stock atual no arquivo stock.py. Ela possui uma variável de classe chamada types.
class Stock:
## Class variable for type conversions
types = (str, int, float)
## Rest of the class...
A variável de classe types é usada internamente para converter dados de linha. Para indicar que este é um detalhe de implementação, vamos marcá-la como privada.
Instruções:
Abra o arquivo
stock.pyno editor.Modifique a variável de classe
typesadicionando um sublinhado inicial, alterando-a para_types.class Stock: ## Class variable for type conversions _types = (str, int, float) ## Rest of the class...Atualize o método
from_rowpara usar a variável renomeada_types.@classmethod def from_row(cls, row): values = [func(val) for func, val in zip(cls._types, row)] return cls(*values)Salve o arquivo
stock.py.Crie um script Python chamado
test_stock.pypara testar suas alterações. Você pode criar o arquivo no editor usando o seguinte comando:touch /home/labex/project/test_stock.pyAdicione o seguinte código ao arquivo
test_stock.py. Este código cria instâncias da classeStocke imprime informações sobre elas.from stock import Stock ## Create a stock instance s = Stock('GOOG', 100, 490.10) print(f"Name: {s.name}, Shares: {s.shares}, Price: {s.price}") print(f"Cost: {s.cost()}") ## Create from row row = ['AAPL', '50', '142.5'] apple = Stock.from_row(row) print(f"Name: {apple.name}, Shares: {apple.shares}, Price: {apple.price}") print(f"Cost: {apple.cost()}")Execute o script de teste usando o seguinte comando no terminal:
python /home/labex/project/test_stock.pyVocê deve ver uma saída semelhante a:
Name: GOOG, Shares: 100, Price: 490.1 Cost: 49010.0 Name: AAPL, Shares: 50, Price: 142.5 Cost: 7125.0
Convertendo Métodos em Propriedades
Propriedades em Python permitem que você acesse valores computados como atributos. Isso elimina a necessidade de parênteses ao chamar um método, tornando seu código mais limpo e consistente.
Atualmente, nossa classe Stock possui um método cost() que calcula o custo total das ações.
def cost(self):
return self.shares * self.price
Para obter o valor do custo, precisamos chamá-lo com parênteses:
s = Stock('GOOG', 100, 490.10)
print(s.cost()) ## Calls the method
Podemos melhorar isso convertendo o método cost() em uma propriedade, permitindo que acessemos o valor do custo sem parênteses:
s = Stock('GOOG', 100, 490.10)
print(s.cost) ## Accesses the property
Instruções:
Abra o arquivo
stock.pyno editor.Substitua o método
cost()por uma propriedade usando o decorador@property:@property def cost(self): return self.shares * self.priceSalve o arquivo
stock.py.Crie um novo arquivo chamado
test_property.pyno editor:touch /home/labex/project/test_property.pyAdicione o seguinte código ao arquivo
test_property.pypara criar uma instância deStocke acessar a propriedadecost:from stock import Stock ## Create a stock instance s = Stock('GOOG', 100, 490.10) ## Access cost as a property (no parentheses) print(f"Stock: {s.name}") print(f"Shares: {s.shares}") print(f"Price: {s.price}") print(f"Cost: {s.cost}") ## Using the propertyExecute o script de teste:
python /home/labex/project/test_property.pyVocê deve ver uma saída semelhante a:
Stock: GOOG Shares: 100 Price: 490.1 Cost: 49010.0
Implementando Validação de Propriedades
Propriedades também permitem que você controle como os valores dos atributos são recuperados, definidos e excluídos. Isso é útil para adicionar validação aos seus atributos, garantindo que os valores atendam a critérios específicos.
Em nossa classe Stock, queremos garantir que shares seja um inteiro não negativo e price seja um float não negativo. Usaremos decoradores de propriedade junto com getters e setters para conseguir isso.
Instruções:
Abra o arquivo
stock.pyno editor.Adicione os atributos privados
_sharese_priceà classeStocke modifique o construtor para usá-los:def __init__(self, name, shares, price): self.name = name self._shares = shares ## Using private attribute self._price = price ## Using private attributeDefina propriedades para
sharesepricecom validação:@property def shares(self): return self._shares @shares.setter def shares(self, value): if not isinstance(value, int): raise TypeError("Expected integer") if value < 0: raise ValueError("shares must be >= 0") self._shares = value @property def price(self): return self._price @price.setter def price(self, value): if not isinstance(value, float): raise TypeError("Expected float") if value < 0: raise ValueError("price must be >= 0") self._price = valueAtualize o construtor para usar os setters de propriedade para validação:
def __init__(self, name, shares, price): self.name = name self.shares = shares ## Using property setter self.price = price ## Using property setterSalve o arquivo
stock.py.Crie um script de teste chamado
test_validation.py:touch /home/labex/project/test_validation.pyAdicione o seguinte código ao arquivo
test_validation.py:from stock import Stock ## Create a valid stock instance s = Stock('GOOG', 100, 490.10) print(f"Initial: Name={s.name}, Shares={s.shares}, Price={s.price}, Cost={s.cost}") ## Test valid updates try: s.shares = 50 ## Valid update print(f"After setting shares=50: Shares={s.shares}, Cost={s.cost}") except Exception as e: print(f"Error setting shares=50: {e}") try: s.price = 123.45 ## Valid update print(f"After setting price=123.45: Price={s.price}, Cost={s.cost}") except Exception as e: print(f"Error setting price=123.45: {e}") ## Test invalid updates try: s.shares = "50" ## Invalid type (string) print("This line should not execute") except Exception as e: print(f"Error setting shares='50': {e}") try: s.shares = -10 ## Invalid value (negative) print("This line should not execute") except Exception as e: print(f"Error setting shares=-10: {e}") try: s.price = "123.45" ## Invalid type (string) print("This line should not execute") except Exception as e: print(f"Error setting price='123.45': {e}") try: s.price = -10.0 ## Invalid value (negative) print("This line should not execute") except Exception as e: print(f"Error setting price=-10.0: {e}")Execute o script de teste:
python /home/labex/project/test_validation.pyVocê deve ver a saída mostrando atualizações válidas bem-sucedidas e mensagens de erro apropriadas para atualizações inválidas.
Initial: Name=GOOG, Shares=100, Price=490.1, Cost=49010.0 After setting shares=50: Shares=50, Cost=24505.0 After setting price=123.45: Price=123.45, Cost=6172.5 Error setting shares='50': Expected integer Error setting shares=-10: shares must be >= 0 Error setting price='123.45': Expected float Error setting price=-10.0: price must be >= 0
Usando __slots__ para Otimização de Memória
O atributo __slots__ restringe os atributos que uma classe pode ter. Ele impede a adição de novos atributos às instâncias e reduz o uso de memória.
Em nossa classe Stock, usaremos __slots__ para:
- Restringir a criação de atributos apenas aos atributos que definimos.
- Melhorar a eficiência da memória, especialmente ao criar muitas instâncias.
Instruções:
Abra o arquivo
stock.pyno editor.Adicione uma variável de classe
__slots__, listando todos os nomes de atributos privados usados pela classe:class Stock: ## Class variable for type conversions _types = (str, int, float) ## Define slots to restrict attribute creation __slots__ = ('name', '_shares', '_price') ## Rest of the class...Salve o arquivo.
Crie um script de teste chamado
test_slots.py:touch /home/labex/project/test_slots.pyAdicione o seguinte código ao arquivo
test_slots.py:from stock import Stock ## Create a stock instance s = Stock('GOOG', 100, 490.10) ## Access existing attributes print(f"Name: {s.name}") print(f"Shares: {s.shares}") print(f"Price: {s.price}") print(f"Cost: {s.cost}") ## Try to add a new attribute try: s.extra = "This will fail" print(f"Extra: {s.extra}") except AttributeError as e: print(f"Error: {e}")Execute o script de teste:
python /home/labex/project/test_slots.pyVocê deve ver a saída mostrando que você pode acessar os atributos definidos, mas tentar adicionar um novo atributo levanta um
AttributeError.Name: GOOG Shares: 100 Price: 490.1 Cost: 49010.0 Error: 'Stock' object has no attribute 'extra'
Reconciliando Validação de Tipo com Variáveis de Classe
Atualmente, nossa classe Stock usa tanto a variável de classe _types quanto os setters de propriedade para tratamento de tipos. Para melhorar a consistência e a capacidade de manutenção, reconciliaremos esses mecanismos para que usem as mesmas informações de tipo.
Instruções:
Abra o arquivo
stock.pyno editor.Modifique os setters de propriedade para usar os tipos definidos na variável de classe
_types:@property def shares(self): return self._shares @shares.setter def shares(self, value): if not isinstance(value, self._types[1]): raise TypeError(f"Expected {self._types[1].__name__}") if value < 0: raise ValueError("shares must be >= 0") self._shares = value @property def price(self): return self._price @price.setter def price(self, value): if not isinstance(value, self._types[2]): raise TypeError(f"Expected {self._types[2].__name__}") if value < 0: raise ValueError("price must be >= 0") self._price = valueSalve o arquivo
stock.py.Crie um script de teste chamado
test_subclass.py:touch /home/labex/project/test_subclass.pyAdicione o seguinte código ao arquivo
test_subclass.py:from stock import Stock from decimal import Decimal ## Create a subclass with different types class DStock(Stock): _types = (str, int, Decimal) ## Test the base class s = Stock('GOOG', 100, 490.10) print(f"Stock: {s.name}, Shares: {s.shares}, Price: {s.price}, Cost: {s.cost}") ## Test valid update with float try: s.price = 500.25 print(f"Updated Stock price: {s.price}, Cost: {s.cost}") except Exception as e: print(f"Error updating Stock price: {e}") ## Test the subclass with Decimal ds = DStock('AAPL', 50, Decimal('142.50')) print(f"DStock: {ds.name}, Shares: {ds.shares}, Price: {ds.price}, Cost: {ds.cost}") ## Test invalid update with float (should require Decimal) try: ds.price = 150.75 print(f"Updated DStock price: {ds.price}") except Exception as e: print(f"Error updating DStock price: {e}") ## Test valid update with Decimal try: ds.price = Decimal('155.25') print(f"Updated DStock price: {ds.price}, Cost: {ds.cost}") except Exception as e: print(f"Error updating DStock price: {e}")Execute o script de teste:
python /home/labex/project/test_subclass.pyVocê deve ver que a classe base
Stockaceita valores float para o preço, enquanto a subclasseDStockrequer valoresDecimal.Stock: GOOG, Shares: 100, Price: 490.1, Cost: 49010.0 Updated Stock price: 500.25, Cost: 50025.0 DStock: AAPL, Shares: 50, Price: 142.50, Cost: 7125.00 Error updating DStock price: Expected Decimal Updated DStock price: 155.25, Cost: 7762.50
Resumo
Neste laboratório, você aprendeu como usar atributos privados, converter métodos em propriedades, implementar validação de propriedade, usar __slots__ para otimização de memória e reconciliar a validação de tipo com variáveis de classe. Essas técnicas aprimoram a robustez, a eficiência e a capacidade de manutenção de suas classes, aplicando a encapsulação e fornecendo interfaces claras.