Programación Orientada a Objetos — Python

Ing. Bernardo Prado · DSM3 · UTH 2026-2

🎓 2026-2 🐍 Python 3 📊 85 pts

📝 Examen U2 OP2 — Los 4 Pilares de la POO

Evaluación alternativa de la Unidad 2. Cada ejercicio está resuelto y explicado línea por línea, cubriendo los 4 pilares de la POO: Abstracción, Encapsulamiento, Herencia y Polimorfismo. Los diagramas UML y de casos de uso muestran el diseño completo; el bloque de código incluye la solución comentada y la salida esperada para que verifiques tu comprensión de cada concepto y repases antes del examen.

6Ejercicios
85Puntos
4Pilares
90Min aprox.

📌 Instrucciones Generales

#TemaEscenarioMóduloPuntos
1Encapsulamiento + @propertyClínica de SaludU215 pts
2Herencia 3 niveles + super()Flota de VehículosU215 pts
3Clase Abstracta + PolimorfismoOrquesta de InstrumentosU215 pts
4ComposiciónSistema de CafeteríaU215 pts
5@classmethod + @staticmethodConversor de TemperaturasU210 pts
6Herencia múltiple / MixinAnimales con MixinU215 pts
01

Clínica de Salud — Encapsulamiento

🎯 15 pts📦 @property + @setter validado🔗 Práctica U2

📋 Escenario

Una clínica registra pacientes aplicando encapsulamiento completo: __nombre privado (solo lectura), _edad protegida (setter: 0 < edad < 120), __temperatura privada (setter: 30.0 ≤ temp ≤ 45.0). El método tiene_fiebre() devuelve True si temperatura ≥ 38.0 y presentarse() muestra el estado con operador ternario.

🧠 Lógica de cada método — cómo construirlo

__init__()
Asigna self.__nombre = nombre (privado), self._edad = edad (protegido), self.__id_paciente = id_paciente (privado), self.__temperatura = temperatura (privado). Los dobles guiones bajos impiden acceso directo desde fuera.
@property nombre
Solo getter — sin setter. Decorar con @property y retornar self.__nombre. Al no haber setter, intentar p.nombre = "X" lanza AttributeError automáticamente.
@property + @edad.setter
Getter: return self._edad. Setter: if 0 < nueva_edad < 120: self._edad = nueva_edad — si no cumple, imprimir mensaje de error (sin lanzar excepción).
@property + @temperatura.setter
Getter: return self.__temperatura. Setter: if 30.0 <= nueva_temp <= 45.0: self.__temperatura = nueva_temp — fuera del rango, imprimir mensaje de error sin modificar el valor.
tiene_fiebre()
return self.__temperatura >= 38.0 — una sola línea. Accede al privado directamente desde dentro de la clase.
presentarse()
Ternario: estado = "⚠ Fiebre" if self.tiene_fiebre() else "✓ Normal". Luego print() con f-string usando las properties self.nombre, self.edad, self.temperatura y estado.

📐 Diagrama de Clases

Paciente
Atributos (encapsulados)
__nombre : str
~ _edad : int
__temperatura : float
__id_paciente : str
@property
+ nombre → str (lectura)
+ edad ↔ int (setter: 0<e<120)
+ temperatura ↔ float (30–45)
Métodos
+ tiene_fiebre() : bool
+ presentarse() : None

👤 Diagrama de Casos de Uso

«Sistema Clínica» Médico Registrar paciente Actualizar temperatura Verificar fiebre Consultar datos
📝 paciente.pyPython 3
# ── Ejercicio 1: Clínica de Salud — Encapsulamiento ───────────────
class Paciente:  # Define la clase Paciente con encapsulamiento completo de atributos
    """Paciente con encapsulamiento completo y propiedades validadas."""  # Docstring de la clase

    def __init__(self, nombre, edad, id_paciente, temperatura=36.5):  # Constructor: recibe datos del paciente; temperatura tiene valor por defecto
        self.__nombre      = nombre  # Atributo PRIVADO: nombre no puede modificarse desde fuera de la clase
        self._edad         = edad  # Atributo PROTEGIDO: edad accesible por subclases con validación
        self.__id_paciente = id_paciente  # Atributo PRIVADO: ID del paciente, uso interno exclusivo
        self.__temperatura  = temperatura  # Atributo PRIVADO: temperatura corporal, solo via property

    # ── nombre: solo lectura ─────────────────────────────
    @property  # Decorador que convierte el método en propiedad de solo lectura
    def nombre(self):  # Getter del nombre; sin setter porque el nombre no debe cambiar
        return self.__nombre  # Devuelve el nombre privado de forma controlada

    # ── edad: con setter validado ─────────────────────────
    @property  # Decorador para acceder a _edad como si fuera atributo público
    def edad(self):  # Getter de edad
        return self._edad  # Devuelve la edad protegida

    @edad.setter  # Decorador que define el setter de la propiedad edad
    def edad(self, nueva_edad):  # Setter de edad: valida antes de asignar
        if 0 < nueva_edad < 120:  # Valida que la edad esté en rango biológicamente válido
            self._edad = nueva_edad  # Asigna la nueva edad si pasa la validación
        else:  # Si la edad está fuera del rango permitido...
            print(f"Edad inválida: {nueva_edad}. Debe estar entre 1 y 119.")  # ...muestra mensaje de error sin lanzar excepción

    # ── temperatura: con setter validado ──────────────────
    @property  # Decorador para acceder a __temperatura como propiedad pública
    def temperatura(self):  # Getter de temperatura corporal
        return self.__temperatura  # Devuelve la temperatura privada de forma controlada

    @temperatura.setter  # Decorador que define el setter con validación de rango
    def temperatura(self, nueva_temp):  # Setter de temperatura: acepta solo valores entre 30 y 45 °C
        if 30.0 <= nueva_temp <= 45.0:  # Valida que la temperatura esté en rango fisiológico
            self.__temperatura = nueva_temp  # Asigna la nueva temperatura si es válida
        else:  # Si la temperatura está fuera del rango permitido...
            print(f"Temperatura inválida: {nueva_temp}°C. Rango: 30–45")  # ...muestra mensaje de error descriptivo

    def tiene_fiebre(self) -> bool:  # Método que devuelve True si la temperatura indica fiebre (≥ 38.0 °C)
        return self.__temperatura >= 38.0  # Compara temperatura con el umbral clínico de fiebre

    def presentarse(self):  # Método que imprime el estado completo del paciente
        estado = "⚠ Fiebre" if self.tiene_fiebre() else "✓ Normal"  # Operador ternario según si hay fiebre
        print(f"Paciente: {self.nombre} | Edad: {self.edad} | "  # Primera parte: datos de identidad del paciente
              f"Temp: {self.temperatura}°C | Estado: {estado}")  # Segunda parte: temperatura y estado de salud

