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.
- 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
AttributeErrordo__saldo, o__saldocriado 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ônico | Na classe |
|---|---|
| Os botões | Os métodos públicos: depositar, sacar, ver_saldo |
| O cofre e as peças de dentro | Os 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
returnsozinho 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
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:
- Dentro da classe, tudo funcionou: o
ver_saldoleu oself.__saldoe devolveu 100. - 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__nomeescrito dentro da classe. - De fora,
conta.__saldonão existe, e dáAttributeError. O Python 3.14 ainda sugere o caminho certo: "Você quis dizerver_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.
__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
| Nome | Chamado de | O que o Python faz | Quando usar |
|---|---|---|---|
saldo | Público | Nada | Dados que qualquer um pode ler e mudar sem regra: nome, cidade |
_saldo | Protegido (interno) | Nada: é só um combinado entre pessoas | O normal para dados internos. É o que o guia recomenda |
__saldo | Privado | Troca o nome para _ContaBancaria__saldo | Quando 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ópria | Só 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
__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.
__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).
__ 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.
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
| Escrita | O que quer dizer |
|---|---|
self.saldo | Público: qualquer um usa |
self._saldo | Protegido: "uso interno; de fora, use os métodos". O Python não impede |
self.__saldo | Privado: dentro da classe vira self._ContaBancaria__saldo (name mangling) |
def _registrar(self, …): | Método interno, para a própria classe usar |
obj._Classe__nome | O 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 |
| Sintoma | Causa 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 antigo | De 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 reclamou | O _ é 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.__nomedáAttributeError.- O
__não é cadeado nem segurança:obj._Classe__nomeainda 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.