Python Encapsulamento

Na aula Atributos e Métodos, a conta bancária ganhou um sacar que nega o saque quando falta dinheiro. Mas nada impede alguém de pular o método e escrever direto conta.saldo = -1000. A regra do saque fica lá, bonitinha, e ninguém é obrigado a passar por ela. Encapsulamento (de "pôr numa cápsula") é a ideia de proteger os dados de dentro do objeto e deixar quem está de fora usar só os métodos. Nesta aula você vai ver como o Python faz isso, e por que o jeito dele é diferente do de outras linguagens: em Python, a proteção é um combinado, não um cadeado.

Você vai aprender
  • Por que esconder os dados de um objeto, e o que é a "interface" de uma classe.
  • O atributo protegido, com um sublinhado: self._saldo.
  • O atributo privado, com dois: self.__saldo, e o que o Python faz com ele por baixo (o name mangling).
  • Por que, mesmo assim, nada é "trancado" em Python: "somos todos adultos aqui".
  • A escrever métodos internos, como _registrar.
  • Os erros reais: o AttributeError do __saldo, o __saldo criado de fora sem querer, e a filha que não enxerga o __ da mãe.

A ideia: o caixa eletrônico

No caixa eletrônico, a Ana aperta botões: "saque", "depósito", "saldo". Ela não abre a máquina para pegar as notas com a mão, nem escreve o saldo novo numa folha lá dentro. Os botões são o jeito combinado de usar a máquina, e é por eles que o banco confere tudo: se há saldo, se o valor faz sentido, se o cartão é dela.

Uma classe bem feita funciona igual. Os métodos são os botões: o jeito combinado de usar o objeto. Os atributos internos (o saldo, o extrato) são as peças de dentro da máquina. O conjunto dos botões se chama interface da classe: é o que quem está de fora precisa conhecer. O resto é assunto da própria classe.

No caixa eletrônicoNa classe
Os botõesOs métodos públicos: depositar, sacar, ver_saldo
O cofre e as peças de dentroOs atributos internos: _saldo, _extrato
"Não abra a máquina"O sublinhado no começo do nome

1. O problema: qualquer um mexe em tudo

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo

    def sacar(self, valor):
        if valor > self.saldo:
            print("Saldo insuficiente.")
        else:
            self.saldo -= valor


conta = ContaBancaria("Ana", 100)
conta.sacar(500)
conta.saldo = -1000
print(conta.saldo)
$ python3 problema.py
Saldo insuficiente.
-1000                              « ninguém conferiu nada

O sacar(500) foi negado, como devia. Mas a linha seguinte passou por cima da regra e deixou a conta com menos mil reais, sem erro nenhum. Num programa pequeno, quem escreveu isso foi você mesmo, e você sabe que não devia. Num programa grande, com muita gente mexendo, é questão de tempo alguém fazer isso sem querer.


2. Um sublinhado: "isto é de uso interno"

O primeiro passo é avisar. Em Python, um nome que começa com um sublinhado (_saldo) quer dizer: "isto é de uso interno da classe; de fora, use os métodos". Os programadores chamam de atributo protegido.

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

    def depositar(self, valor):
        if valor <= 0:
            print("Depósito precisa ser maior que zero.")
            return
        self._saldo += valor

    def ver_saldo(self):
        return self._saldo


conta = ContaBancaria("Ana", 100)
conta.depositar(50)
conta.depositar(-30)
print(conta.ver_saldo())
print(conta._saldo)
self._saldo
O sublinhado faz parte do nome, como uma letra. Para o Python, _saldo é um atributo como outro qualquer. O sublinhado é um recado para as pessoas.
def depositar(self, valor):
Um "botão": confere o valor antes de mexer no saldo. O return sozinho encerra o método mais cedo, sem depositar.
def ver_saldo(self):
O jeito combinado de ler o saldo de fora: só devolve, não deixa mudar.
conta._saldo
Funciona! O Python não impede. Mas quem escreve isso de fora da classe está desrespeitando o combinado, e os editores de código e os colegas vão reclamar.
$ python3 protegido.py
Depósito precisa ser maior que zero.
150
150                                « o _saldo continua acessível: é só um aviso
"Somos todos adultos aqui"

