W3docs

Encapsulamento em Python

Aprenda encapsulamento em Python: membros públicos, protegidos e privados, name mangling e getters/setters com @property.

Encapsulamento é um dos quatro pilares da programação orientada a objetos. Significa agrupar os dados (atributos) de um objeto e os métodos que operam sobre esses dados em uma única unidade, controlando quais partes do objeto o mundo externo pode acessar ou modificar.

Bem aplicado, o encapsulamento mantém o estado interno de um objeto consistente, oculta detalhes de implementação para que você possa alterá-los posteriormente sem quebrar o código cliente, e torna suas classes mais seguras de usar.

Este capítulo aborda:

  • O que é encapsulamento e por que ele importa
  • Membros públicos, protegidos e privados — e as convenções de nomenclatura que Python utiliza
  • Name mangling — como atributos com __duplo_underscore realmente funcionam
  • Getters e setters com @property
  • Um exemplo prático que reúne tudo

Antes de continuar, certifique-se de que você já domina classes e objetos em Python. Para controle de acesso via atributos computados, consulte o capítulo relacionado sobre @property.

Por que o Encapsulamento É Importante

Considere uma conta bancária. Internamente ela armazena um saldo. Se esse saldo fosse um atributo simples que qualquer um pudesse definir, nada impediria um bug (ou um agente mal-intencionado) de fazer:

account.balance = -9999999

O encapsulamento resolve isso ocultando o saldo por trás de uma interface controlada. O código externo à classe só pode depositar ou sacar por meio de métodos que aplicam as regras de negócio. O armazenamento interno é um detalhe de implementação — o código cliente nunca o acessa diretamente.

Os três benefícios que isso proporciona são:

  1. Integridade de dados — a lógica de validação em um único lugar, aplicada sempre.
  2. Flexibilidade — você pode alterar a representação interna (por exemplo, armazenar saldos em centavos em vez de reais) sem modificar nenhum código cliente.
  3. Acoplamento reduzido — o código cliente depende apenas da interface pública, não de como a classe funciona internamente.

Níveis de Acesso: Público, Protegido e Privado

Python não possui modificadores de acesso como as palavras-chave private ou public. Em vez disso, utiliza uma convenção de nomenclatura para sinalizar a intenção:

PrefixoExemploNível de acessoSignificado
Sem prefixobalancePúblicoDestinado ao uso por qualquer um
Underscore simples __balanceProtegidoPara uso interno e subclasses; pode ser acessado externamente, mas não é recomendado
Underscore duplo ____pinPrivadoApenas para esta classe; Python o renomeia internamente para dificultar o acesso externo

Estas são convenções e mecanismos, não regras rígidas aplicadas por um compilador. Python confia que os desenvolvedores respeitarão o sinal.

Membros públicos

Atributos e métodos públicos formam a interface oficial da classe — a parte que você pretende que o código cliente utilize:

class BankAccount:
    account_type = 'savings'   # public class attribute

    def __init__(self, owner, balance):
        self.owner = owner     # public instance attribute

    def deposit(self, amount):
        pass                   # public method

Nenhuma nomenclatura especial é necessária. Qualquer código pode ler ou escrever um membro público livremente.

Membros protegidos (underscore simples _)

Um underscore inicial simples é um sinal que diz "este é um detalhe interno — não dependa dele fora da classe." Python não impõe isso; é puramente uma convenção:

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self._balance = balance   # protected — internal, but subclasses may need it

    def _validate_amount(self, amount):   # protected helper
        return isinstance(amount, (int, float)) and amount > 0

_balance ainda é acessível como account._balance de fora, mas o underscore avisa outros desenvolvedores (e linters) de que estão violando o contrato pretendido.

Um uso comum: uma classe base armazena dados em um atributo _ para que subclasses possam lê-lo, mantendo-o oculto de código não relacionado.

Membros privados (underscore duplo __)

Um underscore duplo inicial aciona o name mangling — Python renomeia o atributo internamente para _NomeDaClasse__atributo. Isso torna o acesso acidental externo muito mais difícil:

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self._balance = balance
        self.__pin = 1234       # private — not meant to be touched at all

    def verify_pin(self, pin):
        return pin == self.__pin

De fora da classe:

acc = BankAccount('Alice', 1000)
print(acc.owner)      # Alice   — public, fine
print(acc._balance)   # 1000    — protected, works but frowned upon
print(acc.__pin)      # AttributeError: 'BankAccount' object has no attribute '__pin'

O atributo ainda existe, mas com um nome diferente. Consulte a próxima seção para saber como encontrá-lo.

Name Mangling

Quando Python encontra self.__nome dentro de uma definição de classe, ele o reescreve internamente para self._NomeDaClasse__nome. Isso é o name mangling. O objetivo é evitar colisões acidentais em subclasses — não fornecer segurança real.

class Counter:
    def __init__(self):
        self.__count = 0

    def increment(self):
        self.__count += 1

    def value(self):
        return self.__count

c = Counter()
c.increment()
c.increment()
print(c.value())          # 2

# Direct access fails:
# print(c.__count)        # AttributeError

# But mangled name still works if you know it:
print(c._Counter__count)  # 2

Você pode inspecionar todos os atributos com vars() ou dir() para descobrir o nome com mangling:

