IF0100 - Programación OO II

Unidad 1: Programación Orientada a Objetos

Clase 4: Encapsulamiento

Jueves, 19 de febrero de 2026 Semana 3 - Jueves (60 minutos) 30 min Teoría 30 min Práctica

E1: Examen POO - Jueves 26/02/2026 (7 días)

Objetivos de Aprendizaje

Al finalizar esta clase, serás capaz de:

  • Comprender el concepto de encapsulamiento
  • Usar convenciones _ (protegido) y __ (privado)
  • Crear getters y setters con @property
  • Validar datos en los setters
  • Aplicar encapsulamiento al modelo Usuario

¿Qué es el Encapsulamiento? (10 min)

El encapsulamiento es ocultar los detalles internos de una clase y exponer solo lo necesario. Protege los datos del acceso directo.

Analogía: Un coche tiene un motor, pero no puedes tocarlo directamente. Solo interactúas con el volante, pedales y botones. El motor está "encapsulado".

Problema sin Encapsulamiento

class CuentaBancaria:
    def __init__(self, saldo):
        self.saldo = saldo  # Acceso público

cuenta = CuentaBancaria(1000)
cuenta.saldo = -5000  # ¡Cualquiera puede modificar!
print(cuenta.saldo)   # -5000 (¡inválido!)

Solución con Encapsulamiento

class CuentaBancaria:
    def __init__(self, saldo_inicial):
        self._saldo = saldo_inicial  # Atributo "protegido"
    
    def depositar(self, cantidad):
        if cantidad > 0:
            self._saldo += cantidad
    
    def retirar(self, cantidad):
        if 0 < cantidad <= self._saldo:
            self._saldo -= cantidad
    
    def obtener_saldo(self):
        return self._saldo

cuenta = CuentaBancaria(1000)
cuenta.depositar(500)
cuenta.retirar(200)
print(cuenta.obtener_saldo())  # 1300

# El saldo solo se modifica por métodos controlados
# cuenta._saldo = -5000  # No deberías hacer esto

Convenciones en Python (15 min)

Python no tiene verdaderos atributos privados como Java o C++. Usa convenciones:

Convención Significado Ejemplo
nombre Público self.nombre (puede usarse libremente)
_nombre Protegido self._nombre (no tocar desde fuera)
__nombre Privado (name mangling) self.__nombre (difícil de acceder)
class Ejemplo:
    def __init__(self):
        self.publico = "Todos pueden verme"
        self._protegido = "No deberías tocarme desde fuera"
        self.__privado = "Difícil de acceder desde fuera"

obj = Ejemplo()

print(obj.publico)       # ✅ Funciona
print(obj._protegido)    # ⚠️ Funciona pero no deberías
# print(obj.__privado)   # ❌ Error! No existe

# Para acceder al privado (no recomendado):
print(obj._Ejemplo__privado)  # "Difícil de acceder..."
Regla de Oro: En Python, "somos todos adultos". No hay atributos verdaderamente privados. La convención _ indica "no tocar" y debe respetarse.

@property - Getters y Setters (20 min)

El decorador @property permite crear atributos "inteligentes" que ejecutan código al leer o escribir.

Getter con @property

class Circulo:
    def __init__(self, radio):
        self._radio = radio
    
    @property
    def radio(self):
        """Getter: se llama al leer circulo.radio"""
        print("Obteniendo radio...")
        return self._radio
    
    @property
    def area(self):
        """Getter calculado: no se almacena, se calcula"""
        return 3.14159 * self._radio ** 2

# Uso
c = Circulo(5)
print(c.radio)    # Llama al getter
print(c.area)     # Calcula automáticamente (78.54)

Setter con @atributo.setter

class Circulo:
    def __init__(self, radio):
        self._radio = radio
    
    @property
    def radio(self):
        """Getter"""
        return self._radio
    
    @radio.setter
    def radio(self, valor):
        """Setter: se llama al asignar circulo.radio = valor"""
        if valor <= 0:
            raise ValueError("El radio debe ser positivo")
        self._radio = valor
    
    @property
    def area(self):
        return 3.14159 * self._radio ** 2

# Uso
c = Circulo(5)
print(c.radio)    # 5

c.radio = 10      # Llama al setter
print(c.area)     # 314.159

