Atributos Privados e Propriedades

Intermediate

This tutorial is from open-source community. Access the source code

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.

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 81%. Recebeu uma taxa de avaliações positivas de 96% dos estudantes.

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:

  1. Abra o arquivo stock.py no editor.

  2. Modifique a variável de classe types adicionando um sublinhado inicial, alterando-a para _types.

    class Stock:
        ## Class variable for type conversions
        _types = (str, int, float)
    
        ## Rest of the class...
  3. Atualize o método from_row para 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)
  4. Salve o arquivo stock.py.

  5. Crie um script Python chamado test_stock.py para testar suas alterações. Você pode criar o arquivo no editor usando o seguinte comando:

    touch /home/labex/project/test_stock.py
  6. Adicione o seguinte código ao arquivo test_stock.py. Este código cria instâncias da classe Stock e 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()}")
  7. Execute o script de teste usando o seguinte comando no terminal:

    python /home/labex/project/test_stock.py

    Você 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:

  1. Abra o arquivo stock.py no editor.

  2. Substitua o método cost() por uma propriedade usando o decorador @property:

    @property
    def cost(self):
        return self.shares * self.price
  3. Salve o arquivo stock.py.

  4. Crie um novo arquivo chamado test_property.py no editor:

    touch /home/labex/project/test_property.py
  5. Adicione o seguinte código ao arquivo test_property.py para criar uma instância de Stock e acessar a propriedade cost:

    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 property
  6. Execute o script de teste:

    python /home/labex/project/test_property.py

    Você 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:

  1. Abra o arquivo stock.py no editor.

  2. Adicione os atributos privados _shares e _price à classe Stock e 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 attribute
  3. Defina propriedades para shares e price com 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 = value
  4. Atualize 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 setter
  5. Salve o arquivo stock.py.

  6. Crie um script de teste chamado test_validation.py:

    touch /home/labex/project/test_validation.py
  7. Adicione 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}")
  8. Execute o script de teste:

    python /home/labex/project/test_validation.py

    Você 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:

  1. Restringir a criação de atributos apenas aos atributos que definimos.
  2. Melhorar a eficiência da memória, especialmente ao criar muitas instâncias.

Instruções:

  1. Abra o arquivo stock.py no editor.

  2. 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...
  3. Salve o arquivo.

  4. Crie um script de teste chamado test_slots.py:

    touch /home/labex/project/test_slots.py
  5. Adicione 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}")
  6. Execute o script de teste:

    python /home/labex/project/test_slots.py

    Você 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:

  1. Abra o arquivo stock.py no editor.

  2. 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 = value
  3. Salve o arquivo stock.py.

  4. Crie um script de teste chamado test_subclass.py:

    touch /home/labex/project/test_subclass.py
  5. Adicione 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}")
  6. Execute o script de teste:

    python /home/labex/project/test_subclass.py

    Você deve ver que a classe base Stock aceita valores float para o preço, enquanto a subclasse DStock requer valores Decimal.

    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.