Encapsulamento#

O que é encapsulamento?#

Encapsulamento é um dos quatro princípios fundamentais da programação orientada a objetos (POO). Até agora vimos apenas herança, vamos ver neste capítulo sobre encapsulamento, e os outros dois, abstração e polimorfismo veremos em capítulos seguintes.

Em essência, encapsulamento é o conceito de agrupar atributos e os métodos que operam nesses dados dentro de objeto. Além disso, o encapsulamento envolve a restrição de acesso direto a alguns dos componentes do objeto, essencialmente «escondendo» os detalhes internos da implementação.

Exemplo conceitual#

O melhor exemplo que consigo trazer é de uma máquina de café. Imagine que você tem uma máquina de café automática, como aquelas que encontramos em escritórios. Você sabe que, ao apertar o botão, a máquina faz café para você. No entanto, você não precisa (e nem deveria) saber todos os detalhes internos sobre como ela faz o café, como os grãos são moídos, como a água é aquecida ou como a pressão é ajustada. Tudo o que você sabe é que, ao apertar o botão, você recebe um café pronto, sem se preocupar com o que acontece dentro da máquina. Aqui vamos literalmente separar o que é público e o que é privado ao criar a nossa classe.

Agora, trazendo isso para o mundo da programação:

  • A máquina de café é a nossa classe.

  • O botão para fazer café é um método público, ou seja, algo que você pode acessar diretamente e usar.

  • O que acontece dentro da máquina — moagem de grãos, controle da temperatura, pressão da água — são as variáveis e métodos privados, protegidos de você (o usuário) porque não é necessário que você mexa diretamente neles. Eles estão encapsulados, «escondidos» dentro da máquina para evitar que você cause algum problema ou quebrem o funcionamento correto.

Vamos construir esse exemplo passo a passo.

Primeiro, vamos criar a nossa classe.

Atributos «privados»#

class MaquinaCafe:
    def __init__(self):
        self._agua = 1000  # Quantidade de água (privado)
        self._graos = 500  # Quantidade de grãos (privado)

Aqui já temos a primeira diferença. Notem que os atributos da máquina de café tem um _ antes do nome do atributo, como acontece em self._agua e self._graos. Eles foram declarados como privados, ou seja, só devem ser acessados dentro da própria classe. Isso é uma «convençao de cavalheiros» em Python e significa que você não deveria acessar ou modificar tais atributos fora da classe.

Mas porque eu disse «convençao de cavalheiros»? Na realidade, em Python, não existe atributos privados. Você pode acessar e modificar qualquer atributo de qualquer objeto em Python. Mas, por convenção, se um atributo tem um sublinhado _ antes do nome, você não deve acessá-lo diretamente. Se você fizer isso, você está assumindo a responsabilidade de saber o que está fazendo e quebrar o código é por sua conta e risco.

Vou mostrar como acessar e modificar estes atributos ainda é possível, mesmo com sublinhado _ antes do nome.

class MaquinaCafe:
    def __init__(self):
        self._agua = 1000  # Quantidade de água (privado)
        self._graos = 500  # Quantidade de grãos (privado)


maquina_cafe = MaquinaCafe()
# Aqui estamos acessando um atributo que, em teoria,
# deveria ser privado e acessado somente pela própria classe!
print(maquina_cafe._agua)

# E aqui estamos modificando um atributo que, em teoria
# deveria ser privado e modificado somente pela própria classe!
maquina_cafe._agua = 5000

# Mudamos algo interno da classe!
print(maquina_cafe._agua)
1000
5000

Portanto, entenda que o _ no início do nome do atributo é uma convenção, e não uma regra rígida. O Python permite acessar e modificar qualquer atributo de qualquer objeto, mas você deve saber o que está fazendo. Como regra geral, siga a convenção e não acesse ou modifique atributos privados diretamente fora da classe!

Métodos «privados»#

