Encapsulamento em Python Orientado a Objetos: convenções, validação e invariantes

Capítulo 4

Tempo estimado de leitura: 8 minutos

+ Exercício

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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

class Exemplo:    def __init__(self):        self.__segredo = 123  # vira _Exemplo__segredo

Na 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 = valor

Note 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.

ClasseEstado internoInvarianteOperações públicas
ContaBancaria_saldo_saldo >= 0depositar(), sacar(), saldo (somente leitura)
Estoque_quantidade, _minimo_quantidade >= _minimorepor(), retirar(), quantidade
Pessoa_idade0 <= _idade <= 130idade (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_qtd

Aqui, 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 = valor

Se 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:        pass

Testando 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:        pass

Testando 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.

Agora responda o exercício sobre o conteúdo:

Qual abordagem melhor aplica encapsulamento para garantir a invariante de que o saldo de uma conta bancária nunca fique negativo?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Encapsulamento protege o estado interno e centraliza validações na API pública. Mantendo o saldo como detalhe interno e alterando-o apenas via depositar/sacar, as regras são verificadas sempre e a invariante saldo >= 0 é preservada.

Próximo capitúlo

Propriedades (property) em POO Python: getters, setters e atributos computados

Arrow Right Icon
Capa do Ebook gratuito Python Orientado a Objetos para Iniciantes: Classes, Herança e Boas Práticas
29%

Python Orientado a Objetos para Iniciantes: Classes, Herança e Boas Práticas

Novo curso

14 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.