if __name__ == "__main__":  # Bloque de prueba principal
    p1 = Paciente("María García", 34, "PAC-001")  # Crea un paciente con temperatura por defecto (36.5 °C)
    p1.presentarse()  # Muestra el estado inicial del paciente (temperatura normal)
    p1.temperatura = 39.2           # setter válido
    p1.presentarse()  # Muestra el estado tras subir la temperatura (fiebre detectada)
    p1.temperatura = 50.0           # setter inválido
    p1.edad = -5                    # setter inválido
    p1.edad = 35                    # setter válido
    print(f"Nueva edad: {p1.edad}")  # Verifica que la edad se actualizó correctamente a 35
▶ Comprobación — Salida esperada
Paciente: María García | Edad: 34 | Temp: 36.5°C | Estado: ✓ Normal
Paciente: María García | Edad: 34 | Temp: 39.2°C | Estado: ⚠ Fiebre
Temperatura inválida: 50.0°C. Rango: 30–45
Edad inválida: -5. Debe estar entre 1 y 119.
Nueva edad: 35

📋 Rúbrica — 15 pts

Atributos privados y protegidos correctos (3 pts)
@property para nombre (solo lectura) (2 pts)
@property + @setter para edad con validación (4 pts)
@property + @setter para temperatura con rango (4 pts)
tiene_fiebre() + presentarse() con ternario (2 pts)
02

Flota de Vehículos — Herencia 3 Niveles

🎯 15 pts📦 super().__init__() multinivel🔗 Práctica U2

📋 Escenario

Una empresa de logística gestiona su flota con jerarquía de 3 niveles: VehiculoCamionetaCamionetaRefrigerada. Cada nivel añade atributos propios y hace override de descripcion() llamando a super().descripcion(). Usa super().__init__() en cada constructor para encadenar la inicialización.

🧠 Lógica de cada método — cómo construirlo

Vehiculo.__init__
Guarda los atributos base: self.placa, self.marca, self.año. Es la raíz de la cadena; no llama super() (hereda directamente de object).
Camioneta.__init__
Primero super().__init__(placa, marca, año) para inicializar los atributos de Vehiculo. Luego agrega sus propios: self.capacidad_carga y self.num_ejes.
CamionetaRefrigerada.__init__
Primero super().__init__(placa, marca, año, capacidad_carga, num_ejes). Luego agrega self.temperatura_min y self.temperatura_max. La cadena completa corre: CamRef.__init__Camioneta.__init__Vehiculo.__init__.
Vehiculo.descripcion()
Retorna (o imprime) un f-string base: placa, marca y año. Es el punto de partida que cada subclase extiende.
Camioneta.descripcion()
base = super().descripcion() captura el texto del nivel superior. Luego retorna f"{base} | Carga: {self.capacidad_carga} | Ejes: {self.num_ejes}".
CamionetaRefrigerada.descripcion()
base = super().descripcion() ya trae placa + carga + ejes. Agrega el rango de temperatura: f"{base} | Temp: {self.temperatura_min}°C a {self.temperatura_max}°C".
__str__()
En cada clase simplemente return self.descripcion(). Así print(obj) muestra la descripción completa automáticamente.

📐 Diagrama de Clases

Vehiculo
+ marca : str
+ modelo : str
+ anio : int
+ descripcion() : str
+ __str__() : str
Camioneta
+ capacidad_kg : float
+ num_ejes : int
+ descripcion() : str (override)
CamionetaRefrigerada
+ temp_min : float
+ temp_max : float
+ descripcion() : str (override)
+ __str__() : str (override)

🔄 Diagrama de Secuencia — cadena super()

main CamRefrig Camioneta Vehiculo CamionetaRefrigerada(...) super().__init__(...) super().__init__(...) + marca, modelo, anio + capacidad, ejes + temp_min, temp_max → objeto completo
📝 vehiculos.pyPython 3
# ── Ejercicio 2: Flota de Vehículos — Herencia 3 niveles ──────────
class Vehiculo:  # Clase base (nivel 1): define atributos y comportamiento general de cualquier vehículo
    def __init__(self, marca, modelo, anio):  # Constructor base: recibe los datos esenciales del vehículo
        self.marca  = marca  # Almacena la marca del vehículo (ej: Toyota)
        self.modelo = modelo  # Almacena el modelo del vehículo (ej: Hilux)
        self.anio   = anio  # Almacena el año de fabricación del vehículo

    def descripcion(self):  # Método que retorna la descripción básica del vehículo
        return f"{self.anio} {self.marca} {self.modelo}"  # Formatea: año + marca + modelo (ej: "2022 Toyota Hilux")

    def __str__(self):  # Representación legible del vehículo base para print()
        return f"Vehículo: {self.descripcion()}"  # Prefija "Vehículo:" a la descripción generada


