Voltar ao Glossario
Glossario Python

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