Encapsulamiento en Python
Aprende el encapsulamiento en Python: miembros públicos, protegidos y privados, name mangling, y getters/setters con @property.
El encapsulamiento es uno de los cuatro pilares de la programación orientada a objetos. Consiste en agrupar los datos de un objeto (atributos) y los métodos que operan sobre esos datos en una sola unidad, controlando qué partes del objeto puede acceder o modificar el mundo exterior.
Bien aplicado, el encapsulamiento mantiene consistente el estado interno de un objeto, oculta los detalles de implementación para que puedas cambiarlos más adelante sin romper el código que lo llama, y hace que tus clases sean más seguras de usar.
Este capítulo cubre:
- Qué es el encapsulamiento y por qué importa
- Miembros públicos, protegidos y privados — y las convenciones de nomenclatura que usa Python
- Name mangling — cómo funcionan realmente los atributos con
__doble_guion_bajo - Getters y setters con
@property - Un ejemplo del mundo real que une todo
Antes de leer, asegúrate de estar familiarizado con las clases y objetos de Python. Para el control de acceso mediante atributos calculados, consulta el capítulo estrechamente relacionado sobre @property.
Por Qué Importa el Encapsulamiento
Considera una cuenta bancaria. Internamente lleva un seguimiento del saldo. Si ese saldo fuera un atributo simple que cualquiera pudiera establecer, nada impediría que un error (o un actor malicioso) hiciera:
account.balance = -9999999El encapsulamiento resuelve esto ocultando el saldo detrás de una interfaz controlada. El código fuera de la clase solo puede depositar o retirar dinero a través de métodos que aplican las reglas de negocio. El almacenamiento interno es un detalle de implementación — los llamadores nunca lo tocan directamente.
Los tres beneficios que esto te da son:
- Integridad de datos — la lógica de validación en un solo lugar, aplicada cada vez.
- Flexibilidad — puedes cambiar la representación interna (p. ej., almacenar saldos en centavos en lugar de dólares) sin tocar ningún código que lo llame.
- Reducción del acoplamiento — los llamadores dependen solo de la interfaz pública, no de cómo funciona internamente la clase.
Niveles de Acceso: Público, Protegido y Privado
Python no tiene modificadores de acceso como las palabras clave private o public. En cambio, usa una convención de nomenclatura para indicar la intención:
| Prefijo | Ejemplo | Nivel de acceso | Significado |
|---|---|---|---|
| Sin prefijo | balance | Público | Destinado a ser usado por cualquiera |
Guion bajo simple _ | _balance | Protegido | Para uso interno y subclases; utilizable externamente pero no recomendado |
Doble guion bajo __ | __pin | Privado | Solo para esta clase; Python lo renombra activamente para evitar el acceso fácil |
Estas son convenciones y mecanismos, no reglas estrictas impuestas por un compilador. Python confía en que los desarrolladores respeten la señal.
Miembros públicos
Los atributos y métodos públicos forman la interfaz oficial de la clase — la parte que se pretende que usen los llamadores:
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 methodNo se necesita ninguna nomenclatura especial. Cualquier código puede leer o escribir un miembro público libremente.
Miembros protegidos (guion bajo simple _)
Un guion bajo inicial es una señal que dice "esto es un detalle interno — por favor no dependas de ello desde fuera de la clase." Python no lo hace cumplir; es puramente una convención:
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 sigue siendo accesible como account._balance desde el exterior, pero el guion bajo advierte a otros desarrolladores (y a los linters) que están rompiendo el contrato previsto.
Un uso común: una clase base almacena datos en un atributo _ para que las subclases puedan leerlos, manteniéndolo oculto del código no relacionado.
Miembros privados (doble guion bajo __)
Un doble guion bajo inicial desencadena el name mangling — Python renombra el atributo internamente a _NombreClase__atributo. Esto hace que el acceso accidental desde el exterior sea mucho más 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.__pinDesde fuera de la clase:
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'El atributo sigue existiendo, pero con un nombre diferente. Consulta la siguiente sección para saber cómo encontrarlo.
Name Mangling
Cuando Python ve self.__nombre dentro de una definición de clase, lo reescribe internamente como self._NombreClase__nombre. Esto es el name mangling. Su propósito es evitar colisiones accidentales en las subclases — no proporcionar verdadera seguridad.
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) # 2Puedes inspeccionar todos los atributos con vars() o dir() para descubrir el nombre modificado:
print(list(vars(c)))
# ['_Counter__count']Name mangling y la herencia
El name mangling es especialmente útil en la herencia. Sin él, una subclase podría sobrescribir accidentalmente un atributo privado de su padre usando el mismo nombre. Con el mangling, cada clase obtiene su propio espacio de nombres:
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 atributos coexisten sin colisión, lo que no sería posible sin el name mangling.
Getters y Setters con @property
En muchos lenguajes se escriben métodos explícitos get_x() y set_x(). Python proporciona un enfoque más limpio: el decorador @property permite exponer un método como si fuera un atributo simple, por lo que el código que lo llama se mantiene legible mientras tienes control total sobre la lectura y escritura.
Getter básico
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsiusLos llamadores leen t.celsius, no t.celsius(). El @property hace invisible la llamada al método:
t = Temperature(25)
print(t.celsius) # 25 — no parentheses neededAñadir un setter con validación
Combina @property con un .setter para validar los valores antes de almacenarlos:
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 es una propiedad calculada de solo lectura — no se define ningún setter, por lo que Python lanza un AttributeError si intentas asignarle un valor.
¿Por qué preferir @property sobre getters/setters simples?
Puedes empezar con un atributo público simple y actualizarlo a una propiedad más tarde sin cambiar ningún código que lo llame:
# 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 = valueLos llamadores que escribieron c.radius = 5 siguen funcionando sin cambios. Solo cambia el comportamiento — ahora se valida el valor.
Para una referencia completa sobre @property incluyendo deleters, consulta Python @property.
Un Ejemplo Completo: Cuenta de Usuario
El siguiente ejemplo muestra los tres niveles de acceso trabajando juntos en una clase 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 lockedObserva:
usernamees público — está bien que cualquiera lo lea._login_attemptsestá protegido — una subclaseThrottledAccountpodría leerlo para implementar una lógica más inteligente.__password_hashy__hash()son privados — la estrategia de almacenamiento de contraseñas es puramente interna. Los llamadores no tienen razón para verla, y si luego cambias a bcrypt solo tienes que modificar esas dos cosas.
Encapsulamiento vs. Otros Pilares de la POO
El encapsulamiento es uno de los cuatro principios de la POO:
| Principio | Definición en una línea |
|---|---|
| Encapsulamiento | Agrupar datos + métodos; ocultar detalles internos |
| Herencia | Permitir que una clase reutilice y extienda otra clase |
| Polimorfismo | Permitir que diferentes tipos respondan a la misma llamada de método |
| Abstracción | Exponer una interfaz simplificada; ocultar la complejidad |
Consulta Python herencia, Python polimorfismo y clases abstractas de Python para los otros pilares.
Referencia Rápida
| Convención | Lo que señala | ¿Lo impone Python? |
|---|---|---|
name | Público — úsalo libremente | No (siempre accesible) |
_name | Protegido — uso interno | No (accesible pero convencionalmente fuera de límites) |
__name | Privado — solo esta clase | Parcialmente — el nombre se modifica a _NombreClase__name |
@property | Acceso de lectura controlado | Sí — hooks getter/setter/deleter |
@name.setter | Acceso de escritura controlado con validación | Sí |