W3docs

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 = -9999999

El 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:

  1. Integridad de datos — la lógica de validación en un solo lugar, aplicada cada vez.
  2. 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.
  3. 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:

PrefijoEjemploNivel de accesoSignificado
Sin prefijobalancePúblicoDestinado a ser usado por cualquiera
Guion bajo simple __balanceProtegidoPara uso interno y subclases; utilizable externamente pero no recomendado
Doble guion bajo ____pinPrivadoSolo 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 method

No 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.__pin

Desde 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)  # 2

Puedes 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__secret

Ambos 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._celsius

Los 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 needed

Añ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 + 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 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 = value

Los 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 locked

Observa:

  • username es público — está bien que cualquiera lo lea.
  • _login_attempts está protegido — una subclase ThrottledAccount podría leerlo para implementar una lógica más inteligente.
  • __password_hash y __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:

PrincipioDefinición en una línea
EncapsulamientoAgrupar datos + métodos; ocultar detalles internos
HerenciaPermitir que una clase reutilice y extienda otra clase
PolimorfismoPermitir que diferentes tipos respondan a la misma llamada de método
AbstracciónExponer una interfaz simplificada; ocultar la complejidad

Consulta Python herencia, Python polimorfismo y clases abstractas de Python para los otros pilares.

Referencia Rápida

ConvenciónLo que señala¿Lo impone Python?
namePúblico — úsalo librementeNo (siempre accesible)
_nameProtegido — uso internoNo (accesible pero convencionalmente fuera de límites)
__namePrivado — solo esta claseParcialmente — el nombre se modifica a _NombreClase__name
@propertyAcceso de lectura controladoSí — hooks getter/setter/deleter
@name.setterAcceso de escritura controlado con validación

Práctica

Práctica
What does a single leading underscore (e.g. `_balance`) signal in Python?
What does a single leading underscore (e.g. `_balance`) signal in Python?
Was this page helpful?