class Camioneta(Vehiculo):  # Clase nivel 2: hereda de Vehiculo y añade datos de carga
    def __init__(self, marca, modelo, anio, capacidad_kg, num_ejes):  # Constructor: recibe datos de Vehiculo + capacidad y ejes
        super().__init__(marca, modelo, anio)  # Llama al constructor padre para inicializar marca, modelo y año
        self.capacidad_kg = capacidad_kg  # Almacena la capacidad de carga en kilogramos
        self.num_ejes     = num_ejes  # Almacena el número de ejes del vehículo

    def descripcion(self):  # Override de descripcion(): extiende la descripción base con datos de carga
        base = super().descripcion()  # Obtiene la descripción del nivel superior (Vehiculo)
        return f"{base} | {self.capacidad_kg} kg | {self.num_ejes} ejes"  # Agrega capacidad y número de ejes


class CamionetaRefrigerada(Camioneta):  # Clase nivel 3: hereda de Camioneta y añade sistema de frío
    def __init__(self, marca, modelo, anio,  # Constructor: todos los parámetros de Camioneta más el rango de temperatura
                 capacidad_kg, num_ejes, temp_min, temp_max):  # Completa la firma del método con los parámetros restantes, iniciada en la línea anterior
        super().__init__(marca, modelo, anio, capacidad_kg, num_ejes)  # Encadena hasta Camioneta → Vehiculo via super()
        self.temp_min = temp_min  # Temperatura mínima de la cámara de refrigeración (ej: -18.0 °C)
        self.temp_max = temp_max  # Temperatura máxima de la cámara de refrigeración (ej: 4.0 °C)

    def descripcion(self):  # Override de nivel 3: agrega rango de temperatura a la descripción de Camioneta
        base = super().descripcion()  # Obtiene descripción completa de Camioneta (ya incluye capacidad y ejes)
        return f"{base} | Frío: {self.temp_min}°C a {self.temp_max}°C"  # Añade el rango de frío de la cámara

    def __str__(self):  # Override de __str__: etiqueta la camioneta como refrigerada
        return f"[Refrig] {self.descripcion()}"  # Añade prefijo "[Refrig]" para identificar el tipo especial


if __name__ == "__main__":  # Bloque de prueba con los 3 niveles de la jerarquía
    flota = [  # Lista que representa la flota con uno de cada tipo de vehículo
        Vehiculo("Toyota",   "Hilux",    2022),  # Vehículo base: sin capacidad ni refrigeración
        Camioneta("Ford",     "F-350",    2021, 2500, 2),  # Camioneta nivel 2: capacidad 2500 kg, 2 ejes
        CamionetaRefrigerada("Mercedes", "Sprinter", 2023,  # Camioneta refrigerada nivel 3...
                              1800, 2, -18.0, 4.0),  # ...capacidad 1800 kg, 2 ejes, rango -18°C a 4°C
    ]  # Fin de la lista flota
    for v in flota:  # Itera sobre cada vehículo de la flota
        print(v)  # Llama a __str__ de cada vehículo (polimorfismo: cada clase imprime diferente)
▶ Comprobación — Salida esperada
Vehículo: 2022 Toyota Hilux
Vehículo: 2021 Ford F-350 | 2500 kg | 2 ejes
[Refrig] 2023 Mercedes Sprinter | 1800 kg | 2 ejes | Frío: -18.0°C a 4.0°C

📋 Rúbrica — 15 pts

Clase base Vehiculo con descripcion() y __str__() (3 pts)
Camioneta con super().__init__() + override descripcion() (4 pts)
CamionetaRefrigerada con super() encadenado (5 pts)
Bucle for que muestra los 3 tipos correctamente (3 pts)
03

Orquesta de Instrumentos — Abstracción y Polimorfismo

🎯 15 pts📦 ABC + @abstractmethod🔗 Práctica U2

📋 Escenario

Una orquesta registra instrumentos. Instrumento es clase abstracta (hereda de ABC) con dos métodos abstractos: tocar() y afinar(). El método concreto describir() se implementa en la base. Crea 3 subclases: Guitarra, Piano, Violin. El polimorfismo se demuestra iterando la lista y llamando tocar().

🧠 Lógica de cada método — cómo construirlo

Instrumento(ABC)
Clase base abstracta: __init__ guarda self.nombre y self.familia. Decorar tocar() y afinar() con @abstractmethod + pass — Python prohíbe instanciar esta clase directamente y exige que cada subclase los implemente.
describir() — concreto en base
Se implementa una sola vez en Instrumento: retorna f-string con nombre y familia. Todas las subclases lo heredan sin reescribirlo — esto es reutilización de código.
Guitarra / Piano / Violin — __init__
super().__init__(nombre, familia) para inicializar la base. Luego cada subclase agrega su atributo propio: Guitarraself.num_cuerdas, Pianoself.num_teclas, Violinself.tipo_arco.
tocar() — en cada subclase
Cada subclase da su propia implementación: Guitarra → "rasgar cuerdas", Piano → "presionar teclas", Violin → "deslizar arco". Este es el polimorfismo: mismo método, comportamiento distinto.
afinar() — en cada subclase
Igual que tocar(), cada clase define su forma de afinarse. Sin esta implementación Python lanza TypeError al instanciar.
for en main (polimorfismo)
for instr in orquesta: instr.tocar() — sin importar el tipo concreto, Python llama el método correcto de cada objeto. Todos comparten la interfaz de Instrumento.

