← Regresar a Unidad 1
🎓 Programación Orientada a Objetos

Guía Didáctica
Sistema de Gestión Hotelera

Análisis línea por línea con sintaxis, ejecución en contexto y glosario pedagógico.

1

La Clase Habitacion — El Molde

Clase base
Python
class Habitacion:  # Define la clase Habitacion
    def __init__(self, numero, tipo, precio_noche):  # Constructor: inicializa el objeto con los datos recibidos
        self.numero       = numero  # Guarda numero como atributo de esta instancia
        self.tipo         = tipo  # Guarda tipo como atributo de esta instancia
        self.precio_noche = precio_noche  # Guarda precio_noche como atributo de esta instancia
        self.disponible   = True   # toda habitación nace libre

🔬 Análisis de Sintaxis

  • class Palabra reservada que define la estructura. Usa CamelCase por convención de Python (PEP 8).
  • __init__ Constructor del objeto. Los dobles guiones indican que es un dunder method (método especial de Python).
  • self El "yo" del objeto. Conecta cada dato con la instancia específica y no con otra.
  • = True Valor booleano por defecto; todas las habitaciones nuevas nacen disponibles.

⚙️ Ejecución en el Contexto

Cuando escribes hab101 = Habitacion(101, "Suite", 350):

  • Python reserva un espacio en memoria exclusivo para hab101.
  • Almacena 101 en self.numero, "Suite" en self.tipo.
  • Fija self.disponible = True automáticamente, sin que el programador tenga que indicarlo.

📖 Glosario

def
"Define" — indica que sigue una función o método.
self
Referencia al objeto actual dentro de sus propios métodos.
Atributo
Variable que pertenece a un objeto (ej. self.tipo).
True
Valor booleano verdadero. En Python se escribe con mayúscula inicial.
💡 La clase no crea objetos; es el plano arquitectónico. Cada vez que escribes Habitacion(...) construyes una instancia diferente en memoria.
🧩 ¿Por qué estos parámetros? — Anatomía del constructor
Parámetro 1
numero
int · identificador único
101 · 202 · 305
Cada habitación debe poder distinguirse de las demás. Es su "cédula". Sin él no podrías buscar ni referenciar una habitación específica.
Parámetro 2
tipo
str · categoría
"Suite" · "Doble"
Define qué clase de habitación es. Permite filtrar ofertas, aplicar precios distintos y mostrar información adecuada al cliente.
Parámetro 3
precio_noche
float · tarifa
350.00 · 180.50
El costo varía entre habitaciones. Necesitamos almacenarlo para calcular el total de una reservación según los días de estancia.
Atributo interno
disponible
bool · estado · NO es parámetro
True → False
No viene del exterior porque siempre empieza en True. El constructor lo fija solo. Es una regla de negocio invariable al crear.
🏗️ ¿Por qué se necesita __init__? — El constructor
📦

Sin constructor → objeto vacío

Si no defines __init__, el objeto se crea pero sin ningún dato. Sería como entregarle una habitación sin número, sin tipo ni precio. Inútil para el hotel.

Se ejecuta automáticamente

Python llama a __init__ en el momento exacto de la creación: Habitacion(101, ...). No necesitas llamarlo manualmente. Es el "arranque" del objeto.

🔒

Garantiza consistencia

Obliga a que cada objeto nazca con los datos mínimos necesarios. Si intentas crear Habitacion() sin argumentos, Python lanza un error inmediato.

🎯

Un constructor por clase

En Python solo puede existir un __init__ por clase (a diferencia de otros lenguajes). Si defines dos, el segundo sobreescribe al primero silenciosamente.

🪪 ¿Qué es self y qué pasa si no lo pones?

✅ Con self — correcto

def __init__(self, numero):  # Constructor: inicializa el objeto con los datos recibidos
    self.numero = numero  # Guarda numero como atributo de esta instancia

hab101 = Habitacion(101)  # Asigna un valor inicial o calculado a hab101
hab202 = Habitacion(202)  # Asigna un valor inicial o calculado a hab202
print(hab101.numero) # 101
print(hab202.numero) # 202
Cada objeto guarda su propio número. self es la dirección de memoria del objeto actual — distingue hab101 de hab202.

❌ Sin self — error en runtime

def __init__(numero):  # falta self
    numero = numero    # variable local

hab101 = Habitacion(101)  # Asigna un valor inicial o calculado a hab101
# TypeError: __init__() takes 1
# positional argument but 2 were given
Python pasa el objeto automáticamente como primer argumento. Sin self declarado, los argumentos se desplazan y Python lanza TypeError.

📍 Memoria RAM — dos objetos, datos separados

hab101 @ 0x7f1a
self.numero = 101
self.tipo = "Suite"
self.precio_noche = 350
self.disponible = True
hab202 @ 0x7f2b
self.numero = 202
self.tipo = "Doble"
self.precio_noche = 180
self.disponible = True

self garantiza que numero = 101 va al espacio de hab101 y no al de hab202.

🔗 self.nombre = nombre — ¿Por qué el mismo nombre? ¿Qué pasa si pongo otro?

La expresión self.numero = numero tiene dos espacios distintos: el lado izquierdo vive en el objeto; el derecho es la variable local del parámetro. Son mundos separados que el = une.

Atributo del objeto Parámetro recibido ¿Funciona? Notas
self.numero numero ✅ Sí Convención recomendada. Mismo nombre = fácil de leer y mantener.
self.numero n ✅ Sí self.numero = n funciona perfecto. El atributo se llama numero pero el parámetro se llama n. Confuso pero válido.
self.num_hab numero ✅ Sí El atributo puede tener nombre diferente. Pero ahora debes recordar usar .num_hab en todo el código.
self.numero numero ❌ Error lógico Si escribes solo numero = numero sin self., estás asignando la variable local a sí misma. No se guarda nada en el objeto. Al salir del método, el dato se pierde.
— (ninguno) numero ❌ No accesible Si no haces self.algo = numero, el parámetro existe solo dentro de __init__ y desaparece. No podrás hacer hab101.numero después.

✅ self.x = x — dato persiste

def __init__(self, numero):  # Constructor: inicializa el objeto con los datos recibidos
    self.numero = numero  # ← guardado en objeto

hab = Habitacion(101)  # Asigna un valor inicial o calculado a hab
print(hab.numero)  # ✅ 101

❌ sin self. — dato se pierde

def __init__(self, numero):  # Constructor: inicializa el objeto con los datos recibidos
    numero = numero  # ← solo variable local

hab = Habitacion(101)  # Asigna un valor inicial o calculado a hab
print(hab.numero)  # ❌ AttributeError
A

¿Qué es una Clase, un Atributo y un Método? — Visualmente

Fundamentos POO
🏨 La Clase = el plano del hotel
Habitacion

🔵 self.numero int · identidad única
🔵 self.tipo str · categoría
🔵 self.precio_noche float · tarifa
🟡 self.disponible bool · estado actual


🟢 __init__(self, ...) construye el objeto
🟢 reservar(self) bloquea la habitación
🟢 liberar(self) libera al hacer checkout
🟢 calcular_costo(self, dias) retorna precio × días
🟢 __str__(self) texto legible del objeto
Clase = el molde (solo existe una)
Atributo = dato que vive en el objeto
Método = acción que puede ejecutar
Estado = atributo que cambia en el tiempo

🏗️ CLASE

Es la definición, el plano. No ocupa datos reales de habitaciones. Es como el diseño arquitectónico de un cuarto — no es el cuarto.

class Habitacion: ...

Solo existe una en el programa.

📦 ATRIBUTO

Una variable que pertenece al objeto. Guarda un dato específico de esa instancia. Dos habitaciones pueden tener el mismo atributo pero con valores distintos.

hab101.numero → 101
hab202.numero → 202

Existen tantos como instancias.

⚡ MÉTODO

Una función que vive dentro de la clase. Siempre recibe self para poder leer o modificar los atributos del objeto que lo llama.

hab101.reservar()
hab101.calcular_costo(3)

Se comparte entre todas las instancias.

📚 Más ejemplos de atributos y métodos en las clases del Hotel
🛏️ Habitacion
# Atributos
self.numero        # 101
self.tipo          # "Suite"
self.precio_noche  # 350.0
self.disponible    # True/False
self.piso          # 3
self.capacidad     # 2 personas

# Métodos
.reservar()              # bloquea
.liberar()               # disponible=True
.calcular_costo(dias)     # precio*dias
.aplicar_descuento(pct)   # precio*0.9
.__str__()               # texto legible
👤 Cliente
# Atributos
self.nombre        # "Ana López"
self.email         # "ana@mail.com"
self.telefono      # "9999-0000"
self.reservaciones # []
self.puntos_fide   # 120

# Métodos
.agregar_reservacion(r)   # guarda historial
.ver_historial()          # lista reservas
.acumular_puntos(monto)   # +puntos
.es_cliente_vip()         # puntos>500
.__str__()               # texto legible
📋 Reservacion
# Atributos
self.cliente       # objeto Cliente
self.habitacion    # objeto Habitacion
self.fecha_entrada # "2026-06-01"
self.fecha_salida  # "2026-06-05"
self.estado        # "activa"

# Métodos
.confirmar()              # activa todo
.cancelar()               # libera hab.
.calcular_noches()        # días entre fechas
.total_a_pagar()          # precio final
.__str__()               # resumen
🏨 Hotel
# Atributos
self.nombre        # "Grand Plaza"
self.habitaciones  # [hab101, ...]
self.clientes      # [cli1, ...]
self.reservaciones # [res1, ...]

# Métodos
.agregar_habitacion(h)    # registra cuarto
.buscar_disponibles()     # filtra libres
.registrar_cliente(c)     # agrega cliente
.crear_reservacion(...)   # orquesta todo
.reporte_ocupacion()      # % ocupado
🧠 Regla de oro: los atributos responden "¿qué soy / qué tengo?" y los métodos responden "¿qué puedo hacer?". Una habitación tiene precio pero puede calcular el costo total.
B

¿Cómo piensa Python al ejecutar el programa?

Modelo mental
1

📖 Lee la definición de la clase

Python lee class Habitacion: y guarda la plantilla en memoria. No crea ningún objeto todavía. Solo registra: "existe un tipo llamado Habitacion con estos atributos y métodos".

class Habitacion: # ← Python lee y guarda el molde
    def __init__(self, numero, tipo, precio_noche): ...
# ↑ En este punto NO hay ninguna habitación real aún
2

🔨 Instanciación — crea el objeto real

Cuando escribes hab101 = Habitacion(101, "Suite", 350), Python hace 3 cosas internas: reserva espacio en RAM, llama a __init__ automáticamente y pasa el objeto nuevo como self.

hab101 = Habitacion(101, "Suite", 350)
# Internamente Python hace:
# 1. obj = object.__new__(Habitacion) ← reserva RAM
# 2. Habitacion.__init__(obj, 101, "Suite", 350) ← llena datos
# 3. hab101 = obj ← apunta la variable al objeto
3

🗃️ Los atributos viven en el objeto, el método en la clase

Sorpresa: los atributos se guardan en cada objeto, pero los métodos viven en la clase (solo una copia). Cuando llamas hab101.reservar(), Python busca reservar en la clase y automáticamente pasa hab101 como self.

hab101.reservar() # tú escribes esto
# Python ejecuta internamente:
Habitacion.reservar(hab101) # ← así lo procesa
4

🔗 Encadenamiento entre objetos

Cuando Reservacion llama a self.habitacion.reservar(), Python sigue la cadena: busca habitacion en la reservación, luego busca reservar en la clase Habitacion, y finalmente ejecuta con ese objeto como self.

reserva.confirmar()
# → self.habitacion.reservar()
# → Python: ¿qué es self.habitacion? → objeto hab101
# → Python: ¿tiene método reservar? → sí, en clase Habitacion
# → Habitacion.reservar(hab101) ← ejecuta
5

🗑️ Cuando el objeto ya no se necesita

Python tiene un garbage collector. Cuando ninguna variable apunta a un objeto, lo elimina de memoria automáticamente. No necesitas destruirlo manualmente (a diferencia de C++ o C).

hab101 = None # ya nadie apunta al objeto anterior
# Python lo elimina de RAM automáticamente
# (garbage collector en acción)
⚙️ Resumen del modelo mental: Clase = molde en códigoInstancia = objeto en RAMMétodo = función de la clase que recibe el objeto como selfAtributo = dato guardado en el objeto.
C

Atributos Públicos, Protegidos y Privados

Control de acceso
Tipo Sintaxis Ejemplo Hotel ¿Accesible desde fuera? Convención
Público self.nombre self.numero, self.tipo ✅ Sí, directo Sin guión bajo. Cualquiera puede leerlo y modificarlo.
Protegido self._nombre self._descuento, self._historial ⚠️ Sí, pero "no deberías" Un guión bajo. Es una convención: "uso interno, maneja con cuidado". Python no lo impide.
Privado self.__nombre self.__id_interno, self.__pin ❌ No directamente — Python lo renombra Doble guión bajo. Python aplica name mangling: renombra a _Clase__nombre. Protege de acceso accidental externo.
💻 Código comparativo — los tres tipos en la clase Habitacion
Python
class Habitacion:  # Define la clase Habitacion
    def __init__(self, numero, tipo, precio_noche):  # Constructor: inicializa el objeto con los datos recibidos

        # ── PÚBLICO: cualquiera puede leer y cambiar ──
        self.numero        = numero         # acceso libre
        self.tipo          = tipo  # Guarda tipo como atributo de esta instancia

        # ── PROTEGIDO: convención "uso interno" ──
        self._precio_noche = precio_noche    # un guión bajo
        self._descuento    = 0  # Guarda _descuento como atributo protegido de la instancia

        # ── PRIVADO: Python lo renombra internamente ──
        self.__id_interno  = id(self)        # doble guión bajo
        self.__disponible  = True  # Guarda __disponible como atributo privado de la instancia

    # Método para acceder al privado desde DENTRO (getter)
    def get_disponible(self):  # Getter: devuelve el valor de disponible
        return self.__disponible  # Retorna el valor calculado al lugar donde se llamó este método

    # Método para cambiar el privado desde DENTRO (setter)
    def set_precio(self, nuevo_precio):  # Setter: valida y actualiza el atributo precio
        if nuevo_precio > 0:  # Verifica nuevo_precio > 0 antes de continuar
            self._precio_noche = nuevo_precio  # Guarda _precio_noche como atributo protegido de la instancia
        else:  # Se ejecuta si la condición anterior no se cumplió
            print("❌ El precio debe ser positivo.")  # Muestra en consola el resultado de esta operación
🔓 ¿Cómo acceder a cada tipo desde fuera de la clase?

🟢 Público — acceso directo

hab = Habitacion(101, "Suite", 350)  # Asigna un valor inicial o calculado a hab

# Leer
print(hab.numero)     # 101 ✅
print(hab.tipo)       # "Suite" ✅

# Modificar
hab.tipo = "Doble"  # ✅ permitido
✅ Sin restricciones. Cualquier parte del código puede leerlo o cambiarlo. Úsalo cuando el dato es información general del objeto.

🟡 Protegido — acceso con advertencia

hab = Habitacion(101, "Suite", 350)  # Asigna un valor inicial o calculado a hab

# Leer (funciona, pero no es ideal)
print(hab._precio_noche)  # 350 ⚠️

# Modificar (funciona, pero es tu riesgo)
hab._precio_noche = -50  # ⚠️ sin validación

# Lo correcto: usar el setter
hab.set_precio(400)       # ✅ validado
⚠️ Python permite el acceso pero la convención dice "no lo hagas desde fuera". Usa los métodos get/set para mantener control y validación.

🔴 Privado — name mangling

hab = Habitacion(101, "Suite", 350)

# Acceso directo → AttributeError
hab.__disponible     # ❌ AttributeError

# Python lo renombró a:
hab._Habitacion__disponible  # ✅ funciona
# (pero nunca deberías usarlo así)

# Forma correcta: usar el getter
hab.get_disponible()  # ✅ True
🔴 Python renombra __x a _Clase__x (name mangling). Esto evita que subclases o código externo lo sobreescriban por accidente. Accede siempre con métodos.
🤔 ¿Cuándo usar cada uno? — Guía práctica para el Hotel
🟢
Público — datos de "tarjeta de presentación"
Usa público para datos que cualquier parte del sistema necesita leer: numero, tipo, nombre. Son la "identidad" del objeto.
🟡
Protegido — datos que cambian con lógica de negocio
Usa protegido para datos que pueden leerse fuera pero deben modificarse solo a través de métodos con validación: _precio_noche, _descuento. Una subclase de HabitacionVIP sí podría usarlos.
🔴
Privado — datos críticos del sistema
Usa privado para datos que nadie debe tocar directamente: __id_interno, __pin_acceso, __numero_tarjeta. Solo el objeto mismo los manipula mediante sus propios métodos.
🔀 Name Mangling — cómo Python transforma el nombre privado
Escribes en código
self.__disponible
Python lo almacena como
_Habitacion__disponible
Resultado
hab.__disponible → ❌
hab._Habitacion__disponible → ✅
🛡️ El nombre mangling protege que si creas una subclase HabitacionVIP(Habitacion) y defines self.__disponible ahí, no colisiona con el de la clase padre porque tendrá nombre _HabitacionVIP__disponible.
💡 Python no tiene acceso "realmente privado" como Java o C++. La filosofía de Python es: "somos adultos responsables". Los guiones bajos son contratos de convención, no barreras técnicas infranqueables. El doble guión bajo es lo más cercano a privado real.
D

Plantillas de Autollenado + Snippets de VS Code

Práctica estructurada
📋 Plantilla Maestra — Los [corchetes] son lo que debes reemplazar
plantilla.py PLANTILLA
class [NombreDeLaClase]: """[Descripción breve del objeto]""" def __init__(self, [param1], [param2]): # ── ATRIBUTOS: lo que el objeto TIENE ── self.[param1] = [param1] self.[param2] = [param2] self.[estado] = [ValorPorDefecto] # ── MÉTODO: lo que el objeto HACE ── def [nombre_metodo](self): [lógica de la acción] return [resultado] # ── REPRESENTACIÓN ── def __str__(self): return f"[texto con {self.param1}]"
habitacion.py EJEMPLO LLENADO
class Habitacion: """Representa un cuarto en el hotel""" def __init__(self, numero, tipo): # ── ATRIBUTOS: lo que el objeto TIENE ── self.numero = numero self.tipo = tipo self.disponible = True # ── MÉTODO: lo que el objeto HACE ── def reservar(self): if self.disponible: self.disponible = False return f"Hab {self.numero} reservada" # ── REPRESENTACIÓN ── def __str__(self): return f"Hab {self.numero} — {self.tipo}"
[ejemplo] → texto que debes reemplazar
[opcional] → puedes omitirlo
[valor] → valor o resultado concreto
llenado → ya fue reemplazado ✅
📐 Guía de llenado — Componente por componente
📦 Atributo
self.nombre_dato = valor

