Encapsulamento em Python: O que É e Como Funciona | Python Brasil
Aprenda encapsulamento em Python: name mangling, property avançado, descritores, __getattr__, slots e a filosofia Python vs Java. Exemplos completos.
O que é Encapsulamento?
Encapsulamento é o princípio da POO que consiste em esconder os detalhes internos de uma classe e expor apenas o que é necessário. Isso protege os dados de modificações indevidas e permite alterar a implementação interna sem afetar quem usa a classe.
Em Python, o encapsulamento é feito por convenção, não por restrição obrigatória da linguagem. A filosofia Python é “somos todos adultos responsáveis” — os mecanismos de proteção servem como sinalização, não como barreiras intransponíveis.
Convenções de acesso
Python usa prefixos de underscore para indicar o nível de acesso:
class ContaBancaria:
def __init__(self, titular, saldo):
self.titular = titular # Público: acesso livre
self._agencia = "0001" # Protegido: convenção, não restricao
self.__saldo = saldo # Privado: name mangling aplicado
@property
def saldo(self):
return self.__saldo
@saldo.setter
def saldo(self, valor):
if valor < 0:
raise ValueError("Saldo não pode ser negativo!")
self.__saldo = valor
conta = ContaBancaria("Maria", 1000)
print(conta.titular) # Maria — acesso público
print(conta._agencia) # 0001 — funciona, mas sinaliza "uso interno"
print(conta.saldo) # 1000 — via property
Name mangling em detalhe
Quando você usa __atributo (dois underscores antes, sem underscores depois), o Python aplica name mangling: renomeia o atributo internamente para _NomeClasse__atributo. Isso serve para evitar colisões de nomes em herança, não para criar acesso impossível.
class Base:
def __init__(self):
self.__secreto = "valor da base"
def revelar(self):
return self.__secreto # Funciona: acessa _Base__secreto
class Filha(Base):
def __init__(self):
super().__init__()
self.__secreto = "valor da filha" # _Filha__secreto — diferente!
def revelar_filha(self):
return self.__secreto # Acessa _Filha__secreto
obj = Filha()
print(obj.revelar()) # "valor da base" — não foi sobrescrito!
print(obj.revelar_filha()) # "valor da filha"
# O name mangling pode ser contornado explicitamente:
print(obj._Base__secreto) # "valor da base" — acesso direto ao nome real
print(obj._Filha__secreto) # "valor da filha"
O name mangling é um mecanismo de proteção contra colisões em herança, não segurança real. Não use __atributo com a expectativa de tornar dados verdadeiramente inacessíveis.
O decorator @property em profundidade
@property transforma um método em um atributo acessível sem parênteses, permitindo validação, computação lazy e manutenção de interface pública estável.
class Temperatura:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Temperatura em graus Celsius."""
return self._celsius
@celsius.setter
def celsius(self, valor):
if valor < -273.15:
raise ValueError("Temperatura abaixo do zero absoluto!")
self._celsius = valor
@celsius.deleter
def celsius(self):
print("Resetando temperatura para 0°C")
self._celsius = 0
@property
def fahrenheit(self):
"""Propriedade somente leitura calculada."""
return self._celsius * 9/5 + 32
@property
def kelvin(self):
return self._celsius + 273.15
temp = Temperatura(25)
print(f"{temp.celsius}°C = {temp.fahrenheit}°F = {temp.kelvin}K")
# 25°C = 77.0°F = 298.15K
temp.celsius = 100
print(temp.fahrenheit) # 212.0
del temp.celsius # Resetando temperatura para 0°C
print(temp.celsius) # 0
Uma vantagem importante de @property: você pode começar com um atributo público simples e depois adicionar validação sem quebrar a interface. Código que usa obj.valor continua funcionando mesmo após a conversão para property.
Properties vs métodos: quando usar cada um
Use @property quando o acesso parece ser de um atributo (leitura direta, sem efeitos colaterais pesados). Use métodos quando a operação tem efeitos colaterais claros, recebe argumentos ou é visivelmente uma ação.
class Pedido:
def __init__(self, itens):
self._itens = itens
# Property: acesso a dado calculado, parece atributo
@property
def total(self):
return sum(item['preco'] * item['qty'] for item in self._itens)
# Método: ação explícita com efeito colateral
def adicionar_item(self, nome, preco, qty):
self._itens.append({'nome': nome, 'preco': preco, 'qty': qty})
# Método: ação com retorno significativo
def aplicar_desconto(self, percentual):
for item in self._itens:
item['preco'] *= (1 - percentual / 100)
return self.total
O protocolo descritor
Descritores são a base dos @property, @staticmethod e @classmethod. Um descritor é qualquer objeto que implemente __get__, __set__ ou __delete__. Eles permitem criar atributos com comportamento customizado reutilizável entre várias classes.
class TipadoValidado:
"""Descritor que valida o tipo e range de atributos numéricos."""
def __init__(self, tipo, minimo=None, maximo=None):
self.tipo = tipo
self.minimo = minimo
self.maximo = maximo
def __set_name__(self, owner, name):
self.nome_publico = name
self.nome_privado = f'_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.nome_privado, None)
def __set__(self, obj, valor):
if not isinstance(valor, self.tipo):
raise TypeError(
f"{self.nome_publico} deve ser {self.tipo.__name__}, "
f"recebeu {type(valor).__name__}"
)
if self.minimo is not None and valor < self.minimo:
raise ValueError(f"{self.nome_publico} mínimo é {self.minimo}")
if self.maximo is not None and valor > self.maximo:
raise ValueError(f"{self.nome_publico} máximo é {self.maximo}")
setattr(obj, self.nome_privado, valor)
class Produto:
nome = TipadoValidado(str)
preco = TipadoValidado((int, float), minimo=0)
estoque = TipadoValidado(int, minimo=0, maximo=10000)
def __init__(self, nome, preco, estoque):
self.nome = nome
self.preco = preco
self.estoque = estoque
p = Produto("Notebook", 3500.0, 10)
# p.preco = -100 # ValueError: preco mínimo é 0
# p.estoque = "5" # TypeError: estoque deve ser int
getattr vs getattribute
__getattr__ é chamado apenas quando o atributo não é encontrado pelos meios normais — ideal para atributos dinâmicos. __getattribute__ é chamado sempre para qualquer acesso de atributo — use com extremo cuidado para evitar recursão infinita.
class Configuracao:
"""Classe que permite acesso a configurações com fallback para padrão."""
def __init__(self, dados):
# Usar object.__setattr__ para evitar recursão no __setattr__
object.__setattr__(self, '_dados', dados)
object.__setattr__(self, '_padroes', {
'timeout': 30,
'retries': 3,
'debug': False,
})
def __getattr__(self, nome):
# Chamado apenas quando o atributo normal não existe
dados = object.__getattribute__(self, '_dados')
padroes = object.__getattribute__(self, '_padroes')
if nome in dados:
return dados[nome]
if nome in padroes:
return padroes[nome]
raise AttributeError(f"Configuração '{nome}' não encontrada")
def __setattr__(self, nome, valor):
dados = object.__getattribute__(self, '_dados')
dados[nome] = valor
cfg = Configuracao({'host': 'localhost', 'porta': 5432})
print(cfg.host) # localhost
print(cfg.porta) # 5432
print(cfg.timeout) # 30 (fallback para padrão)
print(cfg.debug) # False (fallback para padrão)
slots e encapsulamento
__slots__ controla quais atributos um objeto pode ter, evitando a adição acidental de novos atributos e reduzindo o consumo de memória:
class Ponto:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Ponto(1, 2)
print(p.x) # 1
# p.z = 3 # AttributeError: 'Ponto' object has no attribute 'z'
# Sem __dict__, sem adição de atributos não declarados
# Com __slots__, não há __dict__:
# print(p.__dict__) # AttributeError
Dataclass e visibilidade de campos
Com @dataclass, a visibilidade segue as mesmas convenções:
from dataclasses import dataclass, field
@dataclass
class Usuario:
nome: str
email: str
_senha_hash: str = field(default="", repr=False, compare=False)
__id: int = field(default=0, repr=False, compare=False, init=False)
def definir_senha(self, senha):
import hashlib
self._senha_hash = hashlib.sha256(senha.encode()).hexdigest()
def verificar_senha(self, senha):
import hashlib
return self._senha_hash == hashlib.sha256(senha.encode()).hexdigest()
Python vs Java: filosofia de encapsulamento
Em Java, private realmente impede o acesso externo. Em Python, a filosofia é diferente: o código-fonte é sempre acessível, e tentar criar barreiras intransponíveis vai contra a filosofia da linguagem.
Python aposta na transparência e convenção: _atributo diz “prefiro que você não use isso diretamente”, __atributo diz “este nome é específico desta classe para evitar colisões”. A confiança no desenvolvedor é intencional — facilita testes, inspeção e debugging.
Isso não significa que encapsulamento é ignorado em Python. Significa que é aplicado através de design de API claro, não de restrições de acesso impostas pela linguagem.
Erros comuns
Nunca use atributos mutáveis (listas, dicionários) como padrão em __init__ sem inicializá-los individualmente. Outro erro frequente é usar __atributo pensando que é “realmente privado” — ele ainda é acessível via _Classe__atributo. Evite também criar properties para todos os atributos de forma reflexiva — use-as apenas quando há lógica de validação ou computação.
Quando usar cada nível de acesso?
Use atributo público para dados que fazem parte da interface principal do objeto. Use _atributo para implementação interna que pode mudar. Use __atributo para evitar colisões de nomes em hierarquias de herança. Use @property quando precisar de validação ou computação no momento do acesso.
Termos Relacionados
- Classe - Base da programação orientada a objetos
- Herança - Reutilização de código entre classes
- Polimorfismo - Múltiplas formas para mesma interface
- Decorators - @property é um decorator