Vamos continuar com a nossa classe. Agora vamos criar os métodos privados, que são os métodos que não devem ser acessados diretamente. Eles são métodos que fazem parte da implementação interna da classe e não devem ser chamados diretamente pelo usuário.

class MaquinaCafe:
    def __init__(self):
        self._agua = 1000  # Quantidade de água (privado)
        self._graos = 500  # Quantidade de grãos (privado)

    def _moer_graos(self):  # Método privado
        print("Moendo os grãos...")
        self._graos -= 50

    def _aquecer_agua(self):  # Método privado
        print("Aquecendo a água...")
        self._agua -= 200

Novamente, métodos privados começam com _ no nome, e vale a mesma ideia de convenção e não regra rígida. Você conseguiria criar uma instância e chamar o método privado diretamente, mas não é recomendado, apesar de a linguagem Python nos permitir. Novamente, siga a convenção e não execute métodos privados diretamente fora da classe!

Métodos públicos#

Vamos agora criar um método público fazer_cafe(). Ele é definido normalmente, sem _ no nome, e são métodos que em teoria deveriam ser os únicos acessados diretamente por quem usa a classe.

class MaquinaCafe:
    def __init__(self):
        self._agua = 1000  # Quantidade de água (privado)
        self._graos = 500  # Quantidade de grãos (privado)

    def _moer_graos(self):  # Método privado
        print("Moendo os grãos...")
        self._graos -= 50

    def _aquecer_agua(self):  # Método privado
        print("Aquecendo a água...")
        self._agua -= 200

    def fazer_cafe(self):  # Método público
        if self._agua > 0 and self._graos > 0:
            self._moer_graos()  # Método privados sendo acessados pela própria classe!
            self._aquecer_agua()  # Método privados sendo acessados pela própria classe!
            print("Café pronto!")
        else:
            print("Sem água ou grãos suficientes.")


# Usuário (externo) da classe
maquina = MaquinaCafe()
maquina.fazer_cafe()  # Acesso ao método público fora da classe
Moendo os grãos...
Aquecendo a água...
Café pronto!

Reparem bem que os métodos privados _moer_graos() e _aquecer_agua() são chamados dentro do método público fazer_cafe(). Aqui é a magia do encapsulamento em ação! O usuário da classe não precisa saber como o café é feito, ele só precisa saber que ao chamar fazer_cafe(), ele vai receber um café pronto.

Basicamente é esta a distinção mais básica do encapsulamento: o que é público e o que é privado. O usuário da classe só precisa saber o que é público, e não precisa saber o que acontece internamente.

Atenção (definição privado vs público)

Dentro do conceito de encapsulamento, quem define quais métodos e atributos serão privados ou públicos é você, caro leitor! Você é quem decide o que é público e o que é privado, e o que o usuário da sua classe vai poder acessar diretamente. A ideia é esconder os detalhes de implementação e expor apenas o que é necessário para quem vai usar a sua classe.

Portanto, é importante que você pense bem sobre o que é público e o que é privado, e que você siga a convenção de Python de usar _ no início do nome para indicar que algo é privado. Mas lembre-se que, em Python, não existe atributos ou métodos privados de verdade, e você pode acessar e modificar qualquer coisa. A convenção é uma forma de dizer «não faça isso» e de indicar que algo é privado e não deve ser acessado diretamente.

Casos de uso#