Siempre dentro de __init__. El nombre izquierdo queda en el objeto para siempre; el derecho es el parámetro que llegó del exterior.

✅ self.precio = precio
✅ self.activo = True
✅ self.items = []
⚠️ Sin self. el dato existe solo durante __init__ y luego desaparece.
⚡ Método
def nombre_accion(self, extra=None):

self es obligatorio como primer parámetro — permite acceder a los atributos. Los parámetros extra son opcionales.

✅ def reservar(self):
✅ def calcular(self, dias):
✅ def __str__(self):
⚠️ Sin self Python lo trata como función suelta, no como método del objeto.
🚀 Instanciar (crear objeto)
variable = NombreClase(val1, val2)

Crea un objeto real en RAM. Los valores que pasas entre paréntesis van directo a los parámetros del __init__ (excepto self, que Python pasa automáticamente).

✅ hab101 = Habitacion(101, "Suite")
✅ c1 = Cliente("Ana", "ana@x.com")
⚠️ Número de argumentos debe coincidir con los parámetros del __init__.
🛒 Ejemplo de llenado aplicado — Sistema de Ventas: Producto
SecciónCódigo¿Qué hace?
Clase class Producto: Define el concepto general de producto.
Atributos self.nombre
self.stock
self.precio
Guarda nombre, unidades disponibles y precio de cada producto.
Método def vender(self) Resta 1 al stock si hay unidades disponibles.
Método def reabastecer(self, cantidad) Suma unidades al stock cuando llega mercancía.
Invocación p1 = Producto("Laptop", 10, 999) Crea una laptop real con 10 unidades a $999.
producto.py — plantilla llenada
class Producto: """Artículo en el sistema de ventas""" def __init__(self, nombre, stock, precio): self.nombre = nombre # "Laptop" self.stock = stock # 10 self.precio = precio # 999.0 self.activo = True # siempre activo al crear def vender(self): if self.stock > 0: self.stock -= 1 return f"✅ Venta OK. Stock: {self.stock}" return "❌ Sin stock disponible." def reabastecer(self, cantidad): self.stock += cantidad return f"📦 +{cantidad}. Stock: {self.stock}" def __str__(self): return f"{self.nombre} | ${self.precio} | Stock: {self.stock}" # ── Invocación ── p1 = Producto("Laptop", 10, 999) print(p1.vender()) # ✅ Venta OK. Stock: 9 print(p1.reabastecer(5)) # 📦 +5. Stock: 14 print(p1) # Laptop | $999 | Stock: 14
⚡ Snippets de VS Code — Escribe una palabra, obtén toda la estructura
1

Abrir configuración de snippets

En VS Code: File → Preferences → Configure User Snippets (o Ctrl + Shift + P y escribe "snippets")

2

Seleccionar lenguaje

En el menú desplegable selecciona python. Se abre el archivo python.json.

3

Pegar el snippet

Copia el código de abajo y pégalo dentro de las llaves { } del archivo JSON.

4

Usar en cualquier .py

En un archivo Python escribe el prefix del snippet y presiona Tab o Enter.

Cómo funciona:
clase
⇥ Tab
🎉 VS Code escribe toda la clase automáticamente con Tab stops para que saltes entre los campos
📄 Snippet 1 — Clase básica (prefix: "clase")
"Clase Basica": {  # Continúa la lógica del ejemplo paso a paso
  "prefix": "clase",  # Continúa la lógica del ejemplo paso a paso
  "body": [  # Continúa la lógica del ejemplo paso a paso
    "class ${1:NombreClase}:",  # Continúa la lógica del ejemplo paso a paso
    "    \"\"\"${2:Descripcion de la clase}\"\"\"",  # Continúa la lógica del ejemplo paso a paso
    "",  # Continúa la lógica del ejemplo paso a paso
    "    def __init__(self, ${3:param}):",  # Continúa la lógica del ejemplo paso a paso
    "        self.${3:param} = ${3:param}",  # Continúa la lógica del ejemplo paso a paso
    "        self.${4:estado} = ${5:True}",  # Continúa la lógica del ejemplo paso a paso
    "",  # Continúa la lógica del ejemplo paso a paso
    "    def ${6:metodo}(self):",  # Continúa la lógica del ejemplo paso a paso
    "        ${7:pass}",  # Continúa la lógica del ejemplo paso a paso
    "",  # Continúa la lógica del ejemplo paso a paso
    "    def __str__(self):",  # Continúa la lógica del ejemplo paso a paso
    "        return f\"${1:NombreClase}: {self.${3:param}}\""  # Continúa la lógica del ejemplo paso a paso
  ],  # Continúa la lógica del ejemplo paso a paso
  "description": "Clase con init, metodo y __str__"  # Continúa la lógica del ejemplo paso a paso
}  # Cierra la estructura abierta en las líneas anteriores
📄 Snippet 2 — Herencia (prefix: "clasehereda")
"Clase con Herencia": {  # Continúa la lógica del ejemplo paso a paso
  "prefix": "clasehereda",  # Continúa la lógica del ejemplo paso a paso
  "body": [  # Continúa la lógica del ejemplo paso a paso
    "class ${1:Hija}(${2:Padre}):",  # Continúa la lógica del ejemplo paso a paso
    "    \"\"\"${3:Descripcion}\"\"\"",  # Continúa la lógica del ejemplo paso a paso
    "",  # Continúa la lógica del ejemplo paso a paso
    "    def __init__(self, ${4:param_padre}, ${5:param_extra}):",  # Continúa la lógica del ejemplo paso a paso
    "        super().__init__(${4:param_padre})",  # Continúa la lógica del ejemplo paso a paso
    "        self.${5:param_extra} = ${5:param_extra}",  # Continúa la lógica del ejemplo paso a paso
    "",  # Continúa la lógica del ejemplo paso a paso
    "    def __str__(self):",  # Continúa la lógica del ejemplo paso a paso
    "        return f\"${1:Hija}: {self.${5:param_extra}}\""  # Continúa la lógica del ejemplo paso a paso
  ],  # Continúa la lógica del ejemplo paso a paso
  "description": "Clase que hereda de otra con super()"  # Continúa la lógica del ejemplo paso a paso
}  # Cierra la estructura abierta en las líneas anteriores
📄 Snippet 3 — Getter/Setter privado (prefix: "getset")
"Getter Setter Privado": {  # Continúa la lógica del ejemplo paso a paso
  "prefix": "getset",  # Continúa la lógica del ejemplo paso a paso
  "body": [  # Continúa la lógica del ejemplo paso a paso
    "@property",  # Continúa la lógica del ejemplo paso a paso
    "def ${1:nombre}(self):",  # Continúa la lógica del ejemplo paso a paso
    "    return self.__${1:nombre}",  # Continúa la lógica del ejemplo paso a paso
    "",  # Continúa la lógica del ejemplo paso a paso
    "@${1:nombre}.setter",  # Continúa la lógica del ejemplo paso a paso
    "def ${1:nombre}(self, valor):",  # Continúa la lógica del ejemplo paso a paso
    "    if ${2:condicion}:",  # Continúa la lógica del ejemplo paso a paso
    "        self.__${1:nombre} = valor",  # Continúa la lógica del ejemplo paso a paso
    "    else:",  # Continúa la lógica del ejemplo paso a paso
    "        raise ValueError(f\"${3:Mensaje de error}\")"  # Continúa la lógica del ejemplo paso a paso
  ],  # Continúa la lógica del ejemplo paso a paso
  "description": "Property con getter y setter validado"  # Continúa la lógica del ejemplo paso a paso
}  # Cierra la estructura abierta en las líneas anteriores
📄 Snippet 4 — Clase completa Hotel (prefix: "classpro")
"Clase Pro Hotel": {  # Continúa la lógica del ejemplo paso a paso
  "prefix": "classpro",  # Continúa la lógica del ejemplo paso a paso
  "body": [  # Continúa la lógica del ejemplo paso a paso
    "class ${1:Nombre}:",  # Continúa la lógica del ejemplo paso a paso
    "    \"\"\"${2:Descripcion}\"\"\"",  # Continúa la lógica del ejemplo paso a paso
    "",  # Continúa la lógica del ejemplo paso a paso
    "    def __init__(self, ${3:param1}, ${4:param2}):",  # Continúa la lógica del ejemplo paso a paso
    "        self.${3:param1} = ${3:param1}",  # Continúa la lógica del ejemplo paso a paso
    "        self.${4:param2} = ${4:param2}",  # Continúa la lógica del ejemplo paso a paso
    "        self._${5:protegido} = ${6:valor}",  # Continúa la lógica del ejemplo paso a paso
    "        self.__${7:privado} = ${8:valor}",  # Continúa la lógica del ejemplo paso a paso
    "",  # Continúa la lógica del ejemplo paso a paso
    "    def ${9:accion}(self):",  # Continúa la lógica del ejemplo paso a paso
    "        \"\"\"${10:Descripcion del metodo}\"\"\"",  # Continúa la lógica del ejemplo paso a paso
    "        ${11:pass}",  # Continúa la lógica del ejemplo paso a paso
    "",  # Continúa la lógica del ejemplo paso a paso
    "    def __str__(self):",  # Continúa la lógica del ejemplo paso a paso
    "        return (f\"${1:Nombre} | {self.${3:param1}}\")"  # Continúa la lógica del ejemplo paso a paso
  ],  # Continúa la lógica del ejemplo paso a paso
  "description": "Clase completa con publico, protegido y privado"  # Continúa la lógica del ejemplo paso a paso
}  # Cierra la estructura abierta en las líneas anteriores

🎯 ¿Qué son los ${1:placeholder} en el JSON?

${1:NombreClase}
El 1 es el orden del Tab stop. VS Code resalta este texto primero para que lo reemplaces.
${3:param} = ${3:param}
Mismo número = campos vinculados. Escribes el nombre una vez y se actualiza en todos los lugares con ese número.
⇥ Tab para avanzar
Cada vez que presionas Tab, el cursor salta al siguiente número ($1 → $2 → $3...) hasta completar la clase.
"prefix": "clase"
La palabra que escribes en el editor para activar el snippet. Puedes cambiarla por lo que prefieras.
🚀 Con los snippets ahorras escribir la estructura repetitiva de cada clase. Dedica tu tiempo a la lógica de negocio — el __init__, los atributos y los métodos base los genera VS Code por ti en segundos.
2

Métodos de Acción — reservar()

Estado booleano
Python
    def reservar(self):  # Método reservar(): acción que puede realizar este objeto
        if self.disponible:                 # evalúa el booleano directamente
            self.disponible = False  # Guarda disponible como atributo de esta instancia
            return f"✅ Habitación {self.numero} reservada con éxito."  # Retorna el valor calculado al lugar donde se llamó este método
        return f"❌ Habitación {self.numero} ya está ocupada."  # Retorna el valor calculado al lugar donde se llamó este método

🔬 Análisis de Sintaxis

  • if self.disponible Evalúa un booleano directamente. No necesitas == True; Python infiere la verdad de la variable.
  • f"..." F-string (Python 3.6+). Permite insertar variables dentro del texto usando {} sin concatenación.
  • return Termina el método y devuelve el valor al código que lo llamó.

⚙️ Ejecución en el Contexto

  • Evaluación: "¿Está la Hab. 101 disponible?"
  • Si True → cambia el interruptor a False y retorna mensaje de éxito.
  • Este cambio bloquea futuras llamadas a reservar() para la misma habitación.
  • Si ya está ocupada, retorna el mensaje de error sin modificar el estado.

📖 Glosario

return
Envía un valor de vuelta y finaliza el método.
f-string
Plantilla de texto con variables incrustadas usando {variable}.
Booleano
Tipo de dato con solo dos valores: True o False.
Estado
Valor de un atributo en un momento dado de la ejecución.
🔄 El patrón leer estado → modificar estado → retornar resultado es la base de la lógica de negocio en POO. Aquí la habitación "decide" si acepta la reserva.
3

Herencia — Persona y Cliente

Herencia simple
Python
class Persona:  # Define la clase Persona
    def __init__(self, nombre, email):  # Constructor: inicializa el objeto con los datos recibidos
        self.nombre = nombre  # Guarda nombre como atributo de esta instancia
        self.email  = email  # Guarda email como atributo de esta instancia

class Cliente(Persona):             # Cliente hereda de Persona
    def __init__(self, nombre, email, telefono):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(nombre, email)  # delega al padre
        self.telefono       = telefono  # Guarda telefono como atributo de esta instancia
        self.reservaciones  = []          # historial vacío

🔬 Análisis de Sintaxis

  • (Persona) Parámetro en la definición de clase que indica la clase padre.
  • super() Función integrada que referencia al padre directo. Evita repetición de código.
  • super().__init__(...) Llama explícitamente al constructor del padre para inicializar sus atributos.
  • [] Lista vacía. El historial de reservaciones empieza limpio en cada nuevo cliente.

⚙️ Ejecución en el Contexto

  • cliente1 = Cliente("Ana", "ana@mail.com", "9999")
  • Python llama al __init__ de Cliente.
  • super().__init__ delega a Persona → guarda nombre y email.
  • Luego Cliente agrega telefono y reservaciones.
  • Resultado: cliente1 tiene 4 atributos gracias a la jerarquía.

📖 Glosario

Herencia
Capacidad de una clase de adquirir atributos y métodos de otra.
Subclase
Clase que hereda (aquí: Cliente).
Superclase
Clase de la que se hereda (aquí: Persona).
super()
Acceso a la clase padre desde la hija.
Persona
nombre · email
▼ hereda
Cliente
+ telefono · reservaciones
🧬 La herencia modela la realidad: todo Cliente es una Persona, pero no toda Persona es un Cliente. Reutilizamos código sin copiarlo.
4

Orquestación — La Clase Reservacion

Colaboración de objetos
Python
class Reservacion:  # Define la clase Reservacion
    def __init__(self, cliente, habitacion, fecha_entrada, fecha_salida):  # Constructor: inicializa el objeto con los datos recibidos
        self.cliente       = cliente      # objeto Cliente
        self.habitacion    = habitacion   # objeto Habitacion
        self.fecha_entrada = fecha_entrada  # Guarda fecha_entrada como atributo de esta instancia
        self.fecha_salida  = fecha_salida  # Guarda fecha_salida como atributo de esta instancia

    def confirmar(self):  # Método confirmar(): acción que puede realizar este objeto
        resultado = self.habitacion.reservar()          # 1. cierra la habitación
        self.cliente.agregar_reservacion(self)          # 2. registra en el cliente
        return resultado                               # 3. devuelve resultado

🔬 Análisis de Sintaxis

  • self.habitacion Atributo que guarda una referencia a otro objeto (Habitacion). Esto es composición.
  • self.habitacion.reservar() Acceso encadenado: primero el atributo, luego el método del objeto contenido.
  • self.cliente.agregar_reservacion(self) El self al final envía la reservación entera (no solo un dato) al historial del cliente.

⚙️ Ejecución en el Contexto

Al ejecutar reserva.confirmar() se dispara una cadena:

  1. La habitación cambia su estado a ocupada.
  2. El cliente registra esta reservación en su historial.
  3. El resultado (mensaje ✅ o ❌) viaja de vuelta al código principal.

📖 Glosario

Instancia
Objeto real creado a partir de una clase.
Composición
Un objeto contiene referencias a otros objetos.
Método
Función que vive y actúa dentro de una clase.
Colaboración
Objetos que se invocan mutuamente para lograr un objetivo.
reserva
.confirmar()
habitacion
.reservar()
+
cliente
.agregar_reservacion()
🎯 Reservacion es el orquestador. No hace el trabajo él solo — delega a Habitacion y a Cliente. Este es el principio de responsabilidad única en acción.
5

Resumen — Flujo de Datos

Visión global
Paso Acción ¿Qué se evalúa / almacena? Concepto POO
1 · Instanciación hab101 = Habitacion(...) Crea el objeto en memoria con ID único y disponible = True. Instancia
2 · Lógica if self.disponible Revisa el estado booleano de la propiedad. Estado
3 · Confirmación reserva.confirmar() Ejecuta el cambio de estado de True → False y actualiza historial. Colaboración
4 · Visualización print(hab101) Llama automáticamente al método __str__ para texto legible. Dunder method
🗺️ Cada paso delega en el anterior. La aplicación principal no manipula datos directamente: los objetos se gestionan a sí mismos. Eso es la esencia de POO.
E

¿Cómo convierto la plantilla en un Snippet? — Paso a paso

VS Code · python.json

La regla es simple: cada [corchete] de la plantilla se convierte en un ${número:texto} en el snippet. El número indica el orden en que el cursor saltará al presionar Tab. El texto es el placeholder que verás resaltado.

🔄 Conversión línea a línea — Plantilla Python → JSON Snippet
📄 Plantilla original (.py)
class [NombreDeLaClase]: """[Descripción]""" def __init__(self, [param1], [param2]): self.[param1] = [param1] self.[param2] = [param2] self.[estado] = [ValorDefecto] def [nombre_metodo](self): [lógica] return [resultado] def __str__(self): return f"[texto]"
✅ Snippet resultante (python.json)
"Clase Pro": { "prefix": "clase", "body": [ "class ${1:NombreDeLaClase}:", " \"\"\"${2:Descripción}\"\"\"", "", " def __init__(self, ${3:param1}, ${4:param2}):", " self.${3:param1} = ${3:param1}", " self.${4:param2} = ${4:param2}", " self.${5:estado} = ${6:True}", "", " def ${7:nombre_metodo}(self):", " ${8:pass}", " return ${9:None}", "", " def __str__(self):", " return f\"${10:texto}\"" ], "description": "Clase completa con init, método y __str__" }
📊 Tabla de conversión — cada corchete → su Tab stop
Plantilla originalSnippet JSON¿Por qué ese número?
[NombreDeLaClase]
${1:NombreDeLaClase}
Tab stop 1 — lo primero que escribes. Define el nombre de toda la clase.
[Descripción]
${2:Descripción}
Tab stop 2 — el docstring. Opcional pero buena práctica.
[param1] (×3 veces)
${3:param1} (×3)
Tab stop 3 repetido — escribes el nombre una vez y aparece en el parámetro, en self.x y en la asignación.
[param2] (×3 veces)
${4:param2} (×3)
Tab stop 4 repetido — mismo comportamiento que param1.
[estado]
${5:estado}
Tab stop 5 — nombre del atributo de estado interno.
[ValorDefecto]
${6:True}
Tab stop 6 — valor inicial. Ya viene con True como sugerencia.
[nombre_metodo]
${7:nombre_metodo}
Tab stop 7 — nombre de la primera acción del objeto.
[lógica]
${8:pass}
Tab stop 8 — cuerpo del método. pass es el placeholder porque el código necesita al menos una línea válida.
[resultado]
${9:None}
Tab stop 9 — valor de retorno. None como sugerencia por defecto.
[texto]
${10:texto}
Tab stop 10 — lo que mostrará __str__. Último campo antes de terminar.
▶️ Cómo se invoca — Simulación paso a paso en VS Code