📐 Diagrama de Clases

«abstract» Instrumento
+ nombre : str
+ familia : str
+ anio_fabricacion : int
+ tocar() : str {abstract}
+ afinar() : str {abstract}
+ describir() : None
▲ (hereda)
Guitarra
cuerdas
tocar()
afinar()
Piano
teclas
tocar()
afinar()
Violin
tamanio
tocar()
afinar()

👤 Diagrama de Casos de Uso

«Sistema Orquesta» Director Registrar instrumento Afinar instrumento Tocar instrumento Describir instrumento
📝 orquesta.pyPython 3
# ── Ejercicio 3: Orquesta — Abstracción y Polimorfismo ────────────
from abc import ABC, abstractmethod  # Importa ABC y abstractmethod del módulo abc para crear clases abstractas

class Instrumento(ABC):  # Clase abstracta base: no puede instanciarse directamente, es un contrato para subclases
    """Clase abstracta base para instrumentos musicales."""  # Docstring de la clase abstracta
    def __init__(self, nombre, familia, anio_fabricacion):  # Constructor base: datos comunes a todos los instrumentos
        self.nombre           = nombre  # Nombre del instrumento (ej: "Gibson Les Paul")
        self.familia          = familia  # Familia musical a la que pertenece (Cuerda, Teclado, etc.)
        self.anio_fabricacion = anio_fabricacion  # Año en que fue fabricado el instrumento

    @abstractmethod  # Decorador: obliga a todas las subclases a implementar este método
    def tocar(self) -> str:  # Método abstracto: cada instrumento toca de forma diferente
        pass  # Sin implementación en la base; las subclases definen el comportamiento concreto

    @abstractmethod  # Decorador: obliga a todas las subclases a implementar este método
    def afinar(self) -> str:  # Método abstracto: cada instrumento se afina de forma diferente
        pass  # Sin implementación en la base; cada subclase define cómo se afina

    def describir(self):                    # método concreto en la base
        print(f"{self.nombre} | {self.familia} | Año: {self.anio_fabricacion}")  # Imprime nombre, familia y año del instrumento


class Guitarra(Instrumento):  # Subclase concreta: implementa los métodos abstractos para una guitarra
    def __init__(self, nombre, anio_fabricacion, cuerdas=6):  # Constructor: nombre y año heredados + número de cuerdas
        super().__init__(nombre, "Cuerda", anio_fabricacion)  # Llama al constructor de Instrumento con familia "Cuerda"
        self.cuerdas = cuerdas  # Almacena el número de cuerdas (por defecto 6)

    def tocar(self) -> str:  # Implementación concreta de tocar() para la guitarra
        return f"🎸 {self.nombre}: rasgueo de {self.cuerdas} cuerdas"  # Describe la acción específica de tocar guitarra

    def afinar(self) -> str:  # Implementación concreta de afinar() para la guitarra
        return "Guitarra: afinada E estándar (EADGBE)"  # Retorna la afinación estándar de la guitarra


class Piano(Instrumento):  # Subclase concreta: implementa los métodos abstractos para un piano
    def __init__(self, nombre, anio_fabricacion, teclas=88):  # Constructor: nombre y año + número de teclas (88 por defecto)
        super().__init__(nombre, "Teclado", anio_fabricacion)  # Llama al constructor de Instrumento con familia "Teclado"
        self.teclas = teclas  # Almacena el número de teclas del piano (por defecto 88)

    def tocar(self) -> str:  # Implementación concreta de tocar() para el piano
        return f"🎹 {self.nombre}: melodía en {self.teclas} teclas"  # Describe la acción de tocar el piano

    def afinar(self) -> str:  # Implementación concreta de afinar() para el piano
        return "Piano: afinado por técnico certificado"  # El piano requiere afinador profesional


class Violin(Instrumento):  # Subclase concreta: implementa los métodos abstractos para un violín
    def __init__(self, nombre, anio_fabricacion, tamanio="4/4"):  # Constructor: nombre y año + tamaño (4/4 por defecto)
        super().__init__(nombre, "Cuerda-Arco", anio_fabricacion)  # Llama al constructor de Instrumento con familia "Cuerda-Arco"
        self.tamanio = tamanio  # Almacena el tamaño del violín (4/4 es el estándar adulto)

    def tocar(self) -> str:  # Implementación concreta de tocar() para el violín
        return f"🎻 {self.nombre}: arco sobre cuerdas, tamaño {self.tamanio}"  # Describe la técnica de arco del violín

    def afinar(self) -> str:  # Implementación concreta de afinar() para el violín
        return "Violín: afinado con diapasón"  # El violín se afina usando un diapasón como referencia