Em linguagens como Java e C++, um atributo private é trancado: tentar usá-lo de fora é erro antes mesmo de o programa rodar. O Python escolheu outro caminho, resumido num ditado famoso da comunidade: "somos todos adultos aqui" (em inglês, we are all consenting adults here). O sublinhado avisa "não mexa"; se mesmo assim alguém mexer, é porque sabe o que está fazendo (por exemplo, num teste, ou para investigar um problema) e assume a responsabilidade. O Python confia no programador, em vez de trancar a porta.


3. Dois sublinhados: o __privado e o name mangling

Com dois sublinhados no começo (__saldo), o Python faz algo a mais: troca o nome do atributo por baixo dos panos. Veja:

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.__saldo = saldo

    def ver_saldo(self):
        return self.__saldo


conta = ContaBancaria("Ana", 100)
print(conta.ver_saldo())
print(vars(conta))
print(conta.__saldo)
$ python3 privado.py
100
{'titular': 'Ana', '_ContaBancaria__saldo': 100}
Traceback (most recent call last):
  File "/home/ana/privado.py", line 13, in <module>
    print(conta.__saldo)
          ^^^^^^^^^^^^^
AttributeError: 'ContaBancaria' object has no attribute '__saldo'. Did you mean: 'ver_saldo'?

Três coisas aconteceram:

  1. Dentro da classe, tudo funcionou: o ver_saldo leu o self.__saldo e devolveu 100.
  2. O vars(conta) mostra o segredo: o atributo não se chama __saldo. Ele foi guardado como _ContaBancaria__saldo: um sublinhado, o nome da classe, e o nome que você escreveu. Essa troca de nome se chama name mangling ("embaralhamento de nome"). O Python faz a troca em todo __nome escrito dentro da classe.
  3. De fora, conta.__saldo não existe, e dá AttributeError. O Python 3.14 ainda sugere o caminho certo: "Você quis dizer ver_saldo?". Use o botão.

Mas não é um cadeado

Sabendo o nome trocado, dá para ler e até mudar o atributo de fora:

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.__saldo = saldo

    def ver_saldo(self):
        return self.__saldo


conta = ContaBancaria("Ana", 100)
print(conta._ContaBancaria__saldo)
conta._ContaBancaria__saldo = 1_000_000
print(conta.ver_saldo())
$ python3 mangling.py
100
1000000                            « "privado", mas mudou

De novo: somos todos adultos. O __ não é segurança (nunca guarde uma senha achando que o __ a protege); é uma forma de deixar bem difícil mexer sem querer. Ninguém escreve _ContaBancaria__saldo por acidente. (O 1_000_000 é só um jeito de escrever um milhão com separadores, para ler melhor: o Python ignora os sublinhados dentro de números.)

Para que serve, então, o __?

O motivo de verdade do name mangling é a herança: evitar que uma classe filha, sem saber, use o mesmo nome de um atributo interno da mãe e estrague o dela.

class Produto:
    def __init__(self, nome):
        self.__codigo = "P-001"
        self.nome = nome

    def codigo(self):
        return self.__codigo


class ProdutoImportado(Produto):
    def __init__(self, nome, pais):
        super().__init__(nome)
        self.__codigo = "IMP-77"
        self.pais = pais


vinho = ProdutoImportado("Vinho", "Chile")
print(vinho.codigo())
print(vars(vinho))
$ python3 colisao.py
P-001
{'_Produto__codigo': 'P-001', 'nome': 'Vinho', '_ProdutoImportado__codigo': 'IMP-77', 'pais': 'Chile'}