# c.radio = -5    # ❌ ValueError: El radio debe ser positivo

Ejemplo: Usuario con Email Validado

class Usuario:
    def __init__(self, username, email):
        self._username = username
        self._email = None  # Inicializa vacío
        self.email = email  # Usa el setter para validar
    
    @property
    def username(self):
        return self._username
    
    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, valor):
        if "@" not in valor:
            raise ValueError("Email inválido: debe contener @")
        self._email = valor

# Uso
u = Usuario("juan", "juan@email.com")
print(u.email)           # juan@email.com

u.email = "nuevo@email.com"  # ✅ Válido
print(u.email)

# u.email = "invalido"    # ❌ ValueError: Email inválido...

Ejercicio: Usuario Encapsulado (25 min)

Mejora la clase Usuario con encapsulamiento completo.

Requisitos

  • username: solo lectura (no setter), validar ≥3 caracteres
  • email: getter/setter con validación de @
  • edad: getter/setter, validar 0-120 años
  • _activo: protegido, métodos activar()/desactivar()
# usuario_encapsulado.py
class Usuario:
    """Usuario con encapsulamiento completo."""
    
    def __init__(self, username, email, edad=None):
        # Validar username en el constructor
        if len(username) < 3:
            raise ValueError("Username debe tener al menos 3 caracteres")
        
        self._username = username
        self._email = None
        self._edad = None
        self._activo = True
        
        # Usar setters para validar
        self.email = email
        if edad is not None:
            self.edad = edad
    
    @property
    def username(self):
        """Username es de solo lectura (no hay setter)."""
        return self._username
    
    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, valor):
        if "@" not in valor or "." not in valor.split("@")[1]:
            raise ValueError("Email inválido")
        self._email = valor
    
    @property
    def edad(self):
        return self._edad
    
    @edad.setter
    def edad(self, valor):
        if not isinstance(valor, int):
            raise ValueError("Edad debe ser un número entero")
        if not (0 <= valor <= 120):
            raise ValueError("Edad debe estar entre 0 y 120")
        self._edad = valor
    
    @property
    def activo(self):
        return self._activo
    
    def activar(self):
        self._activo = True
    
    def desactivar(self):
        self._activo = False
    
    def __str__(self):
        return f"@{self._username} ({self._email})"


# === PRUEBAS ===
if __name__ == "__main__":
    # Crear usuario válido
    try:
        u = Usuario("juan123", "juan@email.com", 25)
        print(f"✅ Creado: {u}")
        print(f"   Edad: {u.edad}")
        print(f"   Activo: {u.activo}")
    except ValueError as e:
        print(f"❌ Error: {e}")
    
    # Intentar modificar username (solo lectura)
    print("\n--- Username es solo lectura ---")
    try:
        u.username = "otro"  # Esto debería fallar
    except AttributeError as e:
        print(f"✅ Correcto: No se puede modificar username")
    
    # Cambiar email válido
    print("\n--- Cambiar email ---")
    u.email = "nuevo@email.com"
    print(f"✅ Nuevo email: {u.email}")
    
    # Intentar email inválido
    print("\n--- Email inválido ---")
    try:
        u.email = "sin-arroba"
    except ValueError as e:
        print(f"✅ Correcto: {e}")
    
    # Edad inválida
    print("\n--- Edad inválida ---")
    try:
        u.edad = 150
    except ValueError as e:
        print(f"✅ Correcto: {e}")
    
    # Activar/Desactivar
    print("\n--- Estado ---")
    u.desactivar()
    print(f"Desactivado: Activo = {u.activo}")
    u.activar()
    print(f"Activado: Activo = {u.activo}")
Conexión con TaskFlow: El encapsulamiento protege los datos del usuario. El email no puede ser cualquier texto, la edad debe ser razonable, y el username no cambia (es la identidad del usuario).

Resumen

  • Encapsulamiento: Ocultar detalles internos, exponer solo lo necesario
  • _ (underscore): Convención para atributos protegidos
  • __ (dunder): Atributos privados con name mangling
  • @property: Define getters elegantes
  • @atributo.setter: Define setters con validación
Para el examen E1: Practica crear clases con @property y setters que validen datos. Es fundamental para el modelo de dominio.
← Anterior: Clases y Objetos
Clase 4 de 25
Siguiente: Herencia y Polimorfismo →