if __name__ == "__main__":  # Bloque de prueba que demuestra el polimorfismo de la orquesta
    orquesta = [  # Lista de instrumentos de distintos tipos (polimorfismo en acción)
        Guitarra("Gibson Les Paul", 2019),  # Crea una guitarra con 6 cuerdas por defecto
        Piano("Steinway D-274",    2015),   # Crea un piano con 88 teclas por defecto
        Violin("Stradivarius",       1710),  # Crea un violín con tamaño 4/4 por defecto
    ]  # Fin de la lista orquesta
    for inst in orquesta:  # Itera sobre cada instrumento; el tipo real determina qué método se llama
        inst.describir()  # Llama al método concreto de la clase base (igual para todos)
        print("  ▶", inst.tocar())  # Polimorfismo: cada instrumento ejecuta su propia implementación de tocar()
        print("    ↳", inst.afinar())  # Polimorfismo: cada instrumento ejecuta su propia implementación de afinar()
        print()  # Línea en blanco para separar visualmente cada instrumento en la salida
▶ Comprobación — Salida esperada
Gibson Les Paul | Cuerda | Año: 2019
  ▶ 🎸 Gibson Les Paul: rasgueo de 6 cuerdas
    ↳ Guitarra: afinada E estándar (EADGBE)

Steinway D-274 | Teclado | Año: 2015
  ▶ 🎹 Steinway D-274: melodía en 88 teclas
    ↳ Piano: afinado por técnico certificado

Stradivarius | Cuerda-Arco | Año: 1710
  ▶ 🎻 Stradivarius: arco sobre cuerdas, tamaño 4/4
    ↳ Violín: afinado con diapasón

📋 Rúbrica — 15 pts

Instrumento(ABC) con 2 @abstractmethod correctos (4 pts)
Método concreto describir() en la base (2 pts)
3 subclases con super().__init__() e impl. de abstractos (6 pts)
Polimorfismo: bucle for llamando tocar() + afinar() (3 pts)
04

Sistema de Cafetería — Composición

🎯 15 pts📦 Composición🔗 Práctica U2

📋 Escenario

Una cafetería necesita registrar pedidos de clientes. Cada Pedido contiene varios objetos Producto, por lo que se debe aplicar composición: el pedido administra su propia lista de productos, calcula el total, aplica descuentos y limita la cantidad máxima de artículos permitidos.

🧠 Lógica de cada método — cómo construirlo

Producto.__init__()
Guarda nombre, precio y categoria como atributos de instancia. Cada producto existe como objeto independiente que luego será agregado a un pedido.
Pedido.__init__()
Inicializa numero_pedido, cliente, una lista vacía de productos y un acumulador de descuento. La lista vive dentro del pedido, mostrando la relación de composición.
agregar_producto()
Verifica si la cantidad actual es menor que MAXIMO_PRODUCTOS. Si hay espacio, agrega el objeto recibido a la lista y devuelve True; si no, devuelve False.
calcular_total()
Usa sum() con una comprensión para recorrer todos los productos del pedido y sumar sus precios. Este método centraliza el cálculo del subtotal.
aplicar_descuento()
Calcula el monto descontado según el porcentaje recibido, lo guarda en el pedido y retorna el total final. Así el descuento queda disponible para mostrarlo luego en __str__.
__str__()
Construye una cadena legible con número, cliente, cantidad de productos, total final y detalle de nombres. Sirve para imprimir el pedido completo en una sola línea.

📐 Diagrama de Clases

Producto
Atributos
+ nombre : str
+ precio : float
+ categoria : str
Métodos
+ __init__() : None
+ __str__() : str
◆ composición — un Pedido contiene 0..10 Producto
Pedido
Atributos
+ MAXIMO_PRODUCTOS : int = 10
+ numero_pedido : str
+ cliente : str
+ productos : list[Producto]
+ descuento_aplicado : float
Métodos
+ agregar_producto(producto) : bool
+ calcular_total() : float
+ aplicar_descuento(porcentaje) : float
+ __str__() : str

👤 Diagrama de Casos de Uso

«Sistema Cafetería» Cajero Crear pedido Agregar productos Calcular total Aplicar descuento
📝 cafeteria.pyPython 3
# ── Ejercicio 4: Sistema de Cafetería — Composición ───────────────
class Producto:  # Define la entidad básica que representa cada artículo vendible de la cafetería
    def __init__(self, nombre, precio, categoria):  # Construye un producto con su nombre, precio y categoría para reutilizarlo dentro de pedidos
        self.nombre = nombre  # Guarda el nombre para identificar el producto en reportes y tickets
        self.precio = precio  # Guarda el precio unitario porque será usado al calcular el total del pedido
        self.categoria = categoria  # Guarda la categoría para clasificar el producto dentro del sistema

    def __str__(self):  # Devuelve una representación legible del producto para mostrarlo en pantalla
        return f"{self.nombre} ({self.categoria}) - L {self.precio:.2f}"  # Construye un texto claro con nombre, categoría y precio formateado