Vou trazer aqui alguns casos de uso para te ajudar a pensar e refletir sobre essa questão de público e privado.

  • Saldo de conta bancária: O saldo de uma conta bancária deve ser privado, acessível apenas por métodos autorizados, como depósito ou saque. O usuário não pode simplesmente modificar o saldo diretamente. Já pensou você aumentando seu próprio saldo no terminal do banco ou no seu celular? Não seria legal, né?

  • Controle de acessos em um sistema de segurança: Informações como senhas ou níveis de permissão dos usuários são encapsuladas, garantindo que elas não sejam acessadas ou modificadas diretamente, apenas através de métodos controlados.

  • Configurações de uma máquina industrial: Em uma máquina complexa, parâmetros como temperatura, pressão ou velocidade de operação podem ser ajustados apenas por métodos específicos, sem permitir que esses valores sejam alterados diretamente pelos usuários. Ninguém quer explodir um reator porque alguém que não deveria mexeu na temperatura, certo?

  • Sistema de registro médico: Informações sensíveis sobre pacientes, como diagnósticos e históricos médicos, devem ser protegidas. Apenas funções específicas podem acessar ou modificar esses dados, e quem usa o sistema não pode mexer diretamente nos registros. Aqui é para evitar que alguém veja ou modifique informações sensíveis sem autorização.

  • Gerenciamento de inventário de estoque: O número de itens no estoque pode ser encapsulado para evitar que o número de produtos seja alterado diretamente. Apenas funções específicas de entrada e saída de mercadorias podem atualizar o valor real.

  • Configurações de conexão a um banco de dados: As credenciais e parâmetros de conexão a um banco de dados são encapsulados para que apenas os métodos internos da classe façam a conexão, garantindo a segurança dos dados sensíveis.

  • Jogo de vídeo game - pontuação do jogador: A pontuação ou vida de um jogador em um jogo é protegida por encapsulamento, permitindo que apenas ações válidas dentro do jogo modifiquem esses valores, e não diretamente pelo usuário.

Em resumo, muita das vezes quem determina o que é público e o que é privado é a lógica do seu programa ditada pela regra de negócio. Se algo não deve ser acessado diretamente, você deve encapsular e proteger esse dado ou método. Se algo pode ser acessado diretamente, você pode deixar público. A ideia é esconder os detalhes de implementação e expor apenas o que é necessário para quem for usar a classe.

Conceito de propriedade#

Às vezes um atributo privado da classe precisa ser acessado somente como leitura. É o caso de um saldo de conta bancária, por exemplo. Você não quer que o saldo seja modificado diretamente, mas você quer que ele possa ser lido. Temos um novo conceito aqui de propriedade. Vamos entender melhor com um exemplo.

class ContaBancaria:
    def __init__(self):
        self._saldo = 0

    def depositar(self, valor):
        if valor > 0:
            self._saldo += valor
        else:
            print("Valor inválido para depósito.")

    def sacar(self, valor):
        if 0 < valor <= self._saldo:
            self._saldo -= valor
        else:
            print("Valor inválido para saque.")

Até aqui nenhuma novidade. Porém, pela convenção, sabemos que o atributo _saldo é privado e não deve ser acessado diretamente. Mas e se eu quisesse que um usuário da classe tivesse o acesso somente de leitura do saldo? Como eu faço? Acompanhe a seguir.

class ContaBancaria:
    def __init__(self):
        self._saldo = 0

    @property
    def saldo(self):
        return self._saldo

    def depositar(self, valor):
        if valor > 0:
            self._saldo += valor
        else:
            print("Valor inválido para depósito.")

    def sacar(self, valor):
        if 0 < valor <= self._saldo:
            self._saldo -= valor
        else:
            print("Valor inválido para saque.")

Acrescentamos algo novo no nosso código. Temos um @property, e um método saldo() que retorna o atributo _saldo privado. O que está acontecendo aqui? Vamos com calma…

Esse @property é obrigatório para definirmos uma proriedade. Ele é um decorador, que é um conceito um pouco mais avançado em Python, mas não se preocupe com tal conceito por agora. Entenda o que @property faz: ele transforma o método saldo() abaixo em uma propriedade saldo (mesmo nome do método), que tem o valor do retorno do método (_saldo, no caso).

Uma propriedade funciona exatamente como um atributo, mas com a diferença de ser um atributo somente de leitura. Você não pode modificar o valor de uma propriedade diretamente, apenas lê-lo. Isso é muito útil para proteger atributos privados que você quer que sejam acessados somente como leitura.