A mãe e a filha escreveram self.__codigo, mas cada uma ganhou o seu: _Produto__codigo e _ProdutoImportado__codigo. O método codigo() da mãe continua lendo o dela (P-001), sem ser atrapalhado pela filha. Sem os dois sublinhados, o __init__ da filha teria sobrescrito o código da mãe.

E o __init__? Ele também começa com dois sublinhados…

O name mangling só vale para nomes que começam com dois sublinhados e não terminam com dois. __saldo é trocado; __init__, __class__ e os outros "dunder" (com dois sublinhados dos dois lados) são nomes especiais do Python e ficam como estão. Eles não são privados: são públicos, só que especiais (aula Métodos Especiais).


4. Métodos internos

O sublinhado vale também para métodos. Um método com _ é uma "peça de dentro": a classe usa para se organizar, mas quem está de fora não precisa chamar.

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self._saldo = saldo
        self._extrato = []

    def _registrar(self, texto):
        self._extrato.append(texto)

    def depositar(self, valor):
        self._saldo += valor
        self._registrar(f"Depósito: +{valor:.2f}")

    def sacar(self, valor):
        if valor > self._saldo:
            self._registrar(f"Saque negado: {valor:.2f}")
            return False
        self._saldo -= valor
        self._registrar(f"Saque: -{valor:.2f}")
        return True

    def extrato(self):
        for linha in self._extrato:
            print(linha)
        print(f"Saldo: R$ {self._saldo:.2f}")


conta = ContaBancaria("Ana", 100)
conta.depositar(50)
conta.sacar(500)
conta.sacar(30)
conta.extrato()
$ python3 metodo_interno.py
Depósito: +50.00
Saque negado: 500.00
Saque: -30.00
Saldo: R$ 120.00

A interface da conta são três botões: depositar, sacar e extrato. O _registrar, o _saldo e o _extrato são da cozinha. A vantagem aparece no dia em que o banco quiser guardar o extrato num arquivo, em vez de numa lista: muda só o _registrar, por dentro, e quem usa a conta nem fica sabendo. Isso é o que o encapsulamento compra: liberdade para mudar o lado de dentro sem quebrar o lado de fora.

Missão: o sacar desta conta não confere nada, e o saque de 500 deixa o saldo negativo. Faça o método recusar o saque quando o valor for maior que o self._saldo, mostrando Saldo insuficiente. No fim, o programa deve mostrar Saldo: R$ 70.00.

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

    def sacar(self, valor):
        self._saldo -= valor

    def mostrar(self):
        print(f"Saldo: R$ {self._saldo:.2f}")


conta = ContaBancaria("Ana", 100)
conta.sacar(500)
conta.sacar(30)
conta.mostrar()
class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self._saldo = saldo

    def sacar(self, valor):
        if valor > self._saldo:
            print("Saldo insuficiente")
        else:
            self._saldo -= valor

    def mostrar(self):
        print(f"Saldo: R$ {self._saldo:.2f}")


conta = ContaBancaria("Ana", 100)
conta.sacar(500)
conta.sacar(30)
conta.mostrar()

Formas diferentes de proteger um atributo

NomeChamado deO que o Python fazQuando usar
saldoPúblicoNadaDados que qualquer um pode ler e mudar sem regra: nome, cidade
_saldoProtegido (interno)Nada: é só um combinado entre pessoasO normal para dados internos. É o que o guia recomenda
__saldoPrivadoTroca o nome para _ContaBancaria__saldoQuando a classe vai ter filhas e você quer evitar que um nome delas atrapalhe o seu. Fora disso, o _ basta
__init__Especial (dunder)Nada: o Python usa esses nomes por conta própriaSó os que o Python já define. Não invente nomes novos assim

E quando um atributo precisa ser lido e mudado de fora, mas com regra (o preço não pode ser negativo)? Os métodos ver_saldo() e companhia funcionam, mas o Python tem um jeito mais elegante, em que conta.saldo parece um atributo comum e por baixo chama um método. É o @property, a próxima aula: @property.


Quando dá errado

Ler o __saldo de fora: AttributeError