class Pedido:  # Define la clase contenedora que compone y administra múltiples objetos Producto
    MAXIMO_PRODUCTOS = 10  # Atributo de clase que fija el límite máximo de productos permitidos por pedido

    def __init__(self, numero_pedido, cliente):  # Inicializa un pedido con sus datos principales y su composición interna
        self.numero_pedido = numero_pedido  # Guarda el identificador del pedido para rastrearlo de forma única
        self.cliente = cliente  # Guarda el nombre del cliente asociado al pedido actual
        self.productos = []  # Crea la lista interna que contendrá los objetos Producto del pedido
        self.descuento_aplicado = 0.0  # Inicia el descuento en cero para poder calcular un total final consistente

    def agregar_producto(self, producto):  # Intenta agregar un objeto Producto al pedido respetando el límite permitido
        if len(self.productos) < Pedido.MAXIMO_PRODUCTOS:  # Verifica que todavía exista espacio antes de modificar la composición
            self.productos.append(producto)  # Inserta el producto en la lista interna para que pase a formar parte del pedido
            return True  # Informa que la operación fue exitosa y que el producto quedó agregado
        return False  # Informa que no se agregó porque el pedido ya alcanzó su capacidad máxima

    def calcular_total(self):  # Suma los precios de todos los productos actualmente contenidos en el pedido
        return sum(producto.precio for producto in self.productos)  # Recorre la composición y devuelve el subtotal acumulado

    def aplicar_descuento(self, porcentaje):  # Calcula y registra el descuento a partir del porcentaje recibido
        total = self.calcular_total()  # Obtiene primero el subtotal para usarlo como base del descuento
        self.descuento_aplicado = total * (porcentaje / 100)  # Guarda cuánto dinero se rebaja para reutilizarlo en la impresión final
        return total - self.descuento_aplicado  # Retorna el total final luego de restar el descuento calculado

    def __str__(self):  # Resume el pedido completo en una sola cadena lista para mostrarse al usuario
        detalle = ", ".join(producto.nombre for producto in self.productos) or "Sin productos"  # Construye el detalle de nombres o usa un texto alterno si la lista está vacía
        total_final = self.calcular_total() - self.descuento_aplicado  # Calcula el valor final usando el subtotal menos el descuento almacenado
        return f"Pedido {self.numero_pedido} | Cliente: {self.cliente} | Productos: {len(self.productos)} | Total: L {total_final:.2f} | Detalle: {detalle}"  # Devuelve la vista textual completa del pedido

if __name__ == "__main__":  # Ejecuta una prueba rápida del modelo solo cuando este archivo se corre directamente
    pedido = Pedido("P-1001", "Lucía")  # Crea un pedido nuevo que actuará como objeto compuesto principal del ejemplo
    pedido.agregar_producto(Producto("Latte", 65.0, "Bebida"))  # Crea y agrega un primer producto directamente al pedido
    pedido.agregar_producto(Producto("Croissant", 38.0, "Panadería"))  # Agrega un segundo producto para demostrar la composición múltiple
    pedido.agregar_producto(Producto("Tarta", 52.0, "Postre"))  # Agrega un tercer producto para completar el ejemplo de cálculo
    print(f"Total sin descuento: L {pedido.calcular_total():.2f}")  # Muestra el subtotal antes de aplicar cualquier rebaja
    print(f"Total con descuento: L {pedido.aplicar_descuento(10):.2f}")  # Aplica un 10% y muestra el total final resultante
    print(pedido)  # Imprime el pedido completo usando el método mágico __str__
▶ Comprobación — Salida esperada
Total sin descuento: L 155.00 
Total con descuento: L 139.50 
Pedido P-1001 | Cliente: Lucía | Productos: 3 | Total: L 139.50 | Detalle: Latte, Croissant, Tarta

📋 Rúbrica — 15 pts

Clase Producto bien modelada (3 pts)
Pedido con composición y MAXIMO_PRODUCTOS (4 pts)
Métodos agregar, total, descuento y __str__ correctos (5 pts)
Bloque de prueba funcional en main (3 pts)
05

Conversor de Temperaturas — Métodos de clase y estáticos

🎯 10 pts📦 @classmethod + @staticmethod🔗 Práctica U2

📋 Escenario

Un laboratorio desea trabajar con temperaturas en varias escalas sin repetir fórmulas por todo el sistema. La clase Temperatura debe usar @staticmethod para conversiones matemáticas puras y @classmethod como fábricas para crear instancias a partir de Fahrenheit o Kelvin.

🧠 Lógica de cada método — cómo construirlo

__init__()
Recibe un valor y una escala, normaliza la escala a mayúscula y guarda ambos datos. La instancia queda lista para convertir su propio estado a otras unidades.
@staticmethod
Las funciones estáticas no usan self ni cls porque solo aplican fórmulas. Son ideales para convertir Celsius a Fahrenheit, Celsius a Kelvin y Fahrenheit a Celsius.
@classmethod
Las fábricas reciben cls, convierten el valor externo a Celsius y devuelven cls(...). Así pueden crear objetos Temperatura sin acoplarse al nombre de la clase.
convertir()
Primero transforma la instancia actual a Celsius y luego genera un nuevo objeto en la escala destino. Esto permite convertir desde C, F o K siguiendo el mismo flujo lógico.
__str__()
Devuelve el valor formateado con dos decimales y el símbolo de la escala. Hace que imprimir el objeto sea más claro y legible.

📐 Diagrama de Clases

Temperatura
Atributos
+ valor : float
+ escala : str
Métodos estáticos
+ celsius_a_fahrenheit(c) : float
+ fahrenheit_a_celsius(f) : float
+ celsius_a_kelvin(c) : float
Métodos de clase / instancia
+ desde_fahrenheit(cls, f) : Temperatura
+ desde_kelvin(cls, k) : Temperatura
+ convertir(escala_destino) : Temperatura
+ __str__() : str

👤 Diagrama de Casos de Uso