class ContaBancaria:
    def __init__(self):
        self._saldo = 0

    @property
    def saldo(self):
        return self._saldo

    def depositar(self, valor):
        if valor > 0:
            self._saldo += valor
        else:
            print("Valor inválido para depósito.")

    def sacar(self, valor):
        if 0 < valor <= self._saldo:
            self._saldo -= valor
        else:
            print("Valor inválido para saque.")


conta = ContaBancaria()
conta.depositar(1000)
# Aqui estamos acessando a propriedade saldo,
# que retorna o valor do atributo privado _saldo
print(conta.saldo)

# Mas por ser uma propriedade, não podemos modificar seu valor diretamente
conta.saldo = 5000
1000
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[7], line 29
     26 print(conta.saldo)
     28 # Mas por ser uma propriedade, não podemos modificar seu valor diretamente
---> 29 conta.saldo = 5000

AttributeError: can't set attribute

Percebam como conseguimos com o uso do @property uma forma de garantir acesso de leitura à um determinado atributo privado, sem permitir que ele seja modificado?

Mas é importante ressaltar a convenção de Python: o atributo original _saldo (e não a propriedade saldo) é privado e não deve ser acessado diretamente. O acesso deve ser pela propriedade saldo e é a forma correta de acessar o saldo, garantindo que ele seja somente de leitura.

Atenção (convenção)

Mesmo com uso do @property ainda é possível alterar e modificar o atributo privado _saldo. O @property é uma forma de garantir que o acesso à determinado atributo seja somente de leitura, mas não impede que o atributo privado original seja modificado diretamente. Portanto, siga a convenção de Python e não acesse ou modifique atributos privados diretamente, mesmo que você possa fazer isso!

O mistério com dois sublinhados#

Em uma tentativa de evitar o acesso direto aos atributos e métodos privados, existe ainda uma outra convenção, que é o uso de dois sublinhados __ ao invés de um só _.

Aqui é preciso entender que o uso de dois sublinhados __ antes do nome de um atributo ou método não é apenas uma convenção, mas sim uma modificação que o Python faz por debaixo dos panos. Quando você usa dois sublinhados __ antes do nome de um atributo ou método, o Python modifica o nome do atributo ou método, adicionando o nome da classe na frente. Esta modificação é conhecida como name mangling, que em português significa embaralhamento de nomes.

Vamos usar o mesmo exemplo da máquina de café, mas agora com dois sublinhados __ antes dos atributos e métodos privados.

class MaquinaCafe:
    def __init__(self):
        self.__agua = 1000  # Quantidade de água (privado)
        self.__graos = 500  # Quantidade de grãos (privado)

    def __moer_graos(self):  # Método privado
        print("Moendo os grãos...")
        self.__graos -= 50

    def __aquecer_agua(self):  # Método privado
        print("Aquecendo a água...")
        self.__agua -= 200

    def fazer_cafe(self):  # Método público
        if self.__agua > 0 and self.__graos > 0:
            self.__moer_graos()  # Método privados sendo acessados pela própria classe!
            self.__aquecer_agua()  # Método privados sendo acessados pela própria classe!
            print("Café pronto!")
        else:
            print("Sem água ou grãos suficientes.")


# Usuário (externo) da classe
maquina = MaquinaCafe()
print(maquina.__agua)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 25
     23 # Usuário (externo) da classe
     24 maquina = MaquinaCafe()
---> 25 print(maquina.__agua)

AttributeError: 'MaquinaCafe' object has no attribute '__agua'

A única diferença aqui é que estamos usando dois sublinhados __ ao invés de um sublinhado _ antes dos atributos e métodos privados. A impressão que temos é que o atributo deixou de existir fora da classe, e o «ser privado» realmente funciona.

Quando usamos desta forma, o Python manipula os nomes por debaixo dos panos pra mudar os nomes dos atributos e métodos privados, colocando à frente deles o nome da classe. É só uma forma que o Python usa para tentar evitar que você acesse diretamente os atributos e métodos privados.