Você viu acima: print(conta.__saldo) dá

AttributeError: 'ContaBancaria' object has no attribute '__saldo'. Did you mean: 'ver_saldo'?

O atributo existe, mas com o nome trocado (_ContaBancaria__saldo), e a troca só acontece no código escrito dentro da classe. De fora, __saldo é só um nome que não existe. Como consertar: use o método que a classe oferece (o Python até sugere: ver_saldo). Se a classe não oferece nenhum, é porque ela não quer que aquele dado seja usado de fora.

"Mudei o __saldo de fora e não mudou nada"

Este é traiçoeiro, porque não dá erro nenhum:

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.__saldo = saldo

    def ver_saldo(self):
        return self.__saldo


conta = ContaBancaria("Ana", 100)
conta.__saldo = 999
print(conta.__saldo)
print(conta.ver_saldo())
print(vars(conta))
$ python3 fora.py
999                                « parece que mudou…
100                                « …mas o saldo de verdade continua 100
{'titular': 'Ana', '_ContaBancaria__saldo': 100, '__saldo': 999}

Fora da classe não há troca de nome, então conta.__saldo = 999 criou um atributo novo, chamado __saldo mesmo, ao lado do verdadeiro (_ContaBancaria__saldo). O vars mostra os dois convivendo. A classe continua usando o dela, e o 999 fica lá, enganando quem olha. Como consertar: não escreva atributos internos de fora. Para mudar o saldo, use os métodos (depositar, sacar).

A filha não enxerga o __ da mãe
class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.__saldo = saldo


class ContaPoupanca(ContaBancaria):
    def render(self):
        self.__saldo += self.__saldo * 0.01


poupanca = ContaPoupanca("Bia", 1000)
poupanca.render()
$ python3 filha.py
Traceback (most recent call last):
  File "/home/ana/filha.py", line 13, in <module>
    poupanca.render()
    ~~~~~~~~~~~~~~~^^
  File "/home/ana/filha.py", line 9, in render
    self.__saldo += self.__saldo * 0.01
    ^^^^^^^^^^^^
AttributeError: 'ContaPoupanca' object has no attribute '_ContaPoupanca__saldo'. Did you mean: '_ContaBancaria__saldo'?

Leia a mensagem com calma: "o objeto ContaPoupanca não tem o atributo _ContaPoupanca__saldo. Você quis dizer _ContaBancaria__saldo?". Dentro da ContaPoupanca, o self.__saldo virou _ContaPoupanca__saldo (o nome da classe onde o código está escrito), mas o saldo foi guardado pela mãe como _ContaBancaria__saldo. O __ fez exatamente o seu trabalho: separou os nomes da mãe e da filha. Só que aqui a filha queria o da mãe. Como consertar: se as filhas precisam usar o atributo, use um sublinhado (self._saldo) na mãe e nas filhas:

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

    def ver_saldo(self):
        return self._saldo


class ContaPoupanca(ContaBancaria):
    def render(self):
        self._saldo += self._saldo * 0.01


poupanca = ContaPoupanca("Bia", 1000)
poupanca.render()
print(poupanca.ver_saldo())
$ python3 filha_ok.py
1010.0

É por isso que o _ é a escolha normal: as filhas fazem parte da "família" da classe e costumam precisar dos dados internos.

O sublinhado não impede nada

Não é um erro do Python, é um erro de expectativa: quem acha que _saldo está "protegido" se surpreende quando conta._saldo = -1000 funciona. Funciona mesmo, como você viu na seção 2. O _ é um aviso para pessoas, e quem decide respeitar é quem escreve o código. Se a regra precisa valer sempre, mesmo quando alguém escreve conta.saldo = …, o caminho é o @property, que roda uma validação a cada mudança.


Referência rápida