«Sistema Temperatura» Técnico Crear desde Fahrenheit Crear desde Kelvin Convertir escala Mostrar lectura
📝 temperatura.pyPython 3
# ── Ejercicio 5: Conversor de Temperaturas ───────────────────────
class Temperatura:  # Modela una temperatura y concentra la lógica de conversión entre escalas
    def __init__(self, valor, escala="C"):  # Construye la instancia con un valor numérico y una escala por defecto en Celsius
        self.valor = float(valor)  # Convierte el valor a float para trabajar siempre con números decimales consistentes
        self.escala = escala.upper()  # Normaliza la escala a mayúsculas para evitar errores por diferencias de escritura

    @staticmethod  # Declara un método estático porque esta fórmula no depende de un objeto ni de la clase
    def celsius_a_fahrenheit(c):  # Convierte un valor en Celsius a Fahrenheit usando la fórmula estándar
        return (c * 9 / 5) + 32  # Devuelve la equivalencia matemática exacta en grados Fahrenheit

    @staticmethod  # Declara otro método estático para encapsular una conversión pura
    def fahrenheit_a_celsius(f):  # Convierte un valor en Fahrenheit a Celsius para reutilizarlo en fábricas y conversiones
        return (f - 32) * 5 / 9  # Aplica la fórmula inversa y devuelve el resultado en grados Celsius

    @staticmethod  # Declara un tercer método estático porque solo realiza cálculo sin usar estado interno
    def celsius_a_kelvin(c):  # Convierte un valor en Celsius a Kelvin sumando el desplazamiento absoluto
        return c + 273.15  # Devuelve el valor equivalente en Kelvin para otras conversiones y reportes

    @classmethod  # Declara un método de clase porque debe construir y retornar una nueva instancia
    def desde_fahrenheit(cls, f):  # Crea un objeto Temperatura a partir de una lectura externa en Fahrenheit
        return cls(cls.fahrenheit_a_celsius(f), "C")  # Convierte a Celsius y usa cls para fabricar la instancia resultante

    @classmethod  # Declara otro método de clase para centralizar la creación desde Kelvin
    def desde_kelvin(cls, k):  # Crea un objeto Temperatura transformando Kelvin hacia la escala base Celsius
        return cls(k - 273.15, "C")  # Resta el desplazamiento absoluto y retorna la instancia construida

    def convertir(self, escala_destino):  # Convierte la instancia actual a otra escala y retorna un nuevo objeto Temperatura
        escala_destino = escala_destino.upper()  # Normaliza la escala de destino para aceptar letras minúsculas o mayúsculas
        celsius = self.valor if self.escala == "C" else self.fahrenheit_a_celsius(self.valor) if self.escala == "F" else self.valor - 273.15  # Lleva cualquier origen a Celsius para unificar la lógica
        if escala_destino == "C":  # Revisa si el usuario pidió el resultado en Celsius
            return Temperatura(celsius, "C")  # Retorna una nueva instancia ya expresada en grados Celsius
        if escala_destino == "F":  # Revisa si el usuario pidió el resultado en Fahrenheit
            return Temperatura(self.celsius_a_fahrenheit(celsius), "F")  # Usa la fórmula estática y crea la nueva instancia convertida
        return Temperatura(self.celsius_a_kelvin(celsius), "K")  # Si no fue C ni F, retorna la conversión equivalente en Kelvin

    def __str__(self):  # Devuelve una representación amigable para imprimir la temperatura fácilmente
        return f"{self.valor:.2f} °{self.escala}"  # Formatea el valor con dos decimales y añade la escala correspondiente

if __name__ == "__main__":  # Ejecuta una demostración simple solo cuando el archivo se usa como programa principal
    t1 = Temperatura(25)  # Crea una temperatura base en Celsius para probar la conversión de instancia
    t2 = Temperatura.desde_fahrenheit(86)  # Usa el método de clase para crear una instancia equivalente a 86 °F
    t3 = Temperatura.desde_kelvin(300)  # Usa el otro método de clase para crear una instancia desde 300 K
    print(f"Original: {t1}")  # Muestra la temperatura original para establecer el punto de partida
    print(f"A Fahrenheit: {t1.convertir('F')}")  # Convierte la instancia actual a Fahrenheit y muestra el resultado
    print(f"Desde 86°F: {t2}")  # Imprime la instancia creada por la fábrica desde Fahrenheit
    print(f"Desde 300K: {t3}")  # Imprime la instancia creada por la fábrica desde Kelvin
    print(f"25°C en Kelvin: {Temperatura.celsius_a_kelvin(25):.2f}")  # Llama a un método estático directamente desde la clase para demostrar su uso
▶ Comprobación — Salida esperada
Original: 25.00 °C 
A Fahrenheit: 77.00 °F 
Desde 86°F: 30.00 °C 
Desde 300K: 26.85 °C 
25°C en Kelvin: 298.15

📋 Rúbrica — 10 pts

Uso correcto de 3 @staticmethod (3 pts)
Fábricas con @classmethod funcionales (3 pts)
convertir() y __str__ correctos (2 pts)
Demostración clara en main (2 pts)
06

Animales con Mixin — Herencia múltiple

🎯 15 pts📦 Herencia múltiple + Mixin🔗 Práctica U2

📋 Escenario

Un zoológico educativo quiere modelar animales que comparten comportamientos opcionales. Para ello se define una clase abstracta Animal y dos mixins reutilizables: VolarMixin y NadarMixin. Algunas subclases heredarán solo de Animal y otras combinarán varios comportamientos mediante herencia múltiple.

🧠 Lógica de cada método — cómo construirlo

VolarMixin / NadarMixin
Cada mixin implementa un comportamiento concreto y pequeño. No representa una entidad completa; solo aporta métodos reutilizables a las clases que lo hereden.
Animal(ABC)
La clase base abstracta centraliza atributos comunes y declara sonido() como método obligatorio. También ofrece describir() como comportamiento compartido por todas las subclases.
Perro
Hereda únicamente de Animal e implementa su propio sonido. Sirve para contrastar una subclase normal frente a las que usan mixins.
Pato
Combina Animal, VolarMixin y NadarMixin. Puede describirse, emitir sonido y además volar y nadar gracias a la herencia múltiple.
Aguila
Hereda de Animal y VolarMixin, por lo que puede volar pero no nadar. Esto deja claro que cada subclase toma solo los comportamientos que necesita.
MRO
El orden de resolución de métodos determina dónde busca Python cada comportamiento. Mostrar Pato.__mro__ ayuda a visualizar la ruta seguida por el intérprete.