Vamos entender como isso funciona no detalhe com um único atributo. Vamos definir a classe com um atributo público primeiro.

class MaquinaCafe:
    def __init__(self):
        self.agua = 1000  # Quantidade de água (privado)


maquina = MaquinaCafe()
print(dir(maquina))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'agua']

No código acima, eu apenas usei a função dir() para imprimir todos os métodos e atributos da classe MaquinaCafe. Ignore, por enquanto, todos os outros itens da lista acima, e foque apenas no atributo agua, o último da lista. Quando criamos um atributo público, ele é adicionado à lista de atributos com o mesmo nome.

Porém, no código abaixo, percebam que quando o atributo é criado com __ antes do nome, o Python modifica o nome do atributo, adicionando o nome da classe na frente.

class MaquinaCafe:
    def __init__(self):
        self.__agua = 1000  # Quantidade de água (privado)


maquina = MaquinaCafe()
# print(maquina.__agua) AttributeError: 'MaquinaCafe' object has no attribute '__agua'"
print(dir(maquina))
['_MaquinaCafe__agua', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

De fato o atributo __agua não existe, pois ele não está na lista! O que existe é o atributo _MaquinaCafe__agua. É isso mesmo! O Python modificou o nome do atributo, adicionando o nome da classe na frente, para tentar evitar que você acesse diretamente o atributo privado. Maaaaaaasssssssssss, ainda sim, sabendo disso que eu lhe disse, é possível acessar o atributo privado diretamente (Python de vez em quando é engraçado e nos engana!).

Isso quer dizer então que eu consigo acessar o atributo privado _MaquinaCafe__agua diretamente? A resposta é: sim! Experimente você mesmo dar um print(maquina._MaquinaCafe__agua) e veja você mesmo que funciona! Mas novamente, pela milhonésima vez (de tanto eu encher o saco acho que você vai lembrar né?), siga a convenção de Python e não acesse ou modifique atributos privados diretamente!

A mesma coisa funciona para métodos privados. Vejam abaixo:

class MaquinaCafe:
    def __init__(self):
        self.__agua = 1000  # Quantidade de água (privado)

    def __aquecer_agua(self):  # Método privado
        print("Aquecendo a água...")
        self.__agua -= 200


# Usuário (externo) da classe
maquina = MaquinaCafe()
# print(maquina.__aquecer_agua()) AttributeError: 'MaquinaCafe' object has no attribute '__aquecer_agua'"
print(dir(maquina))

# Sabendo que o Python modifica o nome dos atributos e métodos privados,
# podemos acessá-los... mas não o faça!
maquina._MaquinaCafe__aquecer_agua()
['_MaquinaCafe__agua', '_MaquinaCafe__aquecer_agua', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
Aquecendo a água...

Conclusão#

Neste capítulo vimos o conceito de encapsulamento, que é um dos quatro princípios fundamentais da programação orientada a objetos (POO). O encapsulamento é a ideia de agrupar atributos e métodos que operam nesses dados dentro de objeto, e de restringir o acesso direto a alguns dos componentes do objeto, essencialmente «escondendo» os detalhes internos da implementação.

Temos 3 tipos de métodos e atributos:

  • Normais (públicos): Sem convenções de nomenclatura especiais, acessíveis de fora da classe sem modificações no nome.

  • Único sublinhado _: Indica uso interno da classe e serve como uma convenção. Acessível de fora da classe sem modificações no nome.

  • Duplo sublinhado __: Introduz o embaralhamento de nome, alterando o nome para incluir o nome da classe. Acessível, mas com um nome modificado. Fornece uma maneira de restringir o acesso acidental, mas ainda é possível acessar os atributos e métodos ao usar o nome modificado.

Na sequencia vamos ver o terceiro princípio da POO, que é a polimorfismo, quando um mesmo método ou função pode funcionar de maneiras diferentes, dependendo do objeto que o utiliza.