Classes e Encapsulamento

Beginner

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

Introdução

Ao escrever classes, é comum tentar encapsular detalhes internos. Esta seção introduz alguns idioms de programação Python para isso, incluindo variáveis privadas e propriedades.

Público vs Privado

Um dos papéis primários de uma classe é encapsular dados e detalhes internos de implementação de um objeto. No entanto, uma classe também define uma interface pública que o mundo exterior deve usar para manipular o objeto. Essa distinção entre detalhes de implementação e a interface pública é importante.

Um Problema

Em Python, quase tudo sobre classes e objetos é aberto.

  • Você pode facilmente inspecionar os detalhes internos do objeto.
  • Você pode alterar as coisas à vontade.
  • Não há uma forte noção de controle de acesso (ou seja, membros privados da classe).

Isso é um problema quando você está tentando isolar detalhes da implementação interna.

Encapsulamento em Python

Python se baseia em convenções de programação para indicar o uso pretendido de algo. Essas convenções são baseadas em nomenclatura. Existe uma atitude geral de que cabe ao programador observar as regras, em vez de a linguagem impô-las.

Atributos Privados

Qualquer nome de atributo com um _ inicial é considerado privado.

class Person(object):
    def __init__(self, name):
        self._name = 0

Como mencionado anteriormente, isso é apenas um estilo de programação. Você ainda pode acessá-lo e alterá-lo.

>>> p = Person('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>

Como regra geral, qualquer nome com um _ inicial é considerado uma implementação interna, seja uma variável, uma função ou um nome de módulo. Se você se encontrar usando esses nomes diretamente, provavelmente está fazendo algo errado. Procure por funcionalidades de nível superior.

Atributos Simples

Considere a seguinte classe.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Uma característica surpreendente é que você pode definir os atributos para qualquer valor:

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares = 100
>>> s.shares = "hundred"
>>> s.shares = [1, 0, 0]
>>>

Você pode olhar para isso e pensar que precisa de algumas verificações extras.

s.shares = '50'     ## Raise a TypeError, this is a string

Como você faria isso?

Atributos Gerenciados

Uma abordagem: introduzir métodos acessores (accessor methods).

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.set_shares(shares)
        self.price = price

    ## Function that layers the "get" operation
    def get_shares(self):
        return self._shares

    ## Function that layers the "set" operation
    def set_shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected an int')
        self._shares = value

É uma pena que isso quebre todo o nosso código existente. s.shares = 50 torna-se s.set_shares(50)

Propriedades

Existe uma abordagem alternativa ao padrão anterior.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

O acesso normal ao atributo agora aciona os métodos getter e setter sob @property e @shares.setter.

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares         ## Triggers @property
50
>>> s.shares = 75    ## Triggers @shares.setter
>>>

Com este padrão, não há alterações necessárias no código-fonte. O novo setter também é chamado quando há uma atribuição dentro da classe, inclusive dentro do método __init__().

class Stock:
    def __init__(self, name, shares, price):
        ...
        ## This assignment calls the setter below
        self.shares = shares
        ...

    ...
    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

Frequentemente, há uma confusão entre uma propriedade e o uso de nomes privados. Embora uma propriedade use internamente um nome privado como _shares, o restante da classe (não a propriedade) pode continuar a usar um nome como shares.

As propriedades também são úteis para atributos de dados computados.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price
    ...

Isso permite que você retire os parênteses extras, escondendo o fato de que é realmente um método:

>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares ## Instance variable
100
>>> s.cost   ## Computed Value
49010.0
>>>

Acesso Uniforme

O último exemplo mostra como colocar uma interface mais uniforme em um objeto. Se você não fizer isso, um objeto pode ser confuso de usar:

>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() ## Method
49010.0
>>> b = s.shares ## Data attribute
100
>>>

Por que os () são necessários para o custo, mas não para as ações (shares)? Uma propriedade pode corrigir isso.

Sintaxe de Decorador

A sintaxe @ é conhecida como "decoração" (decoration). Ela especifica um modificador que é aplicado à definição da função que segue imediatamente.

...
@property
def cost(self):
    return self.shares * self.price

Mais detalhes são fornecidos na Seção 7.

Atributo __slots__

Você pode restringir o conjunto de nomes de atributos.

class Stock:
    __slots__ = ('name','_shares','price')
    def __init__(self, name, shares, price):
        self.name = name
        ...

Isso levantará um erro para outros atributos.

>>> s.price = 385.15
>>> s.prices = 410.2
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'Stock' object has no attribute 'prices'

Embora isso evite erros e restrinja o uso de objetos, ele é realmente usado para desempenho e faz com que o Python use a memória de forma mais eficiente.

Comentários Finais sobre Encapsulamento

Não exagere com atributos privados, propriedades, slots, etc. Eles servem a um propósito específico e você pode vê-los ao ler outro código Python. No entanto, eles não são necessários para a maioria da codificação do dia a dia.

Exercício 5.6: Propriedades Simples

Propriedades são uma maneira útil de adicionar "atributos computados" a um objeto. Em stock.py, você criou um objeto Stock. Observe que em seu objeto há uma ligeira inconsistência na forma como diferentes tipos de dados são extraídos:

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares
100
>>> s.price
490.1
>>> s.cost()
49010.0
>>>

Especificamente, observe como você precisa adicionar os parênteses extras () a cost porque é um método.

Você pode se livrar dos parênteses extras () em cost() se o transformar em uma propriedade. Pegue sua classe Stock e modifique-a para que o cálculo do custo funcione assim:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>

Tente chamar s.cost() como uma função e observe que ela não funciona agora que cost foi definida como uma propriedade.

>>> s.cost()
... fails ...
>>>

Fazer essa alteração provavelmente quebrará seu programa pcost.py anterior. Você pode precisar voltar e se livrar dos () no método cost().

Exercício 5.7: Propriedades e Setters

Modifique o atributo shares para que o valor seja armazenado em um atributo privado e que um par de funções de propriedade seja usado para garantir que ele seja sempre definido como um valor inteiro. Aqui está um exemplo do comportamento esperado:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG',100,490.10)
>>> s.shares = 50
>>> s.shares = 'a lot'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: expected an integer
>>>

Exercício 5.8: Adicionando slots

Modifique a classe Stock para que ela tenha um atributo __slots__. Em seguida, verifique se novos atributos não podem ser adicionados:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... see what happens ...
>>>

Quando você usa __slots__, Python usa uma representação interna mais eficiente de objetos. O que acontece se você tentar inspecionar o dicionário subjacente de s acima?

>>> s.__dict__
... see what happens ...
>>>

Deve-se notar que __slots__ é mais comumente usado como uma otimização em classes que servem como estruturas de dados. Usar slots fará com que esses programas usem muito menos memória e executem um pouco mais rápido. No entanto, você provavelmente deve evitar __slots__ na maioria das outras classes.

Resumo

Parabéns! Você concluiu o laboratório de Classes e Encapsulamento. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.