📐 Diagrama de Clases

Animal (ABC)
Atributos
+ nombre : str
+ especie : str
Métodos
+ __init__() : None
+ sonido() : str
+ describir() : str
△ mixins reutilizables
VolarMixin / NadarMixin
Métodos
+ volar() : str
+ nadar() : str
△ heredan de Animal y combinan mixins
Perro / Pato / Aguila
Métodos
+ sonido() : str
+ volar() : str (según mixin)
+ nadar() : str (según mixin)

👤 Diagrama de Casos de Uso

«Sistema Animales» Cuidador Describir animal Escuchar sonido Observar vuelo Observar nado
📝 animales_mixin.pyPython 3
# ── Ejercicio 6: Animales con Mixin ──────────────────────────────
from abc import ABC, abstractmethod  # Importa utilidades del módulo abc para definir una clase base abstracta real

class VolarMixin:  # Define un mixin pequeño que agrega la capacidad de volar a quien lo herede
    def volar(self):  # Implementa el comportamiento de vuelo reutilizable para varias subclases
        return f"{self.nombre} está volando."  # Devuelve un mensaje usando el nombre almacenado por la clase principal

class NadarMixin:  # Define un mixin independiente que aporta la capacidad de nadar
    def nadar(self):  # Implementa el comportamiento de nado reutilizable para distintas especies
        return f"{self.nombre} está nadando."  # Devuelve un texto descriptivo del movimiento acuático

class Animal(ABC):  # Declara la superclase abstracta que concentra datos comunes y contrato obligatorio
    def __init__(self, nombre, especie):  # Inicializa los atributos básicos que todo animal del sistema necesita
        self.nombre = nombre  # Guarda el nombre propio para identificar al animal en pantalla
        self.especie = especie  # Guarda la especie para complementar la descripción general

    @abstractmethod  # Obliga a cada subclase concreta a definir su sonido característico
    def sonido(self):  # Declara el método abstracto que actúa como contrato polimórfico
        pass  # Se deja vacío porque la implementación depende de cada animal concreto

    def describir(self):  # Ofrece un método compartido que cualquier subclase puede reutilizar sin cambios
        return f"{self.nombre} ({self.especie})"  # Devuelve una descripción corta del animal con nombre y especie

class Perro(Animal):  # Hereda solo de Animal para mostrar un caso sin herencia múltiple
    def sonido(self):  # Implementa el contrato abstracto con el sonido típico de un perro
        return "Guau guau"  # Retorna el ladrido que diferencia a esta subclase de las demás

class Pato(Animal, VolarMixin, NadarMixin):  # Combina la base abstracta con dos mixins para reutilizar comportamientos
    def sonido(self):  # Implementa el método abstracto exigido por Animal
        return "Cuac cuac"  # Retorna el sonido propio del pato para completar su comportamiento

class Aguila(Animal, VolarMixin):  # Combina Animal con VolarMixin para agregar solo el comportamiento de vuelo
    def sonido(self):  # Implementa el contrato abstracto con el sonido de un águila
        return "Screech"  # Retorna el sonido característico de esta ave rapaz

if __name__ == "__main__":  # Ejecuta una prueba demostrativa del polimorfismo y la herencia múltiple
    perro = Perro("Max", "Canino")  # Crea un perro que solo posee los métodos de Animal y su propia implementación
    pato = Pato("Donald", "Anátida")  # Crea un pato que combinará describir, sonido, volar y nadar
    aguila = Aguila("Ares", "Ave rapaz")  # Crea un águila que describirá, sonará y volará gracias al mixin
    print(f"{perro.describir()} hace: {perro.sonido()}")  # Muestra que Perro cumple el contrato abstracto sin mixins adicionales
    print(f"{pato.describir()} hace: {pato.sonido()}")  # Muestra la descripción y el sonido del pato como subclase concreta
    print(f"Puede volar: {pato.volar()}")  # Demuestra que el pato obtiene volar() desde VolarMixin
    print(f"Puede nadar: {pato.nadar()}")  # Demuestra que el pato obtiene nadar() desde NadarMixin
    print(f"{aguila.describir()} hace: {aguila.sonido()}")  # Muestra el comportamiento concreto del águila como otra subclase
    print(f"Puede volar: {aguila.volar()}")  # Demuestra que el águila reutiliza el mixin de vuelo
    print(f"MRO Pato: {[clase.__name__ for clase in Pato.__mro__]}")  # Imprime el Method Resolution Order para visualizar el orden de búsqueda de métodos
▶ Comprobación — Salida esperada
Max (Canino) hace: Guau guau 
Donald (Anátida) hace: Cuac cuac 
Puede volar: Donald está volando. 
Puede nadar: Donald está nadando. 
Ares (Ave rapaz) hace: Screech 
Puede volar: Ares está volando. 
MRO Pato: ['Pato', 'Animal', 'ABC', 'VolarMixin', 'NadarMixin', 'object']

📋 Rúbrica — 15 pts

Animal(ABC) y @abstractmethod correctos (4 pts)
Mixins reutilizables bien implementados (3 pts)
Perro, Pato y Aguila con herencia adecuada (5 pts)
Explicación o demostración del MRO (3 pts)