EscritaO que quer dizer
self.saldoPúblico: qualquer um usa
self._saldoProtegido: "uso interno; de fora, use os métodos". O Python não impede
self.__saldoPrivado: dentro da classe vira self._ContaBancaria__saldo (name mangling)
def _registrar(self, …):Método interno, para a própria classe usar
obj._Classe__nomeO nome "trocado": dá acesso ao privado de fora (só para investigar, nunca no dia a dia)
vars(obj)Mostra os atributos com os nomes de verdade, inclusive os trocados
SintomaCausa provável
AttributeError: … has no attribute '__saldo' (de fora)O atributo é privado: o nome de verdade é _Classe__saldo. Use um método
Mudei o __saldo de fora, e a classe continua com o valor antigoDe fora não há troca de nome: você criou um atributo novo
has no attribute '_Filha__saldo'. Did you mean: '_Mae__saldo'?A filha usou o __ da mãe. Troque por um sublinhado só
Alguém mudou o _saldo de fora e nada reclamouO _ é só um combinado. Para validar sempre, use @property

Exercícios

1. O nome trocado

Na classe Aluno, o __init__ tem self.__nota = 10. Com que nome esse atributo aparece no vars() do objeto?

2. O combinado

Quantos sublinhados o guia recomenda, normalmente, no começo do nome de um atributo interno?

3. O que acontece com conta._saldo = -1000, escrito fora da classe?

4. Para que o name mangling do __ serve de verdade?

5. A conta tem self.__saldo = 100. De fora, a Ana escreve conta.__saldo = 999 e depois chama conta.ver_saldo(), que devolve self.__saldo. O que aparece?

6. O que quer dizer "somos todos adultos aqui", no Python?

Missão final: fechando o cofre

Missão: nesta conta, o saldo é público, e a linha da "fraude" lá embaixo consegue mudá-lo para um milhão. Sem mexer no código de baixo, troque o atributo por um privado, self.__saldo, em todos os lugares da classe. Depois disso, a fraude não atinge mais o saldo de verdade, e o programa mostra Antes: R$ 150.00 e Depois: R$ 150.00.

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo

    def depositar(self, valor):
        if valor > 0:
            self.saldo += valor

    def ver_saldo(self):
        return self.saldo


conta = ContaBancaria("Ana", 100)
conta.depositar(50)
print(f"Antes: R$ {conta.ver_saldo():.2f}")

conta.saldo = 1_000_000  # a fraude
print(f"Depois: R$ {conta.ver_saldo():.2f}")
class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.__saldo = saldo

    def depositar(self, valor):
        if valor > 0:
            self.__saldo += valor

    def ver_saldo(self):
        return self.__saldo


conta = ContaBancaria("Ana", 100)
conta.depositar(50)
print(f"Antes: R$ {conta.ver_saldo():.2f}")

conta.saldo = 1_000_000  # a fraude
print(f"Depois: R$ {conta.ver_saldo():.2f}")

Repare: a "fraude" não deu erro. Ela criou um atributo novo, saldo, que a classe nem olha. Experimente acrescentar um print(vars(conta)) no fim para ver os dois lado a lado. Um _saldo com um sublinhado só também teria resolvido esta missão, já que a fraude escreve saldo, sem sublinhado nenhum.


Resumo

  • Encapsulamento: proteger os dados de dentro do objeto e deixar quem está de fora usar os métodos (a interface), como os botões do caixa eletrônico.
  • _nome (um sublinhado) = "uso interno". É só um combinado: o Python não impede ninguém. É a escolha normal.
  • __nome (dois sublinhados) dentro da classe vira _Classe__nome: o name mangling. De fora, obj.__nome dá AttributeError.
  • O __ não é cadeado nem segurança: obj._Classe__nome ainda funciona. Ele serve para a mãe e as filhas não pisarem nos nomes umas das outras.
  • Escrever obj.__nome = … de fora cria um atributo novo, sem erro e sem mudar o de verdade.
  • "Somos todos adultos aqui": o Python avisa e confia, em vez de trancar.
  • Nomes com dois sublinhados dos dois lados (__init__) são especiais, não privados.