print(list(vars(c)))
# ['_Counter__count']

Name mangling e herança

O name mangling é especialmente útil na herança. Sem ele, uma subclasse poderia sobrescrever acidentalmente um atributo privado de seu pai usando o mesmo nome. Com o mangling, cada classe obtém seu próprio namespace:

class Base:
    def __init__(self):
        self.__secret = 'base'

    def reveal(self):
        return self.__secret    # accesses _Base__secret

class Child(Base):
    def __init__(self):
        super().__init__()
        self.__secret = 'child'  # stored as _Child__secret, not the same thing

    def reveal_child(self):
        return self.__secret     # accesses _Child__secret

c = Child()
print(c.reveal())        # base   — Base.reveal() reads _Base__secret
print(c.reveal_child())  # child  — Child.reveal_child() reads _Child__secret

Ambos os atributos coexistem sem colisão, o que não seria possível sem o name mangling.

Getters e Setters com @property

Em muitas linguagens você escreve métodos explícitos get_x() e set_x(). Python oferece uma abordagem mais elegante: o decorator @property permite expor um método como se fosse um atributo simples, mantendo o código cliente legível enquanto você mantém controle total sobre leitura e escrita.

Getter básico

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

O código cliente lê t.celsius, não t.celsius(). O @property torna a chamada do método invisível:

t = Temperature(25)
print(t.celsius)   # 25  — no parentheses needed

Adicionando um setter com validação

Combine @property com um .setter para validar valores antes de armazená-los:

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError('Temperature below absolute zero')
        self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.celsius)      # 25
print(t.fahrenheit)   # 77.0

t.celsius = 100
print(t.fahrenheit)   # 212.0

t.celsius = -300      # ValueError: Temperature below absolute zero

fahrenheit é uma propriedade computada somente leitura — nenhum setter é definido, então Python lança um AttributeError se você tentar atribuir a ela.

Por que preferir @property em vez de getters/setters simples?

Você pode começar com um atributo público simples e promovê-lo a uma propriedade mais tarde sem alterar nenhum código cliente:

# v1 — plain attribute
class Circle:
    def __init__(self, radius):
        self.radius = radius

# v2 — property with validation, same public interface
class Circle:
    def __init__(self, radius):
        self.radius = radius   # still works from the caller's point of view

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError('Radius cannot be negative')
        self._radius = value

O código cliente que escreveu c.radius = 5 continua funcionando sem alterações. Apenas o comportamento muda — agora você valida o valor.

Para uma referência completa sobre @property, incluindo deleters, consulte Python @property.

Um Exemplo Completo: Conta de Usuário

O exemplo a seguir mostra os três níveis de acesso funcionando juntos em uma classe realista:

class UserAccount:
    def __init__(self, username, password):
        self.username = username           # public
        self._login_attempts = 0           # protected — subclasses may need this
        self.__password_hash = self.__hash(password)  # private

    def __hash(self, password):
        """Private helper — implementation detail, may change."""
        return hash(password)

    def check_password(self, password):
        """Public method — part of the official interface."""
        return self.__hash(password) == self.__password_hash

    def login(self, password):
        if self._login_attempts >= 3:
            return 'Account locked'
        if self.check_password(password):
            self._login_attempts = 0
            return 'Login successful'
        self._login_attempts += 1
        return f'Wrong password ({self._login_attempts}/3)'


user = UserAccount('alice', 'secret123')
print(user.login('bad'))         # Wrong password (1/3)
print(user.login('bad'))         # Wrong password (2/3)
print(user.login('bad'))         # Wrong password (3/3)
print(user.login('secret123'))   # Account locked

Observe:

  • username é público — qualquer um pode lê-lo sem problemas.
  • _login_attempts é protegido — uma subclasse ThrottledAccount poderia lê-lo para implementar uma lógica mais sofisticada.
  • __password_hash e __hash() são privados — a estratégia de armazenamento de senha é puramente interna. O código cliente não tem motivo para vê-la, e se você posteriormente mudar para bcrypt, só precisará alterar essas duas coisas.

Encapsulamento vs. Outros Pilares da POO

O encapsulamento é um dos quatro princípios da POO:

PrincípioDefinição em uma linha
EncapsulamentoAgrupa dados + métodos; oculta detalhes internos
HerançaPermite que uma classe reutilize e estenda outra classe
PolimorfismoPermite que tipos diferentes respondam à mesma chamada de método
AbstraçãoExpõe uma interface simplificada; oculta a complexidade

Consulte herança em Python, polimorfismo em Python e classes abstratas em Python para os demais pilares.

Referência Rápida

ConvençãoO que sinalizaAplicado pelo Python?
namePúblico — use livrementeNão (sempre acessível)
_nameProtegido — uso internoNão (acessível, mas convencionalmente fora dos limites)
__namePrivado — apenas esta classeParcialmente — o nome sofre mangling para _NomeDaClasse__name
@propertyAcesso de leitura controladoSim — hooks de getter/setter/deleter
@name.setterAcesso de escrita controlado com validaçãoSim

Prática

Prática
O que um underscore inicial simples (por exemplo, `_balance`) sinaliza em Python?
O que um underscore inicial simples (por exemplo, `_balance`) sinaliza em Python?
Was this page helpful?