Abres un archivo .py y escribes el prefix

Escribe la palabra "clase" (el prefix del snippet). VS Code muestra una sugerencia en el autocompletado.

📄 mi_programa.py
clase 💡 Clase Pro · Clase completa con init, método y __str__

Presionas Tab o Enter

VS Code expande toda la estructura. El cursor salta automáticamente al Tab stop 1 — el nombre de la clase — que aparece resaltado en azul.

📄 mi_programa.py — Tab stop 1 activo
class NombreDeLaClase : """Descripción""" def __init__(self, param1, param2): self.param1 = param1 self.param2 = param2 self.estado = True def nombre_metodo(self): pass return None def __str__(self): return f"texto"

Escribes el nombre de tu clase → Tab → Tab stop 2

Escribes "Habitacion". El placeholder se reemplaza. Al presionar Tab el cursor salta al docstring (stop 2).

📄 mi_programa.py — escribiste "Habitacion", Tab stop 2 activo
class Habitacion : """ Descripción """ def __init__(self, param1, param2): self.param1 = param1 ...

Llegas al Tab stop 3 — param1 → se actualiza en 3 lugares a la vez

Escribes "numero" y VS Code reemplaza simultáneamente el parámetro del __init__, el self.numero y la asignación = numero. ¡Un solo tecleo, tres cambios!

📄 mi_programa.py — Tab stop 3 · "numero" reemplaza 3 campos
def __init__(self, numero , param2): self. numero = numero ← los 3 se actualizan solos self.param2 = param2 self.estado = True

Sigues con Tab hasta completar todos los campos

Cada Tab salta al siguiente número. Al llegar al stop 10, el cursor queda libre. Tu clase está 100% completada.

📄 mi_programa.py — ✅ Clase completa
class Habitacion: """Representa un cuarto en el hotel""" def __init__(self, numero, tipo): self.numero = numero self.tipo = tipo self.disponible = True def reservar(self): pass return None def __str__(self): return f"Habitacion {self.numero}" # ✅ listo para agregar tu lógica
⌨️ Orden de Tab stops — qué campo llenas en cada presión
1
${1:NombreDeLaClase}
Nombre de la clase — primer campo, define todo
2
${2:Descripción}
Docstring — descripción breve del objeto
3
${3:param1} × 3
Primer parámetro — aparece en 3 lugares a la vez
4
${4:param2} × 3
Segundo parámetro — también vinculado en 3 lugares
5
${5:estado}
Nombre del atributo de estado interno
6
${6:True}
Valor por defecto del estado (True, False, 0, []…)
7
${7:nombre_metodo}
Nombre del primer método del objeto
8
${8:pass}
Cuerpo del método — reemplaza pass con tu lógica
9
${9:None}
Valor de retorno del método
10
${10:texto}
Contenido del __str__ — último campo, cursor libre
Resaltado en azul = campo activo ahora. Presiona Tab para avanzar al siguiente.

🔢 Números únicos = campos independientes

Si le pones número diferente a cada campo ($1, $2, $3...) el cursor los recorre uno por uno.

🔗 Mismo número = campos vinculados

Si repites el mismo número ($3, $3, $3) escribes una vez y se actualiza en todos los sitios simultáneamente.

💬 El texto entre : es el placeholder

${1:NombreClase} → muestra "NombreClase" en gris hasta que empiezas a escribir. Lo que escribes lo reemplaza.

🏁 $0 es la posición final

Si añades $0 al final del snippet, el cursor termina ahí después del último Tab. Opcional pero útil para posicionarte tras la clase.

🎯 Resumen del flujo: escribe claseTab → se expande toda la estructura → Tab × 10 para llenar cada campo → clase completa en menos de 30 segundos.
F

Herramientas Intermedias de Python que ya aparecen en los ejercicios

Puente entre la teoría y la práctica

Hasta aquí ya dominas clases, constructores, self, __str__, super(), herencia y modificadores de acceso. Sin embargo, en los ejercicios de biblioteca, veterinaria, nómina, tienda, universidad y figuras aparecen herramientas extra de Python que conviene entender con calma.

Esta sección F reúne esas piezas “intermedias” para que no copies código a ciegas: la idea es que ahora puedas leer, explicar, modificar y crear soluciones por tu cuenta.

🧭 Qué aprenderás

  • Control Cuándo un archivo se ejecuta directo y cuándo solo se importa.
  • Contratos Cómo obligar a las subclases a implementar métodos importantes.
  • Criterios Cómo sumar, buscar máximos o formatear reportes con menos código.
  • Introspección Cómo un objeto puede saber qué clase es realmente.

🎯 Idea central

La POO no vive aislada: se apoya en funciones integradas, módulos estándar, expresiones compactas y reglas especiales del lenguaje.

Si entiendes estas herramientas, tus clases dejan de ser “solo estructura” y empiezan a comportarse como sistemas completos.

📦 Mapa rápido

abc
Módulo estándar para clases abstractas.
math
Módulo estándar con operaciones matemáticas frecuentes.
key=
Criterio de comparación personalizado.
lambda
Función anónima corta de una sola expresión.

F.1 — if __name__ == "__main__": — Guardián de Módulo

Sirve para distinguir si un archivo Python fue ejecutado directamente o si solo fue importado desde otro archivo.

🔧 ¿Qué es __name__?

__name__ es una variable especial que Python crea automáticamente para cada archivo cargado como módulo.

  • Si corres el archivo directamente, su valor es "__main__".
  • Si otro archivo lo importa, su valor pasa a ser el nombre del módulo.

🧠 ¿Por qué existe?

Porque muchas veces quieres que un archivo tenga funciones reutilizables y además un pequeño bloque de prueba o arranque.

El guardián evita que esas pruebas se ejecuten accidentalmente al importar el archivo desde otro módulo.

📖 Idea clave

Módulo
Cualquier archivo .py que Python puede ejecutar o importar.
Importar
Cargar funciones, clases o variables desde otro archivo.
Ejecutar
Lanzar el archivo como programa principal.
Python
# archivo biblioteca.py
def cargar_libros():  # Método cargar_libros(): acción que puede realizar este objeto
    return ["POO desde cero", "Diseño de clases", "Herencia útil"]  # Retorna el valor calculado al lugar donde se llamó este método

def mostrar_catalogo():  # Método mostrar_catalogo(): acción que puede realizar este objeto
    for titulo in cargar_libros():  # Itera sobre cada titulo en cargar_libros()
        print(f"• {titulo}")  # Muestra en consola el resultado de esta operación

if __name__ == "__main__":  # Se ejecuta solo cuando este archivo se corre directamente (no cuando se importa)
    print("Ejecutando prueba rápida del módulo biblioteca")  # Muestra en consola el resultado de esta operación
    mostrar_catalogo()  # Completa la llamada o expresión iniciada anteriormente
Python
# archivo app.py
from biblioteca import mostrar_catalogo  # Importa mostrar_catalogo del módulo biblioteca

print("Bienvenido al sistema de biblioteca")  # Muestra en consola el resultado de esta operación
mostrar_catalogo()  # Completa la llamada o expresión iniciada anteriormente

# Al importar biblioteca.py NO se ejecuta el bloque protegido
# porque __name__ ya no vale "__main__".
🔬 Anatomía del guardián de módulo
Parte Qué significa
if Evalúa una condición antes de ejecutar un bloque.
__name__ Nombre especial que Python asigna al módulo actual.
== Compara si ambos valores son exactamente iguales.
"__main__" Nombre reservado del archivo que se está ejecutando como programa principal.
Escenario Valor de __name__ ¿Se ejecuta el bloque?
Corres python biblioteca.py "__main__" Sí, porque el archivo es el punto de entrada.
Haces import biblioteca "biblioteca" No, porque el archivo ahora es un módulo auxiliar.
💡 Piensa en este bloque como una puerta de seguridad: deja pasar pruebas, demos o arranque del programa solo cuando el archivo se usa como programa principal.
⚠️ Error común: poner código de prueba suelto al final del archivo sin el guardián. Luego, al hacer import, ese código también corre y ensucia la salida o modifica datos inesperadamente.

F.2 — from abc import ABC, abstractmethod — Clases abstractas

Se usan cuando quieres definir un molde obligatorio: una clase base que describe qué métodos deben existir, aunque todavía no sepas cómo se implementan en cada hija.

🏗️ ¿Qué significa ABC?

ABC significa Abstract Base Class (Clase Base Abstracta).

Cuando una clase hereda de ABC, puede declarar métodos abstractos que actúan como contrato.

📜 ¿Qué hace @abstractmethod?

Marca un método que debe ser implementado por las subclases concretas.

Si una hija no lo implementa, Python impide crear objetos de esa clase.

🤝 Idea de contrato

Contrato
Regla que obliga a las clases hijas a cumplir cierta interfaz.
Abstracta
Clase pensada para heredar, no para instanciarse directamente.
Concreta
Clase completa que ya implementa todos los métodos requeridos.
Python
from abc import ABC, abstractmethod  # Importa ABC, abstractmethod del módulo abc
import math  # Importa el módulo math para usar sus funciones y clases

class Figura(ABC):  # Figura hereda de ABC — recibe todos sus atributos y métodos
    @abstractmethod  # Obliga a las subclases a implementar el siguiente método
    def area(self):  # Método area(): acción que puede realizar este objeto
        pass  # Deja el bloque vacío porque la implementación se completará después

    @abstractmethod  # Obliga a las subclases a implementar el siguiente método
    def perimetro(self):  # Método perimetro(): acción que puede realizar este objeto
        pass  # Deja el bloque vacío porque la implementación se completará después

class Circulo(Figura):  # Circulo hereda de Figura — recibe todos sus atributos y métodos
    def __init__(self, radio):  # Constructor: inicializa el objeto con los datos recibidos
        self.radio = radio  # Guarda radio como atributo de esta instancia

    def area(self):  # Método area(): acción que puede realizar este objeto
        return math.pi * self.radio ** 2  # Retorna el valor calculado al lugar donde se llamó este método

    def perimetro(self):  # Método perimetro(): acción que puede realizar este objeto
        return 2 * math.pi * self.radio  # Retorna el valor calculado al lugar donde se llamó este método
🔬 Anatomía de una clase abstracta
Fragmento Función dentro del diseño
from abc import ABC, abstractmethod Importa las herramientas necesarias desde el módulo estándar abc.
class Figura(ABC) Declara que Figura es una base abstracta.
@abstractmethod Marca un método como obligatorio para toda subclase concreta.
pass Deja el bloque vacío porque la implementación real vivirá en las clases hijas.
Sin ABC Con ABC
Puedes olvidar implementar area() y el error aparece más tarde, durante la ejecución. Python detecta desde el inicio que la hija está incompleta y no deja instanciarla.
La clase base solo “sugiere” una estructura. La clase base obliga a respetar esa estructura.
Es fácil crear hijos inconsistentes. Las hijas quedan alineadas con el mismo contrato de trabajo.
Python
class Rectangulo(Figura):  # Rectangulo hereda de Figura — recibe todos sus atributos y métodos
    def __init__(self, base, altura):  # Constructor: inicializa el objeto con los datos recibidos
        self.base = base  # Guarda base como atributo de esta instancia
        self.altura = altura  # Guarda altura como atributo de esta instancia

    # Falta implementar perimetro()
    def area(self):  # Método area(): acción que puede realizar este objeto
        return self.base * self.altura  # Retorna el valor calculado al lugar donde se llamó este método

# rect = Rectangulo(4, 5)
# TypeError: Can't instantiate abstract class Rectangulo
# with abstract method perimetro
💡 En el ejercicio de figuras, una clase abstracta comunica claramente que toda figura debe poder calcular area() y perimetro(), aunque cada fórmula cambie.
⚠️ No confundas “clase abstracta” con “clase padre común”. Puedes tener herencia sin ABC, pero usas ABC cuando quieres imponer reglas estrictas, no solo compartir código.

F.3 — isinstance(objeto, Clase) — Verificación de tipo en tiempo de ejecución

Permite preguntar si un objeto pertenece a cierta clase o a alguna de sus clases hijas mientras el programa está corriendo.

Sintaxis

isinstance(objeto, Clase) devuelve True o False.

También acepta una tupla de clases: isinstance(x, (Perro, Gato)).

🌳 Herencia incluida

Su mayor ventaja es que entiende la jerarquía de herencia.

Si Perro hereda de Mascota, entonces un perro también “cuenta” como Mascota.

🩺 Caso veterinaria

Animal
Clase general de la clínica.
Mascota
Especialización de Animal.
Perro
Clase más concreta del sistema.
Python
class Animal:  # Define la clase Animal
    pass  # Deja el bloque vacío porque la implementación se completará después

class Mascota(Animal):  # Mascota hereda de Animal — recibe todos sus atributos y métodos
    pass  # Deja el bloque vacío porque la implementación se completará después

class Perro(Mascota):  # Perro hereda de Mascota — recibe todos sus atributos y métodos
    pass  # Deja el bloque vacío porque la implementación se completará después

paciente = Perro()  # Asigna un valor inicial o calculado a paciente

print(isinstance(paciente, Perro))  # Muestra en consola el resultado de esta operación
print(isinstance(paciente, Mascota))  # Muestra en consola el resultado de esta operación
print(isinstance(paciente, Animal))  # Muestra en consola el resultado de esta operación
print(isinstance(paciente, object))  # Muestra en consola el resultado de esta operación
Expresión Resultado Razón
isinstance(paciente, Perro) True El objeto fue creado exactamente desde Perro.
isinstance(paciente, Mascota) True Perro hereda de Mascota.
isinstance(paciente, Animal) True Toda la cadena de herencia cuenta.
isinstance(paciente, object) True En Python, casi todo hereda finalmente de object.
Python
# MENOS FLEXIBLE
if type(paciente) == Mascota:  # Verifica type(paciente) == Mascota antes de continuar
    print("Es mascota")  # Muestra en consola el resultado de esta operación

# MEJOR
if isinstance(paciente, Mascota):  # Verifica isinstance(paciente, Mascota) antes de continuar
    print("Es mascota")  # Muestra en consola el resultado de esta operación
🔬 type(...) == Clase vs isinstance(...)
Opción Comportamiento
type(obj) == Clase Solo funciona cuando el tipo exacto coincide; ignora herencia.
isinstance(obj, Clase) Reconoce el tipo exacto y también cualquier relación válida de herencia.
💡 En programas orientados a objetos, casi siempre conviene preguntar “¿se comporta como este tipo?” en lugar de “¿fue creado exactamente con esta clase?”. Por eso isinstance() es la herramienta idiomática.
⚠️ Error común: comparar con type() y luego sorprenderse porque una subclase no entra en la condición. Ese enfoque rompe la flexibilidad natural de la herencia.

F.4 — import math — Módulo matemático

El módulo math reúne operaciones y constantes matemáticas listas para usar, sin reinventar fórmulas manualmente.

📐 ¿Por qué usarlo?

Porque ofrece resultados correctos, nombres claros y funciones que ya vienen optimizadas en Python.

  • math.pi para el valor de π.
  • math.sqrt(x) para raíz cuadrada.
  • math.pow(a, b) para elevar potencias.

🧮 Contexto figuras

En el ejercicio de figuras, math.pi hace más legible el cálculo del círculo y sqrt() ayuda con diagonales o distancias derivadas.

📚 Recordatorio

Módulo
Archivo o librería de Python que agrupa herramientas relacionadas.
Constante
Valor fijo, como pi.
Función
Bloque reutilizable que recibe datos y devuelve un resultado.
Python
import math  # Importa el módulo math para usar sus funciones y clases

class Circulo:  # Define la clase Circulo
    def __init__(self, radio):  # Constructor: inicializa el objeto con los datos recibidos
        self.radio = radio  # Guarda radio como atributo de esta instancia

    def area(self):  # Método area(): acción que puede realizar este objeto
        return math.pi * math.pow(self.radio, 2)  # Retorna el valor calculado al lugar donde se llamó este método

    def diametro(self):  # Método diametro(): acción que puede realizar este objeto
        return self.radio * 2  # Retorna el valor calculado al lugar donde se llamó este método

    def diagonal_de_caja(self):  # Método diagonal_de_caja(): acción que puede realizar este objeto
        lado = self.diametro()  # Asigna un valor inicial o calculado a lado
        return math.sqrt(lado ** 2 + lado ** 2)  # Retorna el valor calculado al lugar donde se llamó este método
Herramienta Qué hace Ejemplo típico
math.pi Constante π para círculos y trigonometría. math.pi * radio ** 2
math.sqrt(x) Devuelve la raíz cuadrada de x. math.sqrt(49)7.0
math.pow(a, b) Eleva a a la potencia b. math.pow(5, 2)25.0
math.ceil(x) Redondea hacia arriba. math.ceil(3.1)4
math.floor(x) Redondea hacia abajo. math.floor(3.9)3
🔬 ¿Cuándo usar math.pow() y cuándo **?
Forma Lectura
radio ** 2 Más común y directa para exponentes sencillos.
math.pow(radio, 2) Útil cuando quieres resaltar visualmente que estás llamando una función matemática.
💡 Usar math mejora la legibilidad. Cuando otro estudiante lee math.sqrt(), entiende de inmediato que hay una raíz cuadrada, sin descifrar fórmulas improvisadas.
⚠️ Error común: escribir pi o sqrt() sin importar antes el módulo. Si no haces import math, Python no sabe de dónde salen esos nombres.

F.5 — Especificadores de formato en f-strings

Ya conoces la f-string básica. Ahora toca aprender la parte que vuelve elegantes los reportes: alineación, decimales y separadores de miles.

🎨 ¿Para qué sirven?

Permiten controlar cómo se ve un valor al imprimirlo, sin cambiar el valor original.

  • :.2f fija dos decimales.
  • :>14 alinea a la derecha en 14 espacios.
  • :<19 alinea a la izquierda en 19 espacios.
  • :,.2f agrega comas y dos decimales.

🧾 Contexto nómina / tienda

Son ideales para tickets, planillas, facturas y recibos, donde las columnas deben verse ordenadas aunque los datos tengan longitudes distintas.

🧩 Piezas

Formato
Regla visual aplicada al mostrar el dato.
Ancho
Cantidad mínima de espacios reservados.
Precisión
Cantidad de decimales después del punto.
🔬 Anatomía de {total:>14,.2f}
Parte Significado
total La variable que quieres mostrar.
: Indica que a partir de ahí viene la especificación de formato.
>14 Alinea a la derecha y reserva 14 caracteres.
, Agrega separador de miles.
.2f Muestra exactamente 2 decimales en formato flotante.
Python
nombre = "Ana López"  # Asigna un valor inicial o calculado a nombre
total = 15432.5  # Asigna un valor inicial o calculado a total

