Skip to content

📙 Clase 16 — Encapsulación y properties

Fase 3 · Programación Orientada a Objetos ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • La convención _privado y el name mangling de __doble.
  • @property: atributos calculados y setters que validan.
  • Proteger el estado de tu app para que nadie lo deje inválido.

📖 PARTE TEÓRICA

🔒 1. "Privado" a la Python: convención, no candado

Python no tiene private como Java/C#. Usa convenciones:

PrefijoSignificado¿Bloquea el acceso?
nombrepúblico: úsalo libremente
_nombre"interno: no lo toques desde fuera"No (solo convención)
__nombreactiva name mangling (se renombra)Dificulta, no imposibilita
python
class Cuenta:
    def __init__(self):
        self._saldo = 0        # _ = "esto es interno, respétalo"

class Secreta:
    def __init__(self):
        self.__clave = "x"     # __ = Python lo renombra a _Secreta__clave

s = Secreta()
print(hasattr(s, "__clave"))           # False  ← "desapareció"
print(hasattr(s, "_Secreta__clave"))   # True   ← en realidad se renombró

📌 Filosofía Python: "somos todos adultos". _saldo comunica la intención; el 99% del tiempo basta con el guion bajo simple. __ se reserva para evitar choques de nombres en herencia.

🎛️ 2. @property: métodos disfrazados de atributos

Un @property se lee como atributo pero ejecuta código. Sirve para dos cosas:

a) Atributos calculados (siempre frescos, nunca desincronizados):

python
class Carrito:
    def __init__(self):
        self.items = []

    @property
    def total(self):                   # se calcula al leer
        return sum(i["precio"] for i in self.items)

c = Carrito()
c.items.append({"precio": 120})
print(c.total)        # 120   ← SIN paréntesis: parece atributo, es método

b) Setters que validan (el estado nunca queda inválido):

python
class Cuenta:
    def __init__(self):
        self._saldo = 0                # el dato real, "interno"

    @property
    def saldo(self):                   # el GETTER: leer cuenta.saldo
        return self._saldo

    @saldo.setter
    def saldo(self, valor):            # el SETTER: cuenta.saldo = x pasa por AQUÍ
        if valor < 0:
            raise ValueError("El saldo no puede ser negativo")
        self._saldo = valor

cta = Cuenta()
cta.saldo = 100        # OK (pasó por el setter)
print(cta.saldo)       # 100
cta.saldo = -50        # 💥 ValueError: El saldo no puede ser negativo
cuenta.saldo           ──▶  getter  ──▶  devuelve _saldo
cuenta.saldo = -50     ──▶  setter  ──▶  valida  ──▶  💥 o guarda

🧪 Tip de entrevista: "¿Para qué sirve @property?" → Exponer un método como atributo: permite calcular al leer y validar al asignar sin cambiar la interfaz pública (obj.x sigue siendo obj.x, sin get_x()/set_x() a la Java).


🖥️ EN TU APP DE ESCRITORIO

La encapsulación protege el estado de tu app de quedar inválido por un descuido de la GUI:

python
class App(ctk.CTk):
    def __init__(self):
        super().__init__()
        self._tareas = []              # interno: la GUI no lo toca directo

    @property
    def pendientes(self):              # dato calculado para la barra de estado
        return sum(1 for t in self._tareas if not t.hecha)

    def agregar_tarea(self, texto):    # ÚNICA puerta de entrada (valida)
        texto = texto.strip()
        if not texto:
            raise ValueError("La tarea no puede estar vacía")
        self._tareas.append(Tarea(texto))

# En el callback:
#   self.status.configure(text=f"Pendientes: {self.pendientes}")
Sin encapsularEncapsulado
cualquier callback hace tareas.append("") y mete basurasolo agregar_tarea() inserta, y valida
el contador de pendientes se recalcula "a mano" por ahíself.pendientes siempre correcto

💡 CustomTkinter funciona igual por dentro: por eso configuras con boton.configure(text=...) y lees con .cget("text") — no tocas sus atributos internos directamente.


🗄️ CON BASE DE DATOS (caso de uso)

Un setter con validación es tu última línea de defensa antes del INSERT: nada inválido llega a la BD.

python
class Contacto:
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, valor):
        if "@" not in valor:
            raise ValueError("Email inválido")
        self._email = valor.lower().strip()   # además NORMALIZA antes de guardar

🗄️ Las BD también validan (NOT NULL, UNIQUE, CHECK), pero validar en el objeto da errores más claros y evita viajes inútiles a la BD.


🏋️ EJERCICIOS CON SOLUCIÓN

Ejercicio 1 — Temperatura segura

Crea Nevera con property temperatura que rechace valores fuera de -30 a 10 °C.

Ver solución
python
class Nevera:
    def __init__(self):
        self._temperatura = 4

    @property
    def temperatura(self):
        return self._temperatura

    @temperatura.setter
    def temperatura(self, valor):
        if not -30 <= valor <= 10:
            raise ValueError("Temperatura fuera de rango")
        self._temperatura = valor

n = Nevera()
n.temperatura = -5
print(n.temperatura)    # -5
# n.temperatura = 50    # ValueError: Temperatura fuera de rango

Ejercicio 2 — Atributo calculado

Crea Rectangulo(base, altura) con property area (sin paréntesis al leer).

Ver solución
python
class Rectangulo:
    def __init__(self, base, altura):
        self.base = base
        self.altura = altura

    @property
    def area(self):
        return self.base * self.altura

r = Rectangulo(3, 4)
print(r.area)      # 12
r.base = 10
print(r.area)      # 40   ← siempre fresco, nunca desincronizado

Ejercicio 3 — ¿Qué comunica el guion bajo?

En self._conexion, ¿qué le dice el _ a otro programador (o a tu yo del futuro)?

Ver solución
python
# "Este atributo es INTERNO de la clase: no lo leas ni modifiques desde fuera.
#  Usa los métodos/properties públicos." No es un candado técnico — es un contrato
#  de equipo que Python confía en que respetes.

❓ Preguntas y respuestas (autoevaluación)

1. ¿Python tiene atributos privados de verdad?

No. _nombre es convención ("no tocar") y __nombre solo renombra (name mangling_Clase__nombre).

2. ¿Qué dos superpoderes da @property?

Atributos calculados al leer y validación al asignar (con @x.setter) — manteniendo sintaxis de atributo.

3. ¿Cómo se lee un property: c.total o c.total()?

c.total, sin paréntesis: se usa como atributo aunque ejecute código.

4. ¿Dónde valida mejor una app con BD: en el objeto o en la BD?

En ambos: el objeto da errores claros e inmediatos (setter); la BD garantiza integridad (UNIQUE, NOT NULL) contra cualquier camino que se salte al objeto.


📎 Apuntes relacionados

➡️ Siguiente

Clase 17 · Métodos dunder — que tus objetos se impriman, comparen y midan como los nativos.