O que é encapsulamento (na prática)
Encapsulamento é a prática de proteger o estado interno de um objeto e expor uma API pública (métodos e propriedades) que controla como esse estado pode ser lido e alterado. Em Python, encapsulamento não é “trava” absoluta como em algumas linguagens; ele é feito principalmente por convenções e por um design que centraliza validações e mantém invariantes (regras que devem ser sempre verdadeiras).
Exemplos de invariantes comuns: saldo nunca negativo, estoque nunca abaixo de um mínimo, idade dentro de um intervalo válido. O objetivo é impedir que código externo coloque o objeto em um estado inválido.
Convenções de visibilidade em Python: público, _protegido e __privado
Atributos públicos
Um atributo público (ex.: cliente.nome) pode ser lido e alterado livremente. Isso é útil quando não há regra a proteger, mas é perigoso quando existe uma invariante.
Prefixo _: “uso interno” (convenção)
Um atributo com _ (ex.: _saldo) sinaliza: “não use diretamente fora da classe”. Não impede acesso, mas ajuda a deixar claro o que é detalhe interno. Ferramentas e IDEs também usam essa convenção para sugerir que não é parte da API pública.
Prefixo __: name mangling (reduz colisões e desencoraja acesso)
Um atributo com __ (ex.: __saldo) sofre name mangling: internamente ele vira algo como _NomeDaClasse__saldo. Isso não é segurança, mas dificulta acesso acidental e evita colisões em herança.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
class Exemplo: def __init__(self): self.__segredo = 123 # vira _Exemplo__segredoNa prática, use _ para a maioria dos detalhes internos. Use __ quando quiser reduzir risco de colisão em subclasses ou reforçar que algo é estritamente interno.
Controle de acesso com métodos e propriedades
Para encapsular, você normalmente faz duas coisas:
- Guarda o estado em atributos internos (geralmente com
_). - Expõe operações públicas (métodos e/ou propriedades) que aplicam validações e preservam invariantes.
Propriedades (@property) para leitura e escrita controladas
Propriedades permitem oferecer uma interface “parecida com atributo”, mas com lógica por trás. Isso ajuda a manter uma API limpa sem expor o atributo interno.
class Pessoa: def __init__(self, idade): self.idade = idade # passa pelo setter e valida @property def idade(self): return self._idade @idade.setter def idade(self, valor): if not isinstance(valor, int): raise TypeError("idade deve ser um inteiro") if valor < 0 or valor > 130: raise ValueError("idade deve estar entre 0 e 130") self._idade = valorNote que a validação fica centralizada no setter. Assim, qualquer alteração (no construtor ou depois) passa pela mesma regra.
Invariantes: regras que o objeto nunca pode violar
Uma invariante é uma condição que deve ser verdadeira após qualquer operação pública. Encapsulamento é o mecanismo que ajuda a garantir isso, pois impede (ou desencoraja) alterações diretas no estado.
| Classe | Estado interno | Invariante | Operações públicas |
|---|---|---|---|
| ContaBancaria | _saldo | _saldo >= 0 | depositar(), sacar(), saldo (somente leitura) |
| Estoque | _quantidade, _minimo | _quantidade >= _minimo | repor(), retirar(), quantidade |
| Pessoa | _idade | 0 <= _idade <= 130 | idade (getter/setter com validação) |
Exemplo 1: Conta bancária com saldo não negativo
Objetivo
- Saldo nunca pode ficar negativo.
- API pública clara: consultar saldo, depositar, sacar.
- Detalhe interno: como o saldo é armazenado.
Passo a passo
1) Defina o estado interno como “interno”
Use _saldo e exponha saldo como propriedade somente leitura.
2) Centralize validações nos métodos públicos
depositar() e sacar() devem validar valores e manter a invariante.
3) Mensagens de erro úteis
Erros devem dizer o que está errado e, quando útil, mostrar limites (ex.: saldo atual).
class ContaBancaria: def __init__(self, saldo_inicial=0.0): self._saldo = 0.0 self.depositar(saldo_inicial) # reutiliza validação @property def saldo(self): return self._saldo def depositar(self, valor): if not isinstance(valor, (int, float)): raise TypeError("valor do depósito deve ser numérico") if valor <= 0: raise ValueError("valor do depósito deve ser maior que zero") self._saldo += float(valor) def sacar(self, valor): if not isinstance(valor, (int, float)): raise TypeError("valor do saque deve ser numérico") if valor <= 0: raise ValueError("valor do saque deve ser maior que zero") if valor > self._saldo: raise ValueError(f"saldo insuficiente: saldo atual é {self._saldo:.2f}") self._saldo -= float(valor)Repare que não existe saldo com setter. Isso evita que alguém faça conta.saldo = -100. A única forma de alterar o saldo é via métodos que preservam a regra.
Exemplo 2: Estoque com mínimo e retirada controlada
Objetivo
- Quantidade nunca pode ficar abaixo de um mínimo configurado.
- Regras de retirada e reposição centralizadas.
class Estoque: def __init__(self, quantidade_inicial, minimo=0): self._minimo = int(minimo) self._quantidade = 0 self.repor(quantidade_inicial) if self._quantidade < self._minimo: raise ValueError("quantidade inicial não pode ser menor que o mínimo") @property def quantidade(self): return self._quantidade @property def minimo(self): return self._minimo def repor(self, unidades): if not isinstance(unidades, int): raise TypeError("unidades para repor deve ser int") if unidades <= 0: raise ValueError("unidades para repor deve ser maior que zero") self._quantidade += unidades def retirar(self, unidades): if not isinstance(unidades, int): raise TypeError("unidades para retirar deve ser int") if unidades <= 0: raise ValueError("unidades para retirar deve ser maior que zero") nova_qtd = self._quantidade - unidades if nova_qtd < self._minimo: raise ValueError( f"retirada inválida: mínimo é {self._minimo}, " f"quantidade atual é {self._quantidade}" ) self._quantidade = nova_qtdAqui, a invariante é mais específica: quantidade >= minimo. O método retirar() calcula a nova quantidade e só aplica se continuar válida.
Exemplo 3: Idade válida com @property (setter com validação)
Quando faz sentido permitir alteração direta “como atributo”, use propriedade com setter. Isso mantém a API simples e ainda aplica regras.
class Cadastro: def __init__(self, nome, idade): self.nome = nome self.idade = idade # valida via setter @property def idade(self): return self._idade @idade.setter def idade(self, valor): if not isinstance(valor, int): raise TypeError("idade deve ser int") if valor < 0: raise ValueError("idade não pode ser negativa") if valor > 130: raise ValueError("idade acima do permitido (130)") self._idade = valorSe amanhã a regra mudar (por exemplo, máximo 150), você altera em um único lugar.
Como expor uma API pública clara e esconder detalhes internos
Checklist prático
- Escolha nomes públicos estáveis: métodos e propriedades que você quer que o usuário da classe use (ex.:
sacar,depositar,saldo). - Prefixe detalhes internos com
_: atributos que não devem ser manipulados diretamente (ex.:_saldo,_quantidade). - Evite setters quando não fizer sentido: se alterar diretamente quebra invariantes, exponha apenas operações (ex.: saldo só muda por depósito/saque).
- Centralize validações: não espalhe checagens em vários lugares; reaproveite métodos/setters para validar (ex.: construtor chama
depositar). - Erros úteis: prefira mensagens que indiquem o motivo e, quando relevante, o limite (ex.: “saldo insuficiente: saldo atual é ...”).
Testes simples: uso esperado e inválido (sem frameworks)
Mesmo sem bibliotecas de teste, você pode escrever verificações com assert e blocos try/except. A ideia é testar:
- Casos válidos (o estado muda corretamente).
- Casos inválidos (a classe levanta exceções e não viola invariantes).
Testando ContaBancaria
def test_conta_fluxo_basico(): c = ContaBancaria(100) assert c.saldo == 100.0 c.depositar(50) assert c.saldo == 150.0 c.sacar(20) assert c.saldo == 130.0 def test_conta_saque_invalido(): c = ContaBancaria(10) try: c.sacar(11) assert False, "era esperado ValueError por saldo insuficiente" except ValueError as e: assert "saldo insuficiente" in str(e) assert c.saldo == 10.0 # invariante preservada def test_conta_deposito_invalido(): c = ContaBancaria(10) try: c.depositar(0) assert False, "era esperado ValueError para depósito zero" except ValueError: passTestando Estoque
def test_estoque_retirada_respeita_minimo(): e = Estoque(quantidade_inicial=10, minimo=3) e.retirar(5) assert e.quantidade == 5 try: e.retirar(3) # levaria a 2, abaixo do mínimo 3 assert False, "era esperado ValueError por violar mínimo" except ValueError as err: assert "mínimo" in str(err) assert e.quantidade == 5 def test_estoque_repor_invalido(): e = Estoque(quantidade_inicial=1, minimo=0) try: e.repor(-2) assert False, "era esperado ValueError" except ValueError: passTestando Cadastro (idade)
def test_idade_valida(): p = Cadastro("Ana", 30) assert p.idade == 30 p.idade = 31 assert p.idade == 31 def test_idade_invalida(): p = Cadastro("Bia", 20) try: p.idade = -1 assert False, "era esperado ValueError" except ValueError as e: assert "negativa" in str(e)Esses testes ajudam a garantir que suas regras continuam valendo quando você refatorar a implementação interna (por exemplo, mudar como o saldo é armazenado), desde que a API pública permaneça a mesma.