print(f"Empleado: {nombre:<19}")  # Muestra en consola el resultado de esta operación
print(f"Total bruto: {total:.2f}")  # Muestra en consola el resultado de esta operación
print(f"Total alineado: {total:>14}")  # Muestra en consola el resultado de esta operación
print(f"Total con miles: {total:,.2f}")  # Muestra en consola el resultado de esta operación
print(f"Columna completa: {total:>14,.2f}")  # Muestra en consola el resultado de esta operación
Especificador Resultado esperado Uso típico
{precio:.2f} 1450.50 Monedas y promedios.
{nombre:>14} Texto a la derecha en 14 espacios Columnas numéricas o etiquetas cortas.
{nombre:<19} Texto a la izquierda en 19 espacios Listados y reportes tabulares.
{valor:,.2f} 12,540.75 Montos grandes en nómina o ventas.
{valor:>14,.2f} Valor alineado, con miles y decimales Recibos profesionales y tablas impresas.
Python
# SIN FORMATO
print(f"Laptop {15432.5}")  # Muestra en consola el resultado de esta operación
print(f"Mouse {250}")  # Muestra en consola el resultado de esta operación

# CON FORMATO
print(f"{'Laptop':<19} L {15432.5:>14,.2f}")  # Muestra en consola el resultado de esta operación
print(f"{'Mouse':<19} L {250:>14,.2f}")  # Muestra en consola el resultado de esta operación
💡 El valor no cambia, solo su presentación. 15432.5 sigue siendo el mismo número; simplemente decides mostrarlo como 15,432.50 para que el usuario lo lea mejor.
⚠️ Error común: creer que :>14 “convierte” el número. No lo convierte; solo reserva espacio visual. El tipo del dato sigue siendo el mismo hasta que lo imprimes.

F.6 — Expresiones generadoras (generator expressions)

Son expresiones compactas que producen valores uno por uno, bajo demanda, sin construir una lista completa desde el inicio.

⚙️ Sintaxis base

(expresion for variable in iterable)

Se parecen a las listas por comprensión, pero usan paréntesis y trabajan de forma perezosa (lazy).

🐢 Evaluación perezosa

No calculan todo de golpe. Van entregando cada valor cuando otra función lo necesita, por ejemplo sum().

🛒 Contexto tienda

Iterable
Objeto por el que puedes recorrer elemento por elemento.
Lazy
Se calcula solo cuando hace falta.
sum()
Función integrada que acumula valores numéricos.
Python
# carrito.__items contiene objetos Producto
total = sum(item.get_precio() for item in carrito._Carrito__items)  # Asigna un valor inicial o calculado a total

# equivalente más largo
total = 0  # Asigna un valor inicial o calculado a total
for item in carrito._Carrito__items:  # Itera sobre cada item en carrito._Carrito__items
    total += item.get_precio()  # Completa la llamada o expresión iniciada anteriormente
🔬 Anatomía de sum(p.get_precio() for p in items)
Parte Papel
sum(...) Recibe una secuencia de números y los acumula.
p.get_precio() Transforma cada objeto p en un valor numérico.
for p in items Recorre todos los productos del carrito.
(...) Indica que es un generador y no una lista creada por adelantado.
Forma Característica Ejemplo
[x.area() for x in figuras] Crea una lista completa en memoria. Útil si necesitas reutilizar los resultados varias veces.
(x.area() for x in figuras) Produce valores uno por uno. Ideal cuando otra función consume el resultado inmediatamente.
Python
# universidad: promedio de créditos aprobados
creditos_aprobados = sum(curso.creditos for curso in cursos if curso.aprobado)  # Asigna un valor inicial o calculado a creditos_aprobados

# nómina: total de pagos
pago_total = sum(empleado.calcular_pago() for empleado in empleados)  # Asigna un valor inicial o calculado a pago_total
💡 Las expresiones generadoras combinan muy bien con funciones como sum(), max(), min() o any(). Te dejan escribir “qué dato quieres” sin montar manualmente una lista intermedia.
⚠️ Error común: intentar recorrer el mismo generador varias veces. Un generador se consume; si ya lo recorrió sum(), tendrás que crearlo otra vez.

F.7 — Funciones lambda

Una lambda es una función anónima y corta, pensada para usarse justo en el lugar donde se necesita.

✍️ Sintaxis

lambda parametros: expresion

No lleva nombre y su cuerpo debe ser una sola expresión.

🎯 ¿Cuándo conviene?

Cuando necesitas una función muy pequeña y de uso inmediato, por ejemplo para decirle a max() cuál valor comparar.

🧪 Ejemplos típicos

key=
Recibe una función que extrae el criterio de comparación.
sorted()
Ordena una colección.
max()
Busca el mayor elemento según cierto criterio.
Python
# figuras: encontrar la de mayor área
mayor = max(figuras, key=lambda f: f.area())

# veterinaria: ordenar pacientes por peso
pacientes_ordenados = sorted(pacientes, key=lambda p: p.peso)

# universidad: alumno con mejor promedio
mejor = max(estudiantes, key=lambda e: e.promedio())
Forma Lectura Cuándo usarla
lambda x: x.area() “Dado x, devuelve su área”. Cuando solo necesitas una función breve para una sola llamada.
def criterio(x): return x.area() Versión con nombre del mismo criterio. Cuando el criterio se reutiliza o merece un nombre descriptivo.
🔬 Anatomía de lambda f: f.area()
Parte Interpretación
lambda Le dice a Python que vas a crear una función anónima.
f Parámetro temporal que representa cada objeto recibido.
: Separa los parámetros de la expresión a devolver.
f.area() Valor que la lambda calcula y devuelve automáticamente.
Python
# CON lambda
mayor = max(figuras, key=lambda f: f.area())  # Asigna un valor inicial o calculado a mayor

# SIN lambda
def obtener_area(figura):  # Método obtener_area(): acción que puede realizar este objeto
    return figura.area()  # Retorna el valor calculado al lugar donde se llamó este método

mayor = max(figuras, key=obtener_area)  # Asigna un valor inicial o calculado a mayor
💡 Una lambda no es “más poderosa” que def; simplemente es más cómoda cuando el criterio es corto y no vale la pena bautizarlo con un nombre.
⚠️ Error común: meter demasiada lógica en una lambda. Si la expresión ya no es clara de leer, mejor crea una función normal con def.

F.8 — sum(), max(), min() y el parámetro key=

Estas funciones integradas trabajan sobre iterables. sum() acumula números; max() y min() buscan extremos; y key= permite decirles qué criterio usar.

🛠️ Lo esencial

  • sum(iterable) suma números.
  • max(iterable) obtiene el mayor.
  • min(iterable) obtiene el menor.
  • max(iterable, key=funcion) compara usando un criterio.

🧭 ¿Qué hace key=?

Recibe una función que transforma cada objeto en el valor por el que se va a comparar.

Normalmente se usa con lambda, aunque cualquier función sirve.

📌 Precisión importante

sum()
No usa key=; para “sumar por criterio” primero transformas con un generador.
max()
Sí acepta key=.
min()
Sí acepta key=.
Python
# tienda: total de la compra
total = sum(producto.get_precio() for producto in items)  # Asigna un valor inicial o calculado a total

# figuras: la de mayor área
figura_mayor = max(figuras, key=lambda f: f.area())  # Asigna un valor inicial o calculado a figura_mayor

# universidad: el alumno con menor promedio
en_riesgo = min(estudiantes, key=lambda e: e.promedio())  # Asigna un valor inicial o calculado a en_riesgo
Función ¿Acepta key=? Uso correcto
sum() No sum(x.precio for x in items)
max() max(figuras, key=lambda f: f.area())
min() min(empleados, key=lambda e: e.salario)
Python
# SIN key=
mayor = max([10, 25, 7])  # Asigna un valor inicial o calculado a mayor

# CON key= sobre objetos
mayor_salario = max(empleados, key=lambda e: e.calcular_pago())  # Asigna un valor inicial o calculado a mayor_salario

# sum() transforma primero y suma después
pago_total = sum(e.calcular_pago() for e in empleados)  # Asigna un valor inicial o calculado a pago_total
🔬 Cómo piensa Python cuando usas key=
Paso Qué ocurre
1 Toma un objeto del iterable.
2 Le aplica la función definida en key=.
3 Compara el valor devuelto, no el objeto completo.
4 Retorna el objeto original que produjo el máximo o mínimo.
💡 key= es como ponerle a cada objeto una etiqueta numérica temporal para compararlo. Python no destruye el objeto; solo usa esa etiqueta para decidir cuál gana.
⚠️ Importante: sum() no acepta key=. Si escribes sum(items, key=...), obtendrás error. Primero extrae los números con un generador y luego suma.

F.9 — Métodos de lista y string: append() y join()

En varios ejercicios guardas objetos en listas y luego conviertes esas listas en texto para mostrar recibos o reportes.

📥 append()

lista.append(elemento) agrega un solo elemento al final de la lista.

Es ideal para carritos, historiales, matrículas y colecciones internas del objeto.

🧵 join()

"separador".join(lista_de_textos) une muchas cadenas en una sola.

Es perfecto para generar recibos línea por línea y luego convertirlos en un bloque final.

📝 Precisión importante

append()
Es método de lista.
join()
Es método de string separador, no de la lista.
\n
Salto de línea usado frecuentemente como separador.
Python
class Carrito:  # Define la clase Carrito
    def __init__(self):  # Constructor: inicializa el objeto con los datos recibidos
        self._Carrito__items = []  # Guarda _Carrito__items como atributo protegido de la instancia

    def agregar_producto(self, producto):  # Método agregar_producto(): acción que puede realizar este objeto
        self._Carrito__items.append(producto)  # Agrega un elemento al final de self._Carrito__items

    def generar_recibo(self):  # Método generar_recibo(): acción que puede realizar este objeto
        lineas = []  # Crea una lista vacía para almacenar datos en lineas
        for producto in self._Carrito__items:  # Itera sobre cada producto en self._Carrito__items
            lineas.append(f"{producto.nombre:<19} L {producto.get_precio():>10,.2f}")  # Agrega un elemento al final de lineas
        return "\n".join(lineas)  # Retorna el valor calculado al lugar donde se llamó este método
Método Entrada esperada Resultado
append() Un elemento cualquiera La lista crece en un elemento más al final.
"\n".join(...) Lista de cadenas Devuelve una sola cadena con saltos de línea entre cada parte.
🔬 Flujo típico en un recibo
Paso Acción
1 Creas una lista vacía llamada lineas.
2 En cada iteración haces lineas.append(...).
3 Al final unes todas las líneas con "\n".join(lineas).
4 Obtienes un solo texto listo para imprimir o retornar.
Python
# INCORRECTO
lineas.join("\n")

# CORRECTO
"\n".join(lineas)
💡 append() construye la colección; join() construye la presentación final. En otras palabras: primero guardas piezas, luego las pegas en un solo texto.
⚠️ Error clásico: pensar que existe list.join(). En Python, join() pertenece al string separador, por eso escribimos ", ".join(lista) y no lista.join(", ").

F.10 — __class__.__name__ — Introspección

La introspección es la capacidad de un programa para examinarse a sí mismo: descubrir tipos, nombres y estructura en tiempo de ejecución.

🔍 ¿Qué significa?

  • obj.__class__ da la clase real del objeto.
  • obj.__class__.__name__ da el nombre de esa clase como texto.

📣 ¿Para qué sirve?

Para crear descripciones automáticas, mensajes genéricos, depuración y reportes sin escribir el nombre de la clase “a mano”.

🧠 Contexto figuras

Introspección
Observar propiedades internas del programa mientras corre.
Clase real
La clase concreta del objeto, no solo la referencia de una variable.
Nombre
Cadena lista para interpolar en mensajes.
Python
class Figura(ABC):  # Figura hereda de ABC — recibe todos sus atributos y métodos
    @abstractmethod  # Obliga a las subclases a implementar el siguiente método
    def area(self):  # Método area(): acción que puede realizar este objeto
        pass  # Deja el bloque vacío porque la implementación se completará después

    def describir(self):  # Método describir(): acción que puede realizar este objeto
        return f"Soy un objeto de tipo {self.__class__.__name__}"  # Retorna el valor calculado al lugar donde se llamó este método

class Triangulo(Figura):  # Triangulo hereda de Figura — recibe todos sus atributos y métodos
    def area(self):  # Método area(): acción que puede realizar este objeto
        return 0  # Retorna el valor calculado al lugar donde se llamó este método

figura = Triangulo()  # Asigna un valor inicial o calculado a figura
print(figura.describir())  # Muestra en consola el resultado de esta operación
🔬 Anatomía de self.__class__.__name__
Parte Qué aporta
self La instancia actual.
self.__class__ La clase concreta desde la que fue creada esa instancia.
self.__class__.__name__ El nombre de esa clase convertido en texto, por ejemplo "Triangulo".
Objeto obj.__class__ obj.__class__.__name__
Circulo(5) <class 'Circulo'> "Circulo"
Rectangulo(4, 8) <class 'Rectangulo'> "Rectangulo"
Estudiante(...) <class 'Estudiante'> "Estudiante"
Python
# SIN introspección
def describir(self):  # Método describir(): acción que puede realizar este objeto
    return "Soy una figura"  # Retorna el valor calculado al lugar donde se llamó este método

# CON introspección
def describir(self):  # Método describir(): acción que puede realizar este objeto
    return f"Soy un objeto de tipo {self.__class__.__name__}"  # Retorna el valor calculado al lugar donde se llamó este método
💡 La introspección te permite escribir métodos más generales. En lugar de hardcodear "Circulo", dejas que cada objeto revele su identidad automáticamente.
⚠️ Error común: escribir manualmente el nombre de la clase en un string. Si mañana renombraras la clase, el mensaje quedaría desactualizado; con __class__.__name__, el texto se mantiene correcto.
🔬 Resumen final de la sección F
Herramienta Para qué te servirá en los ejercicios
if __name__ == "__main__" Separar pruebas locales de código reutilizable.
ABC + @abstractmethod Obligar a las clases hijas a cumplir un contrato común.
isinstance() Validar tipos sin romper la herencia.
math Resolver cálculos claros y confiables en figuras.
Formato en f-strings Presentar tickets, nóminas y reportes alineados.
Generadores Sumar o recorrer datos sin crear listas innecesarias.
lambda Definir criterios breves de comparación u ordenamiento.
sum(), max(), min() Calcular totales y encontrar extremos con poco código.
append() y join() Construir colecciones y luego convertirlas en texto.
__class__.__name__ Describir objetos dinámicamente mediante introspección.

F.11 Métodos get y set — Getters y Setters

Cuando los atributos de una clase son privados (comienzan con __), el código externo no puede leerlos ni cambiarlos directamente. Los getters y setters son métodos públicos que abren una ventana controlada hacia esos datos privados: los getters los leen y los setters los modifican con validación.

🔐 Analogía: la caja fuerte del banco

🚫 Acceso directo — PROHIBIDO

Como intentar abrir la caja fuerte con un mazo. Viola el encapsulamiento; si alguien escribe cuenta.__saldo = -99999, nadie lo valida.

✅ Getter — leer a través de ventanilla

El empleado (getter) te muestra el saldo cuando lo pides: cuenta.get_saldo(). El dato sale pero el original permanece intacto.

✅ Setter — depositar con control

Solo puedes modificar el saldo si cumples las reglas: cuenta.set_saldo(500) valida que el valor sea positivo antes de guardarlo.

🔬 Anatomía de getter y setter
Python
class CuentaBancaria:  # Define la clase CuentaBancaria
    def __init__(self, titular, saldo):  # Constructor: inicializa el objeto con los datos recibidos
        self.__titular = titular        # __ = privado: nadie accede desde fuera
        self.__saldo   = saldo          # __ = privado: solo accessible por get/set

    # ── GETTER ────────────────────────────────────────────
    def get_titular(self):             # Nombre: get_ + nombre del atributo (convención)
        return self.__titular         # Solo devuelve — no modifica nada

    def get_saldo(self):  # Getter: devuelve el valor de saldo
        return self.__saldo  # Retorna el valor calculado al lugar donde se llamó este método

    # ── SETTER ────────────────────────────────────────────
    def set_saldo(self, nuevo_saldo):   # Nombre: set_ + nombre del atributo
        if nuevo_saldo >= 0:           # VALIDACIÓN: regla de negocio antes de guardar
            self.__saldo = nuevo_saldo  # Solo modifica si pasa la validación
        else:  # Se ejecuta si la condición anterior no se cumplió
            print("⚠️ Saldo no puede ser negativo")  # Rechaza el valor inválido

    def __str__(self):  # Define cómo se muestra el objeto al usar print()
        return f"[{self.__titular}] Saldo: ${self.__saldo:,.2f}"  # Retorna el valor calculado al lugar donde se llamó este método

# ── USO ────────────────────────────────────────────────
cuenta = CuentaBancaria("Ana López", 1000)  # Asigna un valor inicial o calculado a cuenta

print(cuenta.get_titular())          # → "Ana López"   (getter lee el dato)
print(cuenta.get_saldo())            # → 1000          (getter lee el dato)

cuenta.set_saldo(1500)               # ✅ válido: 1500 >= 0 → se actualiza
cuenta.set_saldo(-200)               # ⚠️ rechazado: imprime advertencia, saldo queda en 1500

print(cuenta.get_saldo())            # → 1500 (el saldo negativo fue bloqueado)
print(cuenta)                        # → [Ana López] Saldo: $1,500.00
⚖️ Con y sin getters/setters

❌ Sin encapsulamiento — peligroso

Python
class Producto:  # Define la clase Producto
    def __init__(self, precio):  # Constructor: inicializa el objeto con los datos recibidos
        self.precio = precio   # público

p = Producto(100)  # Asigna un valor inicial o calculado a p
p.precio = -500          # ⚠️ nadie lo valida
p.precio = "gratis"      # ⚠️ tipo incorrecto
print(p.precio)           # → "gratis" 💥

El atributo público puede recibir cualquier valor sin control: números negativos, strings, None. Errores silenciosos que explotan en otro lugar del código.

✅ Con getter/setter — controlado

Python
class Producto:  # Define la clase Producto
    def __init__(self, precio):  # Constructor: inicializa el objeto con los datos recibidos
        self.__precio = precio  # privado

    def get_precio(self):  # Getter: devuelve el valor de precio
        return self.__precio  # Retorna el valor calculado al lugar donde se llamó este método

    def set_precio(self, v):  # Setter: valida y actualiza el atributo precio
        if isinstance(v, (int, float)) and v > 0:  # Verifica isinstance(v, (int, float)) and v > 0 antes de continuar
            self.__precio = v    # ✅ válido
        else:  # Se ejecuta si la condición anterior no se cumplió
            print("Precio inválido")  # Muestra en consola el resultado de esta operación

Ahora el setter valida tipo (número) y rango (positivo) antes de guardar. La clase controla sus propios datos — eso es encapsulamiento real.

