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_underscorerealmente 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 = -9999999O 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:
- Integridade de dados — a lógica de validação em um único lugar, aplicada sempre.
- Flexibilidade — você pode alterar a representação interna (por exemplo, armazenar saldos em centavos em vez de reais) sem modificar nenhum código cliente.
- 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:
| Prefixo | Exemplo | Nível de acesso | Significado |
|---|---|---|---|
| Sem prefixo | balance | Público | Destinado ao uso por qualquer um |
Underscore simples _ | _balance | Protegido | Para uso interno e subclasses; pode ser acessado externamente, mas não é recomendado |
Underscore duplo __ | __pin | Privado | Apenas 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 methodNenhuma 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.__pinDe 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) # 2Você 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__secretAmbos 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._celsiusO 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 neededAdicionando 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 + 32t = 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 zerofahrenheit é 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 = valueO 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 lockedObserve:
usernameé público — qualquer um pode lê-lo sem problemas._login_attemptsé protegido — uma subclasseThrottledAccountpoderia lê-lo para implementar uma lógica mais sofisticada.__password_hashe__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ípio | Definição em uma linha |
|---|---|
| Encapsulamento | Agrupa dados + métodos; oculta detalhes internos |
| Herança | Permite que uma classe reutilize e estenda outra classe |
| Polimorfismo | Permite que tipos diferentes respondam à mesma chamada de método |
| Abstração | Expõ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ção | O que sinaliza | Aplicado pelo Python? |
|---|---|---|
name | Público — use livremente | Não (sempre acessível) |
_name | Protegido — uso interno | Não (acessível, mas convencionalmente fora dos limites) |
__name | Privado — apenas esta classe | Parcialmente — o nome sofre mangling para _NomeDaClasse__name |
@property | Acesso de leitura controlado | Sim — hooks de getter/setter/deleter |
@name.setter | Acesso de escrita controlado com validação | Sim |