📐 Reglas de diseño — ¿cuándo poner getter/setter?
Situación ¿Getter? ¿Setter? Razón
Dato que otros necesitan leer ✅ Sí Sin getter nadie puede mostrar el dato
Dato que puede cambiar con reglas ✅ Sí ✅ Sí Setter valida antes de modificar
Dato calculado (no almacenado) ✅ Sí ❌ No Ej: get_total() calcula y devuelve; no hay nada que "guardar"
Dato inmutable (ID, ISBN, matrícula) ✅ Sí ❌ No Solo se asigna en __init__, nunca cambia
Dato completamente interno ❌ No ❌ No Nadie fuera de la clase debe ver ni tocar ese dato
📚 Ejemplo real — Ejercicio Biblioteca

En el ejercicio de Biblioteca, la clase Libro tiene atributos privados __titulo, __autor e __isbn. Solo se definen getters (sin setters) porque estos datos no deben cambiar tras crearse el libro.

Python — biblioteca.py
class Libro:  # Define la clase Libro
    def __init__(self, titulo, autor, isbn):  # Constructor: inicializa el objeto con los datos recibidos
        self.__titulo = titulo    # privado — nunca debe cambiar
        self.__autor  = autor  # Guarda __autor como atributo privado de la instancia
        self.__isbn   = isbn  # Guarda __isbn como atributo privado de la instancia

    # Solo GETTERS — sin setters porque titulo/autor/isbn son inmutables
    def get_titulo(self): return self.__titulo   # ← getter
    def get_autor(self):  return self.__autor    # ← getter
    def get_isbn(self):   return self.__isbn     # ← getter

# ── Por qué no hay set_titulo() ──────────────────────────
# Un libro NO cambia su título después de imprimirse.
# Si no hay setter → el atributo es de solo lectura.

libro = Libro("POO con Python", "J. García", "978-001")  # Asigna un valor inicial o calculado a libro
print(libro.get_titulo())         # → "POO con Python"
# libro.__titulo = "Hacking"     # ❌ AttributeError — privado
# libro.set_titulo("Hacking")    # ❌ no existe el setter
📊 Resumen — getter vs setter vs acceso directo
Elemento Sintaxis ¿Puede leer? ¿Puede escribir? ¿Valida?
Atributo público self.nombre ❌ Nunca
Atributo privado directo self.__nombre ✅ (solo dentro) ✅ (solo dentro)
Getter def get_x(self): return self.__x ✅ Desde fuera
Setter def set_x(self, v): if ... self.__x=v ✅ Con control ✅ Siempre

📖 GLOSARIO

getter
Método que devuelve el valor de un atributo privado. Nombre convencional: get_nombreAtributo().
setter
Método que modifica un atributo privado previa validación. Nombre: set_nombreAtributo(valor).
read-only
Atributo con getter pero sin setter — solo se puede leer, nunca cambiar desde fuera.
write-once
Atributo que se asigna en __init__ y no tiene setter — como ISBN o matrícula.

CONVENCIONES

  • get_x(self) sin parámetros extra — solo self
  • set_x(self, valor) recibe el nuevo valor como parámetro
  • El nombre x en get_x y set_x debe coincidir con el atributo privado __x
  • Un getter nunca tiene efectos secundarios — solo lee y devuelve
  • Un setter siempre debería validar antes de guardar
⚠️ Error común: confundir el nombre del parámetro del setter con el nombre del atributo privado. Escribe def set_saldo(self, nuevo_saldo) y dentro self.__saldo = nuevo_saldo. Si usas el mismo nombre — def set_saldo(self, __saldo) — Python lo trata como variable local y el atributo interno nunca se actualiza.
🚀 Si ya entiendes esta sección, ahora puedes leer los ejercicios de tienda, nómina, universidad, veterinaria, biblioteca y figuras con una mirada mucho más profesional: ya no solo ves "código que funciona", sino herramientas del lenguaje trabajando juntas.
G

Clases Abstractas — from abc import ABC, abstractmethod

Polimorfismo & Contrato

G.1 — ¿Qué es una clase abstracta?

Una clase abstracta es una clase que define un contrato: declara qué métodos deben existir en todas sus subclases, pero no los implementa. Nadie puede crear un objeto directamente de una clase abstracta; solo sus subclases concretas (que implementen todos sus métodos abstractos) pueden instanciarse. En Python esto se logra con el módulo estándar abc.

📦 Módulo ABC

  • ABC — clase base que activa el mecanismo abstracto.
  • @abstractmethod — decorador que marca un método como obligatorio en subclases.
  • Si una subclase no implementa todos los métodos abstractos, Python lanza TypeError al intentar instanciarla.
  • La verificación ocurre en tiempo de ejecución, no en compilación.

Ventajas clave

  • Garantiza que todas las subclases tienen la misma interfaz.
  • El error se detecta temprano (al crear el objeto), no cuando ya el código lo usa.
  • Facilita el polimorfismo: puedes tratar objetos distintos de forma uniforme.
  • Documenta el diseño: la clase abstracta es el "plano" del sistema.

📖 Glosario

ABC
Abstract Base Class — clase base abstracta del módulo abc.
@abstractmethod
Decorador que obliga a la subclase a sobreescribir el método.
Contrato
Conjunto de métodos que toda subclase promete implementar.
Clase concreta
Subclase que implementa todos los métodos abstractos y puede instanciarse.
TypeError
Error de Python al intentar instanciar una clase abstracta incompleta.

G.2 — Ejercicio completo: Sistema de Servicios del Hotel (con ABC)

El hotel ofrece distintos servicios: habitación, restaurante y spa. Todos deben poder calcular su costo y mostrar una descripción. Usamos una clase abstracta para garantizar que cualquier servicio nuevo que se agregue en el futuro respete esa interfaz.

🏗️ Estructura del ejercicio — Diagrama de clases
ClaseTipoMétodos abstractos que debe implementar
ServicioAbstracta (ABC)calcular_costo(), descripcion()
HabitacionConcreta✅ implementa ambos
RestauranteConcreta✅ implementa ambos
SpaConcreta✅ implementa ambos
Python — con ABC y abstractmethod
# ── Importación del módulo abc ──────────────────────────────────────────
from abc import ABC, abstractmethod  # Importa ABC, abstractmethod del módulo abc

# ── Clase abstracta: define el CONTRATO ─────────────────────────────────
class Servicio(ABC):                # hereda de ABC para activar el mecanismo
    """Clase base abstracta para todos los servicios del hotel."""  # Continúa la lógica del ejemplo paso a paso

    def __init__(self, nombre: str):  # Constructor: inicializa el objeto con los datos recibidos
        self.__nombre = nombre           # atributo privado compartido

    def get_nombre(self) -> str:  # Getter: devuelve el valor de nombre
        return self.__nombre  # Retorna el valor calculado al lugar donde se llamó este método

    @abstractmethod                  # ← método OBLIGATORIO en subclases
    def calcular_costo(self) -> float:  # Método calcular_costo(): acción que puede realizar este objeto
        """Retorna el costo total del servicio."""  # Continúa la lógica del ejemplo paso a paso
        ...                          # el cuerpo puede ser ... o pass

    @abstractmethod                  # ← otro método OBLIGATORIO
    def descripcion(self) -> str:  # Método descripcion(): acción que puede realizar este objeto
        """Retorna una descripción legible del servicio."""  # Continúa la lógica del ejemplo paso a paso
        ...  # Continúa la lógica del ejemplo paso a paso

    def recibo(self) -> str:          # método CONCRETO: usa los abstractos
        return (  # Retorna el valor calculado al lugar donde se llamó este método
            f"  Servicio : {self.get_nombre()}\n"  # Continúa la lógica del ejemplo paso a paso
            f"  Detalle  : {self.descripcion()}\n"  # Continúa la lógica del ejemplo paso a paso
            f"  Costo    : ${self.calcular_costo():.2f}"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores

# ── Clase concreta 1: Habitacion ─────────────────────────────────────────
class Habitacion(Servicio):  # Habitacion hereda de Servicio — recibe todos sus atributos y métodos
    def __init__(self, numero: int, noches: int, tarifa_noche: float):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(f"Habitación {numero}")  # Llama al constructor de la clase padre para inicializar atributos heredados
        self.__noches       = noches  # Guarda __noches como atributo privado de la instancia
        self.__tarifa_noche = tarifa_noche  # Guarda __tarifa_noche como atributo privado de la instancia

    def calcular_costo(self) -> float:   # ✅ implementa contrato
        return self.__noches * self.__tarifa_noche  # Retorna el valor calculado al lugar donde se llamó este método

    def descripcion(self) -> str:        # ✅ implementa contrato
        return f"{self.__noches} noche(s) × ${self.__tarifa_noche:.2f}/noche"  # Retorna el valor calculado al lugar donde se llamó este método

# ── Clase concreta 2: Restaurante ────────────────────────────────────────
class Restaurante(Servicio):  # Restaurante hereda de Servicio — recibe todos sus atributos y métodos
    def __init__(self, comensales: int, precio_cubierto: float):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__("Restaurante del Hotel")  # Llama al constructor de la clase padre para inicializar atributos heredados
        self.__comensales     = comensales  # Guarda __comensales como atributo privado de la instancia
        self.__precio_cubierto = precio_cubierto  # Guarda __precio_cubierto como atributo privado de la instancia

    def calcular_costo(self) -> float:  # Método calcular_costo(): acción que puede realizar este objeto
        return self.__comensales * self.__precio_cubierto  # Retorna el valor calculado al lugar donde se llamó este método

    def descripcion(self) -> str:  # Método descripcion(): acción que puede realizar este objeto
        return f"{self.__comensales} comensal(es) × ${self.__precio_cubierto:.2f}/cubierto"  # Retorna el valor calculado al lugar donde se llamó este método

# ── Clase concreta 3: Spa ────────────────────────────────────────────────
class Spa(Servicio):  # Spa hereda de Servicio — recibe todos sus atributos y métodos
    def __init__(self, tratamiento: str, duracion_min: int, tarifa_hora: float):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__("Spa & Bienestar")  # Llama al constructor de la clase padre para inicializar atributos heredados
        self.__tratamiento  = tratamiento  # Guarda __tratamiento como atributo privado de la instancia
        self.__duracion_min = duracion_min  # Guarda __duracion_min como atributo privado de la instancia
        self.__tarifa_hora  = tarifa_hora  # Guarda __tarifa_hora como atributo privado de la instancia

    def calcular_costo(self) -> float:  # Método calcular_costo(): acción que puede realizar este objeto
        return (self.__duracion_min / 60) * self.__tarifa_hora  # Retorna el valor calculado al lugar donde se llamó este método

    def descripcion(self) -> str:  # Método descripcion(): acción que puede realizar este objeto
        return f"{self.__tratamiento} — {self.__duracion_min} min a ${self.__tarifa_hora:.2f}/h"  # Retorna el valor calculado al lugar donde se llamó este método

# ── Uso: polimorfismo con lista ──────────────────────────────────────────
servicios = [  # Asigna un valor inicial o calculado a servicios
    Habitacion(101, 3, 1200.00),  # Continúa la lógica del ejemplo paso a paso
    Restaurante(2, 350.00),  # Continúa la lógica del ejemplo paso a paso
    Spa("Masaje sueco", 90, 800.00),  # Continúa la lógica del ejemplo paso a paso
]  # Cierra la estructura abierta en las líneas anteriores

print("═══ FACTURA DE SERVICIOS ═══")  # Muestra en consola el resultado de esta operación
total = 0.0  # Asigna un valor inicial o calculado a total
for s in servicios:  # Itera sobre cada s en servicios
    print(s.recibo())  # Muestra en consola el resultado de esta operación
    print("  ─────────────────────")  # Muestra en consola el resultado de esta operación
    total += s.calcular_costo()  # Completa la llamada o expresión iniciada anteriormente
print(f"  TOTAL A PAGAR: ${total:.2f}")  # Muestra en consola el resultado de esta operación

# ── ¿Qué pasa si intentamos instanciar Servicio directamente? ────────────
try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
    mal = Servicio("Prueba")        # ❌ TypeError — es abstracta
except TypeError as e:  # Captura el error TypeError as e si ocurre en el try
    print(f"\nError esperado: {e}")  # Muestra en consola el resultado de esta operación
📋 Salida esperada al ejecutar el código
═══ FACTURA DE SERVICIOS ═══
  Servicio : Habitación 101
  Detalle  : 3 noche(s) × $1200.00/noche
  Costo    : $3600.00
  ─────────────────────
  Servicio : Restaurante del Hotel
  Detalle  : 2 comensal(es) × $350.00/cubierto
  Costo    : $700.00
  ─────────────────────
  Servicio : Spa & Bienestar
  Detalle  : Masaje sueco — 90 min a $800.00/h
  Costo    : $1200.00
  ─────────────────────
  TOTAL A PAGAR: $5500.00

Error esperado: Can't instantiate abstract class Servicio
without an implementation for abstract methods 'calcular_costo', 'descripcion'
🔬 Anatomía línea a línea — puntos clave
Línea / ElementoExplicación
from abc import ABC, abstractmethod Importa el mecanismo abstracto del módulo estándar de Python. No necesitas instalar nada.
class Servicio(ABC): Al heredar de ABC, Servicio se convierte en clase abstracta. Intentar instanciarla directamente lanzará TypeError.
@abstractmethod Decorador que marca el método como abstracto. La subclase debe sobreescribirlo o no podrá instanciarse.
def recibo(self) en Servicio Método concreto en la clase abstracta — se hereda tal cual. Llama a calcular_costo() y descripcion() aunque no estén definidos aquí; cada subclase los provee.
super().__init__(...) Llama al constructor de Servicio desde la subclase para inicializar el atributo __nombre.
for s in servicios: Polimorfismo en acción: el mismo for llama a recibo() sobre objetos de tipos distintos; Python despacha el método correcto para cada uno.
💡 El método recibo() está definido una sola vez en la clase abstracta y ya funciona para todos los servicios presentes y futuros. Esto es el principio Open/Closed: abierto para extender (crear un nuevo servicio), cerrado para modificar (no tocas recibo()).

G.3 — El mismo ejercicio SIN ABC ni abstractmethod

Antes de que existiera (o se conociera) el módulo abc, los desarrolladores simulaban el comportamiento abstracto usando raise NotImplementedError. El resultado funcional es similar, pero las diferencias son importantes.

Python — SIN ABC (simulación manual)
# ── Sin importar abc — el "contrato" es una convención, no una ley ───────

class Servicio:                      # clase normal, NO hereda de ABC
    """Clase base 'pseudo-abstracta' — sin mecanismo real de Python."""  # Continúa la lógica del ejemplo paso a paso

    def __init__(self, nombre: str):  # Constructor: inicializa el objeto con los datos recibidos
        self.__nombre = nombre  # Guarda __nombre como atributo privado de la instancia

    def get_nombre(self) -> str:  # Getter: devuelve el valor de nombre
        return self.__nombre  # Retorna el valor calculado al lugar donde se llamó este método

    def calcular_costo(self) -> float:  # Método calcular_costo(): acción que puede realizar este objeto
        # En lugar de @abstractmethod, lanzamos el error manualmente
        raise NotImplementedError(  # Lanza una excepción para señalar un error y detener el flujo normal
            f"{self.__class__.__name__} debe implementar 'calcular_costo()'"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores

    def descripcion(self) -> str:  # Método descripcion(): acción que puede realizar este objeto
        raise NotImplementedError(  # Lanza una excepción para señalar un error y detener el flujo normal
            f"{self.__class__.__name__} debe implementar 'descripcion()'"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores

    def recibo(self) -> str:  # Método recibo(): acción que puede realizar este objeto
        return (  # Retorna el valor calculado al lugar donde se llamó este método
            f"  Servicio : {self.get_nombre()}\n"  # Continúa la lógica del ejemplo paso a paso
            f"  Detalle  : {self.descripcion()}\n"  # Continúa la lógica del ejemplo paso a paso
            f"  Costo    : ${self.calcular_costo():.2f}"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores

# Las subclases son idénticas al ejemplo anterior ─────────────────────────
class Habitacion(Servicio):  # Habitacion hereda de Servicio — recibe todos sus atributos y métodos
    def __init__(self, numero, noches, tarifa_noche):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(f"Habitación {numero}")  # Llama al constructor de la clase padre para inicializar atributos heredados
        self.__noches       = noches  # Guarda __noches como atributo privado de la instancia
        self.__tarifa_noche = tarifa_noche  # Guarda __tarifa_noche como atributo privado de la instancia

    def calcular_costo(self):  # Método calcular_costo(): acción que puede realizar este objeto
        return self.__noches * self.__tarifa_noche  # Retorna el valor calculado al lugar donde se llamó este método

    def descripcion(self):  # Método descripcion(): acción que puede realizar este objeto
        return f"{self.__noches} noche(s) × ${self.__tarifa_noche:.2f}/noche"  # Retorna el valor calculado al lugar donde se llamó este método

# ── DIFERENCIA CLAVE: ¿qué pasa si NO implementamos descripcion? ─────────
class ServicioIncompleto(Servicio):   # olvida implementar ambos métodos
    def __init__(self):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__("Incompleto")  # Llama al constructor de la clase padre para inicializar atributos heredados

# Sin ABC: Python permite crear el objeto sin error ──────────────────────
obj_malo = ServicioIncompleto()   # ⚠️ NO lanza error aquí...
print("Objeto creado sin error (sin ABC).")  # Muestra en consola el resultado de esta operación

try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
    obj_malo.calcular_costo()     # ❌ el error llega TARDE, al llamar el método
except NotImplementedError as e:  # Captura el error NotImplementedError as e si ocurre en el try
    print(f"Error tardío: {e}")  # Muestra en consola el resultado de esta operación

# Uso normal — igual que con ABC ─────────────────────────────────────────
h = Habitacion(202, 2, 950.00)  # Asigna un valor inicial o calculado a h
print(h.recibo())  # Muestra en consola el resultado de esta operación
📋 Salida del ejemplo SIN ABC
Objeto creado sin error (sin ABC).
Error tardío: ServicioIncompleto debe implementar 'calcular_costo()'
  Servicio : Habitación 202
  Detalle  : 2 noche(s) × $950.00/noche
  Costo    : $1900.00
⚠️ Observa la diferencia: sin ABC, el objeto ServicioIncompleto() se crea sin ningún error. El error solo aparece cuando intentas usar el método. En un sistema grande, ese error puede surgir mucho después, en una parte del código completamente diferente, lo que hace el bug muy difícil de rastrear.

G.4 — Comparación: ABC + @abstractmethod vs. raise NotImplementedError

📊 Tabla comparativa detallada
Aspecto ✅ Con ABC ⚠️ Sin ABC (NotImplementedError)
¿Cuándo detecta el error? Al instanciar — inmediatamente Al llamar el método — tarde
¿Se puede instanciar la base? No — Python lo impide con TypeError Sí — nadie lo impide
¿Requiere importación? from abc import ABC, abstractmethod No — solo Python puro
Documentación del contrato Explícita y verificada por Python Solo convención — depende del programador
Compatibilidad con IDEs / type checkers Total — mypy, PyCharm, VS Code lo entienden Parcial — el IDE no sabe que es "abstracto"
Facilidad de refactorización Alta — Python avisa si olvidas implementar algo Baja — el olvido pasa desapercibido
Verbosidad del código Media — requiere decorador y herencia de ABC Alta — cada método necesita raise manual
¿Cuándo usarlo? Proyectos profesionales, trabajo en equipo, APIs Scripts simples, código legacy, compatibilidad antigua

Usa ABC cuando…

  • Trabajas en un equipo y otros programadores heredarán tu clase.
  • Quieres que Python verifique automáticamente el contrato.
  • Usas un IDE moderno o type checker (mypy).
  • Construyes una librería o framework que otros van a extender.
  • Es el estándar profesional en Python moderno.

⚠️ NotImplementedError puede ser útil cuando…

  • Trabajas en código muy simple o scripts de una sola persona.
  • Necesitas compatibilidad con Python 2 (muy raro hoy).
  • Quieres dar un mensaje de error más descriptivo que el de ABC.
  • Estás en código heredado (legacy) que no usa ABC.
🏆 Conclusión para el alumno: En proyectos universitarios y profesionales, siempre usa ABC. Es más seguro, más legible y es la forma estándar en Python moderno. El enfoque con NotImplementedError sirve para entender qué hace ABC internamente, pero no lo uses como primera opción.
H

Getters y Setters — Ejercicios Prácticos

Encapsulamiento

H.1 — Repaso rápido

Un getter es un método que lee un atributo privado. Un setter es un método que modifica un atributo privado tras validarlo. El objetivo es proteger los datos internos del objeto y mantener su consistencia.

H.2 — Ejercicio 1: CuentaBancaria

Una cuenta bancaria tiene un saldo privado. Se puede consultar (getter), depositar y retirar (setter con validación). El saldo nunca puede ser negativo.

Python — CuentaBancaria
class CuentaBancaria:  # Define la clase CuentaBancaria
    """Cuenta bancaria con saldo protegido por getter y setters."""  # Continúa la lógica del ejemplo paso a paso

    def __init__(self, titular: str, saldo_inicial: float):  # Constructor: inicializa el objeto con los datos recibidos
        self.__titular = titular  # Guarda __titular como atributo privado de la instancia
        if saldo_inicial < 0:  # Verifica saldo_inicial < 0 antes de continuar
            raise ValueError("El saldo inicial no puede ser negativo.")  # Lanza una excepción para señalar un error y detener el flujo normal
        self.__saldo = saldo_inicial       # atributo privado

    # ── Getters ──────────────────────────────────────────────────────────
    def get_titular(self) -> str:  # Getter: devuelve el valor de titular
        return self.__titular  # Retorna el valor calculado al lugar donde se llamó este método

    def get_saldo(self) -> float:  # Getter: devuelve el valor de saldo
        return self.__saldo  # Retorna el valor calculado al lugar donde se llamó este método

    # ── Setters con validación ────────────────────────────────────────────
    def depositar(self, monto: float) -> None:  # Método depositar(): acción que puede realizar este objeto
        if monto <= 0:  # Verifica monto <= 0 antes de continuar
            raise ValueError("El monto a depositar debe ser positivo.")  # Lanza una excepción para señalar un error y detener el flujo normal
        self.__saldo += monto  # Continúa la lógica del ejemplo paso a paso
        print(f"  ✅ Depósito de ${monto:,.2f} realizado. Saldo: ${self.__saldo:,.2f}")  # Muestra en consola el resultado de esta operación

    def retirar(self, monto: float) -> None:  # Método retirar(): acción que puede realizar este objeto
        if monto <= 0:  # Verifica monto <= 0 antes de continuar
            raise ValueError("El monto a retirar debe ser positivo.")  # Lanza una excepción para señalar un error y detener el flujo normal
        if monto > self.__saldo:  # Verifica monto > self.__saldo antes de continuar
            raise ValueError(f"Fondos insuficientes. Saldo disponible: ${self.__saldo:,.2f}")  # Lanza una excepción para señalar un error y detener el flujo normal
        self.__saldo -= monto  # Continúa la lógica del ejemplo paso a paso
        print(f"  ✅ Retiro de ${monto:,.2f} realizado. Saldo: ${self.__saldo:,.2f}")  # Muestra en consola el resultado de esta operación

    def estado(self) -> str:  # Método estado(): acción que puede realizar este objeto
        return f"Cuenta de {self.__titular} | Saldo: ${self.__saldo:,.2f}"  # Retorna el valor calculado al lugar donde se llamó este método

# ── Uso ──────────────────────────────────────────────────────────────────
cuenta = CuentaBancaria("Ana López", 5000.00)  # Asigna un valor inicial o calculado a cuenta
print("Estado inicial:", cuenta.get_saldo())  # Muestra en consola el resultado de esta operación

cuenta.depositar(2000)  # Completa la llamada o expresión iniciada anteriormente
cuenta.retirar(800)  # Completa la llamada o expresión iniciada anteriormente

try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
    cuenta.retirar(99999)                # ❌ intento de sobregiro
except ValueError as e:  # Captura el error ValueError as e si ocurre en el try
    print(f"  ❌ Error: {e}")  # Muestra en consola el resultado de esta operación

print(cuenta.estado())  # Muestra en consola el resultado de esta operación

# Acceso directo al atributo privado — Python lo rechaza ─────────────────
try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
    cuenta.__saldo = -9999             # ⚠️ crea un atributo nuevo, no modifica el real
    print("Saldo real:", cuenta.get_saldo())  # el __saldo real no cambió
except AttributeError as e:  # Captura el error AttributeError as e si ocurre en el try
    print(f"  ❌ {e}")  # Muestra en consola el resultado de esta operación
📋 Salida esperada — CuentaBancaria
Estado inicial: 5000.0
  ✅ Depósito de $2,000.00 realizado. Saldo: $7,000.00
  ✅ Retiro de $800.00 realizado. Saldo: $6,200.00
  ❌ Error: Fondos insuficientes. Saldo disponible: $6,200.00
Cuenta de Ana López | Saldo: $6,200.00
Saldo real: 6200.0
💡 El último bloque demuestra el name mangling de Python: al escribir cuenta.__saldo = -9999 fuera de la clase, Python crea un atributo nuevo llamado __saldo en el objeto, pero el atributo real internamente se llama _CuentaBancaria__saldo. Por eso get_saldo() sigue devolviendo el valor correcto.

H.3 — Ejercicio 2: Empleado del Hotel

Un empleado del hotel tiene nombre, puesto y salario. El salario no puede ser negativo y el nombre no puede estar vacío. El puesto es read-only (solo getter, sin setter).

Python — Empleado
class Empleado:  # Define la clase Empleado
    """Empleado del hotel con encapsulamiento completo."""  # Continúa la lógica del ejemplo paso a paso

    def __init__(self, nombre: str, puesto: str, salario: float):  # Constructor: inicializa el objeto con los datos recibidos
        self.set_nombre(nombre)     # reutiliza la validación del setter
        self.__puesto  = puesto     # read-only: solo getter
        self.set_salario(salario)  # Completa la llamada o expresión iniciada anteriormente

    # ── Getters ──────────────────────────────────────────────────────────
    def get_nombre(self)  -> str:   return self.__nombre  # Getter: devuelve el valor de nombre
    def get_puesto(self)  -> str:   return self.__puesto   # read-only
    def get_salario(self) -> float: return self.__salario  # Getter: devuelve el valor de salario

    # ── Setters con validación ────────────────────────────────────────────
    def set_nombre(self, nuevo_nombre: str) -> None:  # Setter: valida y actualiza el atributo nombre
        nuevo_nombre = nuevo_nombre.strip()  # Asigna un valor inicial o calculado a nuevo_nombre
        if not nuevo_nombre:  # Verifica not nuevo_nombre antes de continuar
            raise ValueError("El nombre no puede estar vacío.")  # Lanza una excepción para señalar un error y detener el flujo normal
        self.__nombre = nuevo_nombre  # Guarda __nombre como atributo privado de la instancia

    def set_salario(self, nuevo_salario: float) -> None:  # Setter: valida y actualiza el atributo salario
        if nuevo_salario < 0:  # Verifica nuevo_salario < 0 antes de continuar
            raise ValueError(f"El salario no puede ser negativo. Recibido: {nuevo_salario}")  # Lanza una excepción para señalar un error y detener el flujo normal
        self.__salario = nuevo_salario  # Guarda __salario como atributo privado de la instancia

    def aplicar_aumento(self, porcentaje: float) -> None:  # Método aplicar_aumento(): acción que puede realizar este objeto
        if porcentaje <= 0:  # Verifica porcentaje <= 0 antes de continuar
            raise ValueError("El porcentaje de aumento debe ser positivo.")  # Lanza una excepción para señalar un error y detener el flujo normal
        aumento = self.__salario * (porcentaje / 100)  # Asigna un valor inicial o calculado a aumento
        self.__salario += aumento  # Continúa la lógica del ejemplo paso a paso
        print(f"  Aumento del {porcentaje}%: +${aumento:,.2f} → Nuevo salario: ${self.__salario:,.2f}")  # Muestra en consola el resultado de esta operación

    def __str__(self) -> str:  # Define cómo se muestra el objeto al usar print()
        return f"[{self.__puesto}] {self.__nombre} — Salario: ${self.__salario:,.2f}"  # Retorna el valor calculado al lugar donde se llamó este método

# ── Uso ──────────────────────────────────────────────────────────────────
emp = Empleado("Carlos Ruiz", "Recepcionista", 18000.00)  # Asigna un valor inicial o calculado a emp
print(emp)  # Muestra en consola el resultado de esta operación
emp.aplicar_aumento(10)  # Completa la llamada o expresión iniciada anteriormente
emp.set_nombre("Carlos A. Ruiz")  # Completa la llamada o expresión iniciada anteriormente
print(emp)  # Muestra en consola el resultado de esta operación

try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
    emp.set_salario(-5000)             # ❌ salario negativo
except ValueError as e:  # Captura el error ValueError as e si ocurre en el try
    print(f"  ❌ {e}")  # Muestra en consola el resultado de esta operación
📋 Salida esperada — Empleado
[Recepcionista] Carlos Ruiz — Salario: $18,000.00
  Aumento del 10%: +$1,800.00 → Nuevo salario: $19,800.00
[Recepcionista] Carlos A. Ruiz — Salario: $19,800.00
  ❌ El salario no puede ser negativo. Recibido: -5000

H.4 — Ejercicio 3: Reservacion con múltiples atributos privados

Una reservación tiene número de habitación, nombre del huésped y cantidad de noches. La cantidad de noches debe ser entre 1 y 30. El número de habitación es write-once (se asigna al crear, no tiene setter).

Python — Reservacion
class Reservacion:  # Define la clase Reservacion
    """Reservación hotelera con encapsulamiento de todos sus atributos."""  # Continúa la lógica del ejemplo paso a paso

    def __init__(self, num_habitacion: int, huesped: str, noches: int):  # Constructor: inicializa el objeto con los datos recibidos
        self.__num_habitacion = num_habitacion   # write-once: sin setter
        self.set_huesped(huesped)  # Completa la llamada o expresión iniciada anteriormente
        self.set_noches(noches)  # Completa la llamada o expresión iniciada anteriormente

    # ── Getters ──────────────────────────────────────────────────────────
    def get_num_habitacion(self) -> int:   return self.__num_habitacion  # Getter: devuelve el valor de num_habitacion
    def get_huesped(self)       -> str:   return self.__huesped  # Getter: devuelve el valor de huesped
    def get_noches(self)        -> int:   return self.__noches  # Getter: devuelve el valor de noches

    # ── Setters ──────────────────────────────────────────────────────────
    def set_huesped(self, nombre: str) -> None:  # Setter: valida y actualiza el atributo huesped
        nombre = nombre.strip()  # Asigna un valor inicial o calculado a nombre
        if not nombre:  # Verifica not nombre antes de continuar
            raise ValueError("El nombre del huésped no puede estar vacío.")  # Lanza una excepción para señalar un error y detener el flujo normal
        self.__huesped = nombre  # Guarda __huesped como atributo privado de la instancia

    def set_noches(self, cantidad: int) -> None:  # Setter: valida y actualiza el atributo noches
        if not (1 <= cantidad <= 30):  # Verifica not (1 <= cantidad <= 30) antes de continuar
            raise ValueError(f"Las noches deben estar entre 1 y 30. Recibido: {cantidad}")  # Lanza una excepción para señalar un error y detener el flujo normal
        self.__noches = cantidad  # Guarda __noches como atributo privado de la instancia

    def costo_estimado(self, tarifa_noche: float = 1200.0) -> float:  # Método costo_estimado(): acción que puede realizar este objeto
        return self.__noches * tarifa_noche  # Retorna el valor calculado al lugar donde se llamó este método

    def __str__(self) -> str:  # Define cómo se muestra el objeto al usar print()
        return (  # Retorna el valor calculado al lugar donde se llamó este método
            f"Reservación | Hab: {self.__num_habitacion} | "  # Continúa la lógica del ejemplo paso a paso
            f"Huésped: {self.__huesped} | Noches: {self.__noches} | "  # Continúa la lógica del ejemplo paso a paso
            f"Costo est.: ${self.costo_estimado():,.2f}"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores

# ── Uso ──────────────────────────────────────────────────────────────────
r = Reservacion(305, "María García", 3)  # Asigna un valor inicial o calculado a r
print(r)  # Muestra en consola el resultado de esta operación

r.set_noches(5)           # extensión de estancia
r.set_huesped("María G. Pérez")  # Completa la llamada o expresión iniciada anteriormente
print(r)  # Muestra en consola el resultado de esta operación

try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
    r.set_noches(0)         # ❌ valor fuera de rango
except ValueError as e:  # Captura el error ValueError as e si ocurre en el try
    print(f"  ❌ {e}")  # Muestra en consola el resultado de esta operación

try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
    r.set_noches(45)        # ❌ demasiadas noches
except ValueError as e:  # Captura el error ValueError as e si ocurre en el try
    print(f"  ❌ {e}")  # Muestra en consola el resultado de esta operación
📋 Salida esperada — Reservacion
Reservación | Hab: 305 | Huésped: María García | Noches: 3 | Costo est.: $3,600.00
Reservación | Hab: 305 | Huésped: María G. Pérez | Noches: 5 | Costo est.: $6,000.00
  ❌ Las noches deben estar entre 1 y 30. Recibido: 0
  ❌ Las noches deben estar entre 1 y 30. Recibido: 45
🔑 Patrón write-once: El atributo __num_habitacion no tiene setter. Una vez creada la reservación, el número de habitación no puede cambiar — si se necesita otro número, se crea una nueva reservación. Este patrón es ideal para identificadores que no deben mutar (ISBN, matrícula, número de factura).
I

Listas y Diccionarios — Almacenamiento con for

Estructuras de Datos

I.1 — ¿Por qué son tan importantes?

En POO, los objetos no existen solos: un hotel tiene muchos huéspedes, muchas reservaciones, muchos servicios. Las listas y los diccionarios son las estructuras que permiten almacenar y recorrer colecciones de objetos de forma eficiente. Son el pegamento que une las clases con el mundo real.

📋 Lista — resumen

  • Colección ordenada de elementos.
  • Permite duplicados.
  • Acceso por índice: lista[0]
  • Métodos clave: append(), remove(), pop(), len()
  • Se itera con for elemento in lista:

📚 Diccionario — resumen

  • Colección de pares clave → valor.
  • Las claves deben ser únicas.
  • Acceso por clave: dic["nombre"]
  • Métodos clave: keys(), values(), items()
  • Se itera con for k, v in dic.items():

📖 Glosario

append(x)
Agrega x al final de la lista.
items()
Devuelve pares (clave, valor) del diccionario.
Comprensión
Forma compacta de crear una lista con un for dentro de [].
Anidado
Diccionario cuyo valor es a su vez otro diccionario o lista.

I.2 — Ejercicio: Lista de Huéspedes del Hotel

Almacena una lista de objetos Huesped, recórrela con for, filtra y transforma. Se cubren: creación, append(), iteración, enumerate() y comprensión de listas.

Python — Lista de Huéspedes
# ── Definición de la clase ───────────────────────────────────────────────
class Huesped:  # Define la clase Huesped
    def __init__(self, nombre: str, habitacion: int, noches: int):  # Constructor: inicializa el objeto con los datos recibidos
        self.nombre     = nombre  # Guarda nombre como atributo de esta instancia
        self.habitacion = habitacion  # Guarda habitacion como atributo de esta instancia
        self.noches     = noches  # Guarda noches como atributo de esta instancia

    def costo(self, tarifa: float = 1000.0) -> float:  # Método costo(): acción que puede realizar este objeto
        return self.noches * tarifa  # Retorna el valor calculado al lugar donde se llamó este método

    def __str__(self) -> str:  # Define cómo se muestra el objeto al usar print()
        return f"Hab {self.habitacion:>3} | {self.nombre:<20} | {self.noches} noche(s)"  # Retorna el valor calculado al lugar donde se llamó este método

# ── 1. Crear lista vacía y agregar con append() ──────────────────────────
huespedes = []                          # lista vacía

huespedes.append(Huesped("Ana Torres",    101, 3))  # Agrega un elemento al final de huespedes
huespedes.append(Huesped("Luis Ramos",    102, 1))  # Agrega un elemento al final de huespedes
huespedes.append(Huesped("Sandra Paz",    203, 5))  # Agrega un elemento al final de huespedes
huespedes.append(Huesped("Jorge Medina", 204, 2))  # Agrega un elemento al final de huespedes
huespedes.append(Huesped("Laura Soto",   305, 7))  # Agrega un elemento al final de huespedes

# ── 2. Recorrer con for básico ────────────────────────────────────────────
print("═══ LISTA COMPLETA DE HUÉSPEDES ═══")  # Muestra en consola el resultado de esta operación
for h in huespedes:  # Itera sobre cada h en huespedes
    print(f"  {h}")                     # usa __str__ automáticamente

# ── 3. Recorrer con enumerate() — índice + elemento ──────────────────────
print("\n═══ CON NÚMERO DE ORDEN ═══")  # Muestra en consola el resultado de esta operación
for i, h in enumerate(huespedes, start=1):  # Itera sobre cada i, h en enumerate(huespedes, start=1)
    print(f"  {i}. {h.nombre}")  # Muestra en consola el resultado de esta operación

# ── 4. Filtrar con for + if ───────────────────────────────────────────────
print("\n═══ HUÉSPEDES CON MÁS DE 3 NOCHES ═══")  # Muestra en consola el resultado de esta operación
for h in huespedes:  # Itera sobre cada h en huespedes
    if h.noches > 3:  # Verifica h.noches > 3 antes de continuar
        print(f"  {h.nombre} — {h.noches} noches")  # Muestra en consola el resultado de esta operación

# ── 5. Calcular total con for ─────────────────────────────────────────────
total = 0.0  # Asigna un valor inicial o calculado a total
for h in huespedes:  # Itera sobre cada h en huespedes
    total += h.costo()  # Completa la llamada o expresión iniciada anteriormente
print(f"\n  Ingresos totales del hotel: ${total:,.2f}")  # Muestra en consola el resultado de esta operación

# ── 6. Comprensión de lista: obtener solo los nombres ────────────────────
nombres = [h.nombre for h in huespedes]  # Asigna un valor inicial o calculado a nombres
print(f"\n  Nombres (comprensión): {nombres}")  # Muestra en consola el resultado de esta operación

# ── 7. Comprensión de lista con filtro ───────────────────────────────────
estancias_largas = [h.nombre for h in huespedes if h.noches >= 5]  # Asigna un valor inicial o calculado a estancias_largas
print(f"  Estancias ≥ 5 noches: {estancias_largas}")  # Muestra en consola el resultado de esta operación
📋 Salida esperada — Lista de Huéspedes
═══ LISTA COMPLETA DE HUÉSPEDES ═══
  Hab 101 | Ana Torres           | 3 noche(s)
  Hab 102 | Luis Ramos           | 1 noche(s)
  Hab 203 | Sandra Paz           | 5 noche(s)
  Hab 204 | Jorge Medina         | 2 noche(s)
  Hab 305 | Laura Soto           | 7 noche(s)

═══ CON NÚMERO DE ORDEN ═══
  1. Ana Torres
  2. Luis Ramos
  3. Sandra Paz
  4. Jorge Medina
  5. Laura Soto

═══ HUÉSPEDES CON MÁS DE 3 NOCHES ═══
  Sandra Paz — 5 noches
  Laura Soto — 7 noches

  Ingresos totales del hotel: $18,000.00

  Nombres (comprensión): ['Ana Torres', 'Luis Ramos', 'Sandra Paz', 'Jorge Medina', 'Laura Soto']
  Estancias ≥ 5 noches: ['Sandra Paz', 'Laura Soto']
🔬 Anatomía — patrones de iteración con listas
PatrónSintaxisCuándo usarlo
for x in lista for h in huespedes: print(h) Recorrer todos los elementos sin necesitar el índice.
enumerate(lista) for i, h in enumerate(huespedes, 1): Cuando necesitas el número de orden o el índice.
for x in lista if cond for h in huespedes: if h.noches > 3: Filtrar elementos que cumplen una condición.
Comprensión básica [h.nombre for h in huespedes] Extraer una propiedad de todos los objetos en una línea.
Comprensión con filtro [h.nombre for h in huespedes if h.noches >= 5] Extraer + filtrar en una sola expresión concisa.
💡 La comprensión de lista (list comprehension) es Python idiomático. En lugar de escribir un for de 3 líneas con append(), lo expresas en una sola línea. Es más legible, más rápido y más Pythónico. Aprende a leerla de izquierda a derecha: "dame h.nombre por cada h en huespedes si h.noches >= 5".

I.3 — Ejercicio: Diccionario de Inventario de Habitaciones

Usa un diccionario donde la clave es el número de habitación y el valor es su estado. Cubre: creación, acceso, items(), get(), actualización y diccionarios anidados.

Python — Diccionario de Habitaciones
# ── 1. Diccionario simple: clave → valor ─────────────────────────────────
#    clave  = número de habitación (int)
#    valor  = estado (str)
habitaciones = {  # Asigna un valor inicial o calculado a habitaciones
    101: "disponible",  # Continúa la lógica del ejemplo paso a paso
    102: "ocupada",  # Continúa la lógica del ejemplo paso a paso
    103: "mantenimiento",  # Continúa la lógica del ejemplo paso a paso
    201: "disponible",  # Continúa la lógica del ejemplo paso a paso
    202: "ocupada",  # Continúa la lógica del ejemplo paso a paso
    203: "disponible",  # Continúa la lógica del ejemplo paso a paso
}  # Cierra la estructura abierta en las líneas anteriores

# ── 2. Acceso directo por clave ───────────────────────────────────────────
print("Estado hab. 102:", habitaciones[102])  # Muestra en consola el resultado de esta operación

# ── 3. Acceso seguro con get() ─────────────────────────────────────────────
estado = habitaciones.get(999, "no existe")   # clave inexistente
print("Estado hab. 999:", estado)  # Muestra en consola el resultado de esta operación

# ── 4. Recorrer con items() ──────────────────────────────────────────────
print("\n═══ INVENTARIO COMPLETO ═══")  # Muestra en consola el resultado de esta operación
for numero, estado in habitaciones.items():  # Itera sobre cada numero, estado en habitaciones.items()
    print(f"  Hab {numero}: {estado}")  # Muestra en consola el resultado de esta operación

# ── 5. Filtrar con for + if sobre items() ────────────────────────────────
print("\n═══ HABITACIONES DISPONIBLES ═══")  # Muestra en consola el resultado de esta operación
for numero, estado in habitaciones.items():  # Itera sobre cada numero, estado en habitaciones.items()
    if estado == "disponible":  # Verifica estado == "disponible" antes de continuar
        print(f"  ✅ Habitación {numero}")  # Muestra en consola el resultado de esta operación

# ── 6. Actualizar un valor ────────────────────────────────────────────────
habitaciones[201] = "ocupada"             # check-in en hab. 201
print(f"\nHab 201 actualizada: {habitaciones[201]}")  # Muestra en consola el resultado de esta operación

# ── 7. Agregar una habitación nueva ──────────────────────────────────────
habitaciones[301] = "disponible"  # Asigna un valor inicial o calculado a habitaciones[301]
print(f"Total de habitaciones: {len(habitaciones)}")  # Muestra en consola el resultado de esta operación

# ── 8. Comprensión de diccionario: solo las disponibles ──────────────────
disponibles = {num: est for num, est in habitaciones.items() if est == "disponible"}  # Asigna un valor inicial o calculado a disponibles
print(f"\nDisponibles (dict comprehension): {disponibles}")  # Muestra en consola el resultado de esta operación
📋 Salida esperada — Diccionario de Habitaciones
Estado hab. 102: ocupada
Estado hab. 999: no existe

═══ INVENTARIO COMPLETO ═══
  Hab 101: disponible
  Hab 102: ocupada
  Hab 103: mantenimiento
  Hab 201: disponible
  Hab 202: ocupada
  Hab 203: disponible

═══ HABITACIONES DISPONIBLES ═══
  ✅ Habitación 101
  ✅ Habitación 201
  ✅ Habitación 203

Hab 201 actualizada: ocupada
Total de habitaciones: 7
Disponibles (dict comprehension): {101: 'disponible', 203: 'disponible', 301: 'disponible'}

I.4 — Ejercicio: Diccionario Anidado — Registro de Reservaciones

Un diccionario anidado almacena información más rica: la clave es el ID de reservación y el valor es otro diccionario con todos los detalles del huésped. Se cubren: anidamiento, iteración profunda y acceso seguro.

Python — Diccionario Anidado
# ── Diccionario anidado: cada clave es un ID, cada valor es un dict ───────
reservaciones = {  # Asigna un valor inicial o calculado a reservaciones
    "RES-001": {  # Continúa la lógica del ejemplo paso a paso
        "huesped"    : "Ana Torres",  # Continúa la lógica del ejemplo paso a paso
        "habitacion" : 101,  # Continúa la lógica del ejemplo paso a paso
        "noches"     : 3,  # Continúa la lógica del ejemplo paso a paso
        "desayuno"   : True,  # Continúa la lógica del ejemplo paso a paso
    },  # Continúa la lógica del ejemplo paso a paso
    "RES-002": {  # Continúa la lógica del ejemplo paso a paso
        "huesped"    : "Luis Ramos",  # Continúa la lógica del ejemplo paso a paso
        "habitacion" : 204,  # Continúa la lógica del ejemplo paso a paso
        "noches"     : 1,  # Continúa la lógica del ejemplo paso a paso
        "desayuno"   : False,  # Continúa la lógica del ejemplo paso a paso
    },  # Continúa la lógica del ejemplo paso a paso
    "RES-003": {  # Continúa la lógica del ejemplo paso a paso
        "huesped"    : "Sandra Paz",  # Continúa la lógica del ejemplo paso a paso
        "habitacion" : 305,  # Continúa la lógica del ejemplo paso a paso
        "noches"     : 5,  # Continúa la lógica del ejemplo paso a paso
        "desayuno"   : True,  # Continúa la lógica del ejemplo paso a paso
    },  # Continúa la lógica del ejemplo paso a paso
}  # Cierra la estructura abierta en las líneas anteriores

# ── 1. Recorrer con items(): id_reservacion, datos ───────────────────────
TARIFA        = 1200.0  # Asigna un valor inicial o calculado a TARIFA
COSTO_DESAYUNO = 150.0  # Asigna un valor inicial o calculado a COSTO_DESAYUNO

print("═══ RESUMEN DE RESERVACIONES ═══")  # Muestra en consola el resultado de esta operación
for id_res, datos in reservaciones.items():  # Itera sobre cada id_res, datos en reservaciones.items()
    costo = datos["noches"] * TARIFA  # Asigna un valor inicial o calculado a costo
    if datos["desayuno"]:  # Verifica datos["desayuno"] antes de continuar
        costo += datos["noches"] * COSTO_DESAYUNO  # Continúa la lógica del ejemplo paso a paso
    print(f"  {id_res} | {datos['huesped']:<15} | Hab {datos['habitacion']} | ${costo:,.2f}")  # Muestra en consola el resultado de esta operación

# ── 2. Acceder a un dato específico de una reservación ───────────────────
print(f"\nHuésped RES-002: {reservaciones['RES-002']['huesped']}")  # Muestra en consola el resultado de esta operación

# ── 3. Agregar una nueva reservación en tiempo de ejecución ──────────────
reservaciones["RES-004"] = {  # Continúa la lógica del ejemplo paso a paso
    "huesped"    : "Jorge Medina",  # Continúa la lógica del ejemplo paso a paso
    "habitacion" : 102,  # Continúa la lógica del ejemplo paso a paso
    "noches"     : 2,  # Continúa la lógica del ejemplo paso a paso
    "desayuno"   : False,  # Continúa la lógica del ejemplo paso a paso
}  # Cierra la estructura abierta en las líneas anteriores
print(f"\nTotal reservaciones: {len(reservaciones)}")  # Muestra en consola el resultado de esta operación

# ── 4. Filtrar con for: reservaciones CON desayuno ───────────────────────
print("\n═══ RESERVACIONES CON DESAYUNO ═══")  # Muestra en consola el resultado de esta operación
for id_res, datos in reservaciones.items():  # Itera sobre cada id_res, datos en reservaciones.items()
    if datos["desayuno"]:  # Verifica datos["desayuno"] antes de continuar
        print(f"  ☕ {id_res} — {datos['huesped']}")  # Muestra en consola el resultado de esta operación

# ── 5. Comprensión de diccionario ────────────────────────────────────────
# Obtener solo {id: nombre_huesped} para las reservaciones largas
largas = {  # Asigna un valor inicial o calculado a largas
    id_res: datos["huesped"]  # Cierra la lista o el índice construido en las líneas anteriores
    for id_res, datos in reservaciones.items()  # Recorre cada elemento de la colección indicada
    if datos["noches"] >= 3  # Verifica datos["noches"] >= 3 antes de continuar
}  # Cierra la estructura abierta en las líneas anteriores
print(f"\nReservaciones largas (≥3 noches): {largas}")  # Muestra en consola el resultado de esta operación
📋 Salida esperada — Diccionario Anidado
═══ RESUMEN DE RESERVACIONES ═══
  RES-001 | Ana Torres     | Hab 101 | $4,050.00
  RES-002 | Luis Ramos     | Hab 204 | $1,200.00
  RES-003 | Sandra Paz     | Hab 305 | $6,750.00

Huésped RES-002: Luis Ramos

Total reservaciones: 4

═══ RESERVACIONES CON DESAYUNO ═══
  ☕ RES-001 — Ana Torres
  ☕ RES-003 — Sandra Paz

Reservaciones largas (≥3 noches): {'RES-001': 'Ana Torres', 'RES-003': 'Sandra Paz'}
🔬 Anatomía — patrones de iteración con diccionarios
PatrónSintaxis¿Qué devuelve?
for k in dic for num in habitaciones: Solo las claves
dic.values() for est in habitaciones.values(): Solo los valores
dic.items() for num, est in habitaciones.items(): Pares clave, valor — el más usado
dic.get(k, default) habitaciones.get(999, "no existe") Valor o default si la clave no existe — evita KeyError
Dict comprehension {k: v for k, v in dic.items() if cond} Nuevo diccionario filtrado/transformado

📋 ¿Lista o Diccionario?

  • Usa lista cuando el orden importa o tienes una secuencia de objetos del mismo tipo.
  • Usa diccionario cuando necesitas buscar por un identificador único (número de habitación, ID, nombre).
  • Combínalos: diccionario de listas, lista de diccionarios.

Errores comunes

  • KeyError al acceder con dic[clave] si la clave no existe — usa dic.get(clave).
  • IndexError al acceder con lista[i] con un índice fuera de rango.
  • Modificar una lista mientras la iteras con for — itera sobre una copia: for x in lista[:].
  • Confundir append(obj) con extend([obj]).
🏆 Para el alumno: Dominar listas y diccionarios con for es el salto que diferencia al programador principiante del intermedio. En POO, prácticamente toda colección de objetos se almacena en uno de estos dos contenedores. Con las comprensiones, además, puedes filtrar y transformar datos en una sola línea elegante — una habilidad muy valorada en el mundo profesional.
J

Disección profunda — = vs ==, append, self y el método confirmar

Conceptos fundamentales

J.1 — = versus == — son completamente distintos

Este es uno de los errores más frecuentes en programación. Tienen símbolos parecidos pero significados completamente opuestos.

📥 = — Asignación

  • Guarda un valor dentro de una variable.
  • No hace ninguna pregunta — solo almacena.
  • El resultado no es verdadero ni falso.
  • Se usa para crear o actualizar variables y atributos.

== — Comparación

  • Pregunta si dos valores son iguales.
  • Devuelve True o False.
  • No cambia ningún valor — solo evalúa.
  • Se usa en if, while, filtros.

📖 Comparación rápida

=
Escribe / guarda. Siempre cambia datos.
==
Pregunta / compara. Nunca cambia datos.
SyntaxError
Error si usas = dentro de un if.
True/False
El único resultado posible de ==.
Python — = vs ==
# ── = (asignación) — GUARDA un valor ────────────────────────────────────
estado = "disponible"   # la variable 'estado' ahora CONTIENE "disponible"
noches = 3              # la variable 'noches' ahora CONTIENE 3
precio = noches * 1200  # calcula y GUARDA el resultado en 'precio'

# ── == (comparación) — PREGUNTA si dos cosas son iguales ─────────────────
print(estado == "disponible")   # → True  (sí son iguales)
print(estado == "ocupada")      # → False (no son iguales)
print(noches == 5)              # → False
print(noches == 3)              # → True

# ── En un if siempre usamos == ───────────────────────────────────────────
if estado == "disponible":       # ✅ correcto: comparamos
    print("La habitación está libre")  # Muestra en consola el resultado de esta operación

# ── = dentro de un objeto ────────────────────────────────────────────────
class Habitacion:  # Define la clase Habitacion
    def __init__(self, numero):  # Constructor: inicializa el objeto con los datos recibidos
        self.numero     = numero        # = GUARDA el número en el objeto
        self.disponible = True         # = GUARDA True en el atributo

    def reservar(self):  # Método reservar(): acción que puede realizar este objeto
        if self.disponible == True:   # == PREGUNTA si está disponible
            self.disponible = False   # = CAMBIA el estado a False
            return "✅ Habitación reservada"  # Retorna el valor calculado al lugar donde se llamó este método
        return "❌ Ya estaba ocupada"  # Retorna el valor calculado al lugar donde se llamó este método

h = Habitacion(101)  # Asigna un valor inicial o calculado a h
print(h.reservar())     # → ✅ Habitación reservada
print(h.reservar())     # → ❌ Ya estaba ocupada
🔬 Tabla de diferencias — = vs ==
Aspecto= Asignación== Comparación
¿Qué hace?Guarda / escribe un valorCompara dos valores
¿Devuelve algo?No — solo almacenaSí — True o False
¿Cambia datos?Sí, siempreNunca
¿Dónde se usa?x = 5, self.nombre = "Ana"if x == 5:, while estado == "activo":
Error típicoEscribir if x = 5: → SyntaxError
⚠️ Escribir if estado = "disponible": con un solo = dentro de un if provoca un SyntaxError. Python espera una comparación (==), no una asignación. Este error es tan común que Python lo detecta de inmediato.

J.2 — append() — qué significa y qué hace exactamente

append viene del inglés "to append" que significa adjuntar / agregar al final. Es el método más usado para construir listas dinámicas en Python.

Python — append() en detalle
# ── Una lista es como una fila de cajas numeradas ───────────────────────
reservaciones = []               # fila vacía

# append() AGREGA una nueva caja AL FINAL de la fila
reservaciones.append("RES-001")   # → ["RES-001"]
reservaciones.append("RES-002")   # → ["RES-001", "RES-002"]
reservaciones.append("RES-003")   # → ["RES-001", "RES-002", "RES-003"]

print(reservaciones)             # → ['RES-001', 'RES-002', 'RES-003']
print(len(reservaciones))        # → 3

# ── append de objetos enteros ────────────────────────────────────────────
class Reservacion:  # Define la clase Reservacion
    def __init__(self, id_res):  # Constructor: inicializa el objeto con los datos recibidos
        self.id_res = id_res  # Guarda id_res como atributo de esta instancia
    def __str__(self):  # Define cómo se muestra el objeto al usar print()
        return f"Reservación({self.id_res})"  # Retorna el valor calculado al lugar donde se llamó este método

historial = []  # Crea una lista vacía para almacenar datos en historial
historial.append(Reservacion("RES-001"))   # guarda el OBJETO completo
historial.append(Reservacion("RES-002"))  # Agrega un elemento al final de historial

for r in historial:  # Itera sobre cada r en historial
    print(r)   # → Reservación(RES-001)  /  Reservación(RES-002)

# ── Diferencia clave entre append() y = ──────────────────────────────────
lista_a = [1, 2, 3]  # Asigna un valor inicial o calculado a lista_a
lista_a.append(4)  # AGREGA 4 al final → [1, 2, 3, 4]

lista_b = [1, 2, 3]  # Asigna un valor inicial o calculado a lista_b
lista_b = [4]      # REEMPLAZA toda la lista → [4]  ⚠️ pierdes [1,2,3]

# ── Error clásico: guardar el resultado de append() ──────────────────────
lista_c = [1, 2]  # Asigna un valor inicial o calculado a lista_c
lista_c = lista_c.append(3)   # ⚠️ append devuelve None
print(lista_c)                 # → None  ❌ la lista original se perdió
🔬 Visualización mental de append()
MomentoEstado de la listaÍndice del nuevo elemento
Inicio[]— (vacía)
Primer append(r1)[r1]índice 0
Segundo append(r2)[r1, r2]índice 1
Tercer append(r3)[r1, r2, r3]índice 2
Acceso directohistorial[0] → r1Los índices empiezan en 0, no en 1
💡 append(x) modifica la lista en su lugar (in-place) — no devuelve nada útil (None). Por eso escribes lista.append(x) y nunca lista = lista.append(x).

J.3 — Disección completa del método confirmar()

Vamos a desmenuzar este método línea por línea y responder cada pregunta que surge al leerlo.

Python — el método que analizamos
def confirmar(self):  # Método confirmar(): acción que puede realizar este objeto
    resultado = self.habitacion.reservar()      # línea 1
    self.cliente.agregar_reservacion(self)    # línea 2
    return resultado                           # línea 3

❓ ¿Por qué resultado está a la IZQUIERDA del =?

Python — resultado = ...
# Python evalúa siempre de DERECHA a IZQUIERDA cuando hay =
#
#   resultado  =  self.habitacion.reservar()
#       ↑                   ↑
#   DESTINO           ORIGEN (se ejecuta primero)
#
# Paso 1: Python ejecuta self.habitacion.reservar()
# Paso 2: ese método DEVUELVE un valor (ej: "✅ Habitación reservada")
# Paso 3: ese valor se GUARDA en la variable 'resultado'
# Paso 4: 'return resultado' lo entrega al código que llamó confirmar()

# Mismo principio con una suma:
x = 5 + 3          # primero calcula 5+3=8, luego guarda 8 en x

# Sin variable intermedia — también válido:
return self.habitacion.reservar()  # ✅ directo, pero llama al método una sola vez

# Con variable — más legible y evita doble llamada:
resultado = self.habitacion.reservar()  # Asigna un valor inicial o calculado a resultado
return resultado                    # ✅ preferido en código profesional
💡 La variable resultado existe solo dentro de la función (variable local). Cuando return resultado termina de ejecutarse, esa variable desaparece. Su único propósito fue guardar temporalmente el valor para devolverlo.

self.habitacion.reservar() — ¿cómo llega al método de otra clase?

Python — acceso encadenado a otro objeto
# En __init__ de Reservacion guardamos el OBJETO Habitacion completo:
class Reservacion:  # Define la clase Reservacion
    def __init__(self, cliente, habitacion, fecha_entrada, fecha_salida):  # Constructor: inicializa el objeto con los datos recibidos
        self.cliente    = cliente      # ← OBJETO Cliente guardado aquí
        self.habitacion = habitacion   # ← OBJETO Habitacion guardado aquí

    def confirmar(self):  # Método confirmar(): acción que puede realizar este objeto
        # self            → este objeto Reservacion
        # self.habitacion → el objeto Habitacion guardado en el atributo
        # .reservar()     → llama al método reservar() de ESE objeto Habitacion
        resultado = self.habitacion.reservar()  # Asigna un valor inicial o calculado a resultado

# Es exactamente igual a:  hab101.reservar()  — donde hab101 === self.habitacion

# ¿Qué pasa si no pongo self en la definición del método? ────────────────
class ReservacionMal:  # Define la clase ReservacionMal
    def confirmar():              # ⚠️ sin self
        resultado = habitacion.reservar()  # ❌ NameError: 'habitacion' no existe

reserva = ReservacionMal()  # Asigna un valor inicial o calculado a reserva
reserva.confirmar()             # ❌ TypeError: confirmar() takes 0 arguments but 1 was given
# Python intenta pasar el objeto pero no hay parámetro para recibirlo
🔬 Lectura de self.habitacion.reservar() — de izquierda a derecha
FragmentoSignificaAnalogía
self Este objeto Reservacion actual Yo (la reservación)
self.habitacion El objeto Habitacion guardado en mi atributo Mi habitación asignada
self.habitacion.reservar El método reservar de ese objeto Habitacion La acción "reservar" de esa habitación
self.habitacion.reservar() Ejecutar ese método ahora. El () lo dispara. Pedirle a mi habitación que se reserve
⚠️ Regla de oro: Siempre que definas un método dentro de una clase, el primer parámetro debe ser self. Python lo rellena automáticamente con el objeto que llama al método. Sin self, el método no puede acceder a ningún atributo ni a ningún otro método del objeto.

self.cliente — ¿qué significa y qué más puedo acceder?

Python — self.cliente y sus posibilidades
# self.cliente ES el objeto Cliente completo — puedes usar TODOS sus atributos y métodos

class Cliente:  # Define la clase Cliente
    def __init__(self, nombre, email):  # Constructor: inicializa el objeto con los datos recibidos
        self.nombre        = nombre  # Guarda nombre como atributo de esta instancia
        self.email         = email  # Guarda email como atributo de esta instancia
        self.reservaciones = []  # Guarda reservaciones como atributo de esta instancia

    def agregar_reservacion(self, reservacion):  # Método agregar_reservacion(): acción que puede realizar este objeto
        self.reservaciones.append(reservacion)  # Agrega un elemento al final de self.reservaciones

    def get_nombre(self):        return self.nombre  # Getter: devuelve el valor de nombre
    def get_email(self):         return self.email  # Getter: devuelve el valor de email
    def total_reservaciones(self): return len(self.reservaciones)  # Método total_reservaciones(): acción que puede realizar este objeto

# Desde confirmar() puedes hacer CUALQUIERA de estas cosas:
def confirmar(self):  # Método confirmar(): acción que puede realizar este objeto
    # Acceder a atributos:
    print(self.cliente.nombre)              # → "Ana Torres"
    print(self.cliente.email)               # → "ana@email.com"
    # Llamar a métodos:
    print(self.cliente.get_nombre())        # → "Ana Torres" (getter)
    print(self.cliente.total_reservaciones()) # → número de reservaciones
    # Agregar esta reservación al historial del cliente:
    self.cliente.agregar_reservacion(self)  # ← lo que hace la línea 2

agregar_reservacion(self) — ¿por qué self está dentro del paréntesis? ¿Qué pasa si no lo pongo?

Python — self como argumento vs self como parámetro
# ── Hay DOS usos distintos de self — no confundirlos ────────────────────

# USO 1: self en la DEFINICIÓN (parámetro) — significa "yo, este objeto"
def agregar_reservacion(self, reservacion):   # self = el objeto Cliente
    self.reservaciones.append(reservacion)  # Agrega un elemento al final de self.reservaciones

# USO 2: self en la LLAMADA (argumento) — pasa la Reservacion como dato
self.cliente.agregar_reservacion(self)  # Completa la llamada o expresión iniciada anteriormente
#  ↑ objeto Reservacion        ↑ ¡este self también es la Reservacion!
#  llama al método del cliente    y le pasa ESTA reservación como argumento

# Traducido a nombres más claros:
#   self             = la Reservacion actual
#   self.cliente     = el objeto Cliente
#   agregar_reservacion(self) → "cliente, guarda ESTA reservación en tu historial"

# ── ¿QUÉ PASA SI NO PONGO self? ──────────────────────────────────────────
class Cliente:  # Define la clase Cliente
    def __init__(self, nombre):  # Constructor: inicializa el objeto con los datos recibidos
        self.nombre = nombre  # Guarda nombre como atributo de esta instancia
        self.reservaciones = []  # Guarda reservaciones como atributo de esta instancia
    def agregar_reservacion(self, reservacion):  # Método agregar_reservacion(): acción que puede realizar este objeto
        self.reservaciones.append(reservacion)  # Agrega un elemento al final de self.reservaciones

class Reservacion:  # Define la clase Reservacion
    def __init__(self, cliente, id_res):  # Constructor: inicializa el objeto con los datos recibidos
        self.cliente = cliente  # Guarda cliente como atributo de esta instancia
        self.id_res  = id_res  # Guarda id_res como atributo de esta instancia

    def confirmar_correcto(self):  # Método confirmar_correcto(): acción que puede realizar este objeto
        self.cliente.agregar_reservacion(self)   # ✅ pasa la reservación completa

    def confirmar_incorrecto(self):  # Método confirmar_incorrecto(): acción que puede realizar este objeto
        self.cliente.agregar_reservacion()        # ❌ falta el argumento reservacion

cli = Cliente("Ana Torres")  # Asigna un valor inicial o calculado a cli
res = Reservacion(cli, "RES-001")  # Asigna un valor inicial o calculado a res

res.confirmar_correcto()  # Completa la llamada o expresión iniciada anteriormente
print(cli.reservaciones[0].id_res)   # → RES-001  ✅

try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
    res.confirmar_incorrecto()  # Completa la llamada o expresión iniciada anteriormente
except TypeError as e:  # Captura el error TypeError as e si ocurre en el try
    print(f"Error: {e}")   # → TypeError: missing 1 required argument: 'reservacion'
🔬 Los dos self en una sola línea
PosiciónCódigo¿Qué objeto es?¿Quién lo proporciona?
Antes del punto self.cliente.agregar_reservacion(self) La Reservacion actual Python — automáticamente al ejecutar confirmar
Dentro del paréntesis self.cliente.agregar_reservacion(self) También la Reservacion — pasada como argumento para que el cliente la guarde El programador — explícitamente
🧩 En self.cliente.agregar_reservacion(self) hay tres objetos involucrados: la Reservacion (quien ejecuta confirmar), el Cliente (guardado en self.cliente), y de nuevo la Reservacion (pasada como argumento para que el cliente la guarde en su historial). El self que aparece dos veces siempre se refiere a la misma Reservacion.

J.4 — ¿Cómo ver todas las reservaciones de un cliente? — Código completo

Integramos todo lo anterior en un sistema funcional: clases, append, self, for y los métodos que ya conocemos.

Python — Sistema completo con historial por cliente
# ════════════════════════════════════════════════════════════════════
#  SISTEMA COMPLETO — Hotel con historial de reservaciones por cliente  # Comentario de apoyo que guía la lectura del ejemplo
# ════════════════════════════════════════════════════════════════════  # Comentario de apoyo que guía la lectura del ejemplo

class Habitacion:  # Define la clase Habitacion
    def __init__(self, numero: int, tipo: str, tarifa: float):  # Constructor: inicializa el objeto con los datos recibidos
        self.numero     = numero  # Guarda numero como atributo de esta instancia
        self.tipo       = tipo  # Guarda tipo como atributo de esta instancia
        self.tarifa     = tarifa  # Guarda tarifa como atributo de esta instancia
        self.disponible = True  # Guarda disponible como atributo de esta instancia

    def reservar(self) -> str:  # Método reservar(): acción que puede realizar este objeto
        if self.disponible:  # Verifica self.disponible antes de continuar
            self.disponible = False  # Guarda disponible como atributo de esta instancia
            return f"✅ Hab {self.numero} ({self.tipo}) reservada"  # Retorna el valor calculado al lugar donde se llamó este método
        return f"❌ Hab {self.numero} ya está ocupada"  # Retorna el valor calculado al lugar donde se llamó este método

    def __str__(self) -> str:  # Define cómo se muestra el objeto al usar print()
        est = "disponible" if self.disponible else "ocupada"  # Asigna un valor inicial o calculado a est
        return f"Hab {self.numero} [{self.tipo}] ${self.tarifa:.2f}/noche — {est}"  # Retorna el valor calculado al lugar donde se llamó este método


class Cliente:  # Define la clase Cliente
    def __init__(self, nombre: str, email: str):  # Constructor: inicializa el objeto con los datos recibidos
        self.nombre        = nombre  # Guarda nombre como atributo de esta instancia
        self.email         = email  # Guarda email como atributo de esta instancia
        self.reservaciones = []       # lista vacía — se llena con append()

    def agregar_reservacion(self, reservacion) -> None:  # Método agregar_reservacion(): acción que puede realizar este objeto
        self.reservaciones.append(reservacion)  # Agrega un elemento al final de self.reservaciones

    def ver_reservaciones(self) -> None:  # Método ver_reservaciones(): acción que puede realizar este objeto
        """Imprime todo el historial del cliente con totales."""  # Continúa la lógica del ejemplo paso a paso
        print(f"\n  📋 Historial de {self.nombre} ({self.email})")  # Muestra en consola el resultado de esta operación
        print(f"  {'─' * 50}")  # Muestra en consola el resultado de esta operación
        if not self.reservaciones:  # Verifica not self.reservaciones antes de continuar
            print("  Sin reservaciones registradas.")  # Muestra en consola el resultado de esta operación
            return  # Termina el método y devuelve el control al llamador
        total = 0.0  # Asigna un valor inicial o calculado a total
        for i, res in enumerate(self.reservaciones, start=1):  # Itera sobre cada i, res en enumerate(self.reservaciones, start=1)
            costo = res.noches * res.habitacion.tarifa  # Asigna un valor inicial o calculado a costo
            total += costo  # Continúa la lógica del ejemplo paso a paso
            print(  # Muestra en consola el resultado de esta operación
                f"  {i}. Hab {res.habitacion.numero} | "  # Continúa la lógica del ejemplo paso a paso
                f"{res.fecha_entrada} → {res.fecha_salida} | "  # Continúa la lógica del ejemplo paso a paso
                f"{res.noches} noche(s) | ${costo:,.2f}"  # Continúa la lógica del ejemplo paso a paso
            )  # Cierra la estructura abierta en las líneas anteriores
        print(f"  {'─' * 50}")  # Muestra en consola el resultado de esta operación
        print(f"  Total gastado: ${total:,.2f}")  # Muestra en consola el resultado de esta operación

    def __str__(self) -> str:  # Define cómo se muestra el objeto al usar print()
        return f"Cliente: {self.nombre} | {len(self.reservaciones)} reservación(es)"  # Retorna el valor calculado al lugar donde se llamó este método


class Reservacion:  # Define la clase Reservacion
    def __init__(self, cliente: Cliente, habitacion: Habitacion,  # Constructor: inicializa el objeto con los datos recibidos
                 fecha_entrada: str, fecha_salida: str, noches: int):  # Completa la firma o la condición iniciada en la línea anterior
        self.cliente       = cliente  # Guarda cliente como atributo de esta instancia
        self.habitacion    = habitacion  # Guarda habitacion como atributo de esta instancia
        self.fecha_entrada  = fecha_entrada  # Guarda fecha_entrada como atributo de esta instancia
        self.fecha_salida   = fecha_salida  # Guarda fecha_salida como atributo de esta instancia
        self.noches         = noches  # Guarda noches como atributo de esta instancia

    def confirmar(self) -> str:  # Método confirmar(): acción que puede realizar este objeto
        resultado = self.habitacion.reservar()   # 1. cierra la habitación
        self.cliente.agregar_reservacion(self) # 2. agrega al historial del cliente
        return resultado                        # 3. devuelve el mensaje

# ════════════════════════════════════════════════════════════════════
# Crear habitaciones
hab101 = Habitacion(101, "Estándar",    1200.0)  # Asigna un valor inicial o calculado a hab101
hab202 = Habitacion(202, "Junior Suite", 2500.0)  # Asigna un valor inicial o calculado a hab202
hab305 = Habitacion(305, "Suite Master", 4500.0)  # Asigna un valor inicial o calculado a hab305

# Crear clientes
ana  = Cliente("Ana Torres", "ana@email.com")  # Asigna un valor inicial o calculado a ana
luis = Cliente("Luis Ramos", "luis@email.com")  # Asigna un valor inicial o calculado a luis

# Crear y confirmar reservaciones
r1 = Reservacion(ana,  hab101, "2026-06-01", "2026-06-04", 3)  # Asigna un valor inicial o calculado a r1
r2 = Reservacion(ana,  hab305, "2026-07-15", "2026-07-20", 5)  # Asigna un valor inicial o calculado a r2
r3 = Reservacion(luis, hab202, "2026-06-10", "2026-06-12", 2)  # Asigna un valor inicial o calculado a r3

print(r1.confirmar())   # ✅ Hab 101 reservada
print(r2.confirmar())   # ✅ Hab 305 reservada
print(r3.confirmar())   # ✅ Hab 202 reservada

# ── VER TODAS LAS RESERVACIONES DE UN CLIENTE ────────────────────────────
ana.ver_reservaciones()  # Completa la llamada o expresión iniciada anteriormente
luis.ver_reservaciones()  # Completa la llamada o expresión iniciada anteriormente

# ── Acceso directo a la lista con for ────────────────────────────────────
print(f"\nAna tiene {len(ana.reservaciones)} reservación(es):")  # Muestra en consola el resultado de esta operación
for r in ana.reservaciones:  # Itera sobre cada r en ana.reservaciones
    print(f"  → Hab {r.habitacion.numero} — {r.fecha_entrada}")  # Muestra en consola el resultado de esta operación

# ── Intento de reservar habitación ya ocupada ─────────────────────────────
r4 = Reservacion(luis, hab101, "2026-06-05", "2026-06-07", 2)  # Asigna un valor inicial o calculado a r4
print("\n" + r4.confirmar())   # ❌ ya estaba ocupada
📋 Salida esperada — Sistema completo
✅ Hab 101 (Estándar) reservada
✅ Hab 305 (Suite Master) reservada
✅ Hab 202 (Junior Suite) reservada

  📋 Historial de Ana Torres (ana@email.com)
  ──────────────────────────────────────────────────
  1. Hab 101 | 2026-06-01 → 2026-06-04 | 3 noche(s) | $3,600.00
  2. Hab 305 | 2026-07-15 → 2026-07-20 | 5 noche(s) | $22,500.00
  ──────────────────────────────────────────────────
  Total gastado: $26,100.00

  📋 Historial de Luis Ramos (luis@email.com)
  ──────────────────────────────────────────────────
  1. Hab 202 | 2026-06-10 → 2026-06-12 | 2 noche(s) | $5,000.00
  ──────────────────────────────────────────────────
  Total gastado: $5,000.00

Ana tiene 2 reservación(es):
  → Hab 101 — 2026-06-01
  → Hab 305 — 2026-07-15

❌ Hab 101 ya está ocupada
🔬 Flujo completo de r1.confirmar() — paso a paso
PasoCódigo que se ejecutaQué ocurre
1r1.confirmar()Python entra al método. self = r1.
2self.habitacion.reservar()Accede a hab101. Llama a su reservar().
3Dentro de hab101.reservar()disponible cambia de True a False. Retorna "✅ Hab 101...".
4resultado = "✅ Hab 101..."El texto devuelto se guarda en la variable local resultado.
5self.cliente.agregar_reservacion(self)Accede a ana. Pasa r1 (self) como argumento.
6ana.agregar_reservacion(r1)Ejecuta self.reservaciones.append(r1). r1 queda en el historial de Ana.
7return resultadoEl texto "✅ Hab 101 (Estándar) reservada" viaja al código principal.
🏆 Para el alumno: Reservacion no hace todo el trabajo — delega: le pide a Habitacion que se reserve, y le pide a Cliente que guarde el registro. Cada clase hace solo lo que le corresponde. Eso es el principio de responsabilidad única y el corazón de la Programación Orientada a Objetos.