📘 Programación Orientada a Objetos

Universidad Tecnológica de Hermosillo  |  TIID  |  Docente: Bernardo Prado Díaz  |  Mayo–Agosto 2026

🎓 3° Cuatrimestre ⚡ 59 Horas 🐍 Python + MySQL

🎓 Bienvenido al Curso de POO

Esta guía digital cubre los 4 bloques de la asignatura de Programación Orientada a Objetos con explicaciones paso a paso, ejemplos reales de negocios, referencias de sintaxis y guías de instalación. Todo asumiendo que empiezas desde cero.

📅 Mayo–Agosto 2026 🏫 UTH – TIID 👨‍🏫 Bernardo Prado Díaz 💻 Python & MySQL
📐

Unidad 1 – Modelado

Conceptos POO, diagramas UML, patrones de diseño, Draw.io, PyCharm.

17 hrs
🐍

Unidad 2 – Sintaxis POO

Python orientado a objetos completo: clases, herencia, polimorfismo, encapsulamiento.

14 hrs
🎨

Unidad 3 – FLET + MySQL

Interfaces gráficas con FLET, conexión a MySQL, principios SOLID, clean code.

14 hrs
🚀

Unidad 4 – IDE, Git y Proyecto

PyCharm avanzado, Git, manejo de excepciones, pruebas unitarias y proyecto final.

14 hrs

🛠️ Herramientas que usaremos

🐍
Python

Lenguaje principal del curso. Descarga desde python.org

💡
PyCharm Community Edition

IDE profesional para Python. Descarga en jetbrains.com/pycharm

🆚
Visual Studio Code

Editor de código ligero y potente con soporte Python. Descarga en code.visualstudio.com

📊
Draw.io

Diagramas UML online. Entra en app.diagrams.net — sin instalación.

🐬
WampServer 3.3.7

Servidor MySQL local para Windows. Descarga en wampserver.com

🦦
DBeaver 25.1.0

Editor visual de bases de datos. Descarga en dbeaver.io

🎨
FLET (librería Python)

Librería para GUIs modernas. Se instala con: pip install flet

🔀
Git

Control de versiones. Descarga en git-scm.com

💡 Tip inicial Si ves un error en Python que dice "command not found", reinicia tu computadora después de instalar Python y asegúrate de haber marcado "Add Python to PATH" durante la instalación.

📐 Unidad 1 — Modelado Orientado a Objetos

Aprenderás los conceptos fundamentales de la Programación Orientada a Objetos (POO), cómo representarlos con diagramas UML y los patrones de diseño más usados en la industria.

⏱ 17 horas📊 Exámen y Prácticas en clase - Calificación🛠 Draw.io + PyCharm

📋 Temas de la Unidad

🏛️

1.1 Conceptos POO

Clases, objetos, atributos, métodos, herencia, polimorfismo, encapsulamiento, abstracción.

📊

1.2 Diagramas UML

Casos de uso, clases y secuencia usando Draw.io.

🧩

1.3 Patrones de Diseño

Singleton, Factory Method y Observer.

🗺️ Mapa Conceptual — Unidad 1: Del mundo real al código
🌍 Mundo Real
🏨 Hotel Fiesta Inn
🛏️ Habitaciones
👤 Clientes
📋 Reservaciones
modelar con
POO
🐍 Código Python
📦 class Habitacion
📦 class Cliente
📦 class Reservacion
+ herencia, métodos
documentar con
UML
📊 Diagramas UML
👤 Casos de Uso
📦 Clases
📋 Secuencia
en Draw.io
soluciones con
Patrones
🧩 Design Patterns
🔂 Singleton
🏭 Factory
👁️ Observer

🔧 Guía de Instalación: Python y PyCharm

Descargar Python

Ve a python.org/downloads y haz clic en el botón amarillo "Download Python 3.x.x". Se descargará un archivo .exe.

Instalar Python — ¡IMPORTANTE!

Ejecuta el instalador. En la primera pantalla, marca la casilla "Add Python to PATH" antes de hacer clic en "Install Now". Esto es crucial para que Python funcione desde cualquier lugar.

Verificar instalación

Abre CMD (tecla Windows + R, escribe cmd, Enter) y escribe:

CMD
python --version

Deberías ver algo como Python 3.12.x. Si aparece error, reinicia y repite la instalación marcando PATH.

Descargar PyCharm Community

Ve a jetbrains.com/pycharm/download. Selecciona Community Edition (es gratuita). Descarga el instalador Windows.

Instalar PyCharm

Ejecuta el instalador. En "Installation Options" marca:

  • ✅ Create Desktop Shortcut
  • ✅ Add "Open Folder as Project"
  • ✅ Add launchers dir to PATH
Crear tu primer proyecto

Abre PyCharm → New Project → ponle nombre (ej: poo-unidad1) → asegúrate de que el intérprete sea el Python que instalaste → clic Create.

Configurar Draw.io (sin instalación)

Entra a app.diagrams.net en tu navegador. Elige "Device" para guardar en tu computadora. ¡Listo, no se instala nada!

🏛️ Tema 1.1 — Conceptos Fundamentales de POO

🤔 ¿Qué es la POO? Es una forma de programar que organiza el código igual que el mundo real: en objetos. Un objeto tiene características (atributos) y puede hacer cosas (métodos). Por ejemplo, un Carro tiene color, marca y puede arrancar, frenar.

Los 4 pilares de la POO

🎭

1. Abstracción

Mostrar solo lo necesario y ocultar los detalles complejos. Como un control remoto: presionas un botón y funciona, sin saber la electrónica detrás.

🔒

2. Encapsulamiento

Esconder los datos internos del objeto. Solo accedes a ellos mediante métodos controlados. Como la caja negra de un avión: no ves el mecanismo interno, solo el resultado.

🧬

3. Herencia

Una clase puede heredar atributos y métodos de otra clase padre. Como que un Empleado hereda de Persona (nombre, edad) y agrega su propio sueldo.

🎪

4. Polimorfismo

El mismo método puede comportarse diferente según el objeto. calcular_pago() funciona distinto para un empleado fijo que para un freelance.

📝 Referencia de Sintaxis — Clases en Python

ConceptoSintaxis PythonDescripción
Definir claseclass NombreClase:Crea un molde (plantilla) de objetos
Constructordef __init__(self, param):Se ejecuta al crear un objeto. self = el propio objeto
Atributoself.nombre = nombreGuarda un dato dentro del objeto
Métododef mi_metodo(self):Función que pertenece a la clase
Crear objetoobj = NombreClase(args)Instancia un objeto de la clase
Usar objetoobj.metodo()Llama a un método del objeto
Herenciaclass Hijo(Padre):Hijo hereda todo de Padre
Llamar padresuper().__init__()Llama al constructor del padre
Atrib. privadoself.__dato = xSolo accesible dentro de la clase
Atrib. protegidoself._dato = xConvención: no tocar desde afuera
🏨 Ejemplo de Negocio: Sistema de Reservaciones de Hotel

Modelaremos un hotel con POO. El hotel tiene Habitaciones (número, tipo, precio) y Clientes (nombre, email). Una Reservación conecta ambos con fechas de entrada y salida.

🖼️ La Clase como Molde — Un molde, múltiples objetos
📦 CLASE: Habitacion
─── Atributos ───
• numero: int
• tipo: str
• precio_noche: float
• disponible: bool
─── Métodos ───
+ reservar() → str
+ liberar() → str
+ __str__() → str
⚙️
instanciar
hab101 = Habitacion(101, "Suite", 1200)
hab102 = Habitacion(102, "Doble", 650)
hab103 = Habitacion(103, "Sencilla", 400)
📦 CLASE: Cliente
• nombre: str
• email: str
+ agregar_reservacion()
↔️
📦 CLASE: Reservacion
• cliente: Cliente
• habitacion: Habitacion
+ calcular_costo()
+ confirmar()
↔️
📦 CLASE: Habitacion
• numero: int
• precio_noche: float
+ reservar()
💡 Analogía: Como el plano arquitectónico de las habitaciones del Hotel Fiesta Inn de Hermosillo — un plano, muchas habitaciones reales
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
Que una clase es un molde reutilizable: definimos Habitacion, Cliente y Reservacion como plantillas, y luego creamos múltiples objetos de cada una.
🚀 ¿QUÉ LOGRAMOS?
Un sistema funcional de reservaciones de hotel donde los objetos interactúan entre sí: la reserva confirma la habitación y se agrega al cliente automáticamente.
🧠 ¿QUÉ APRENDEMOS?
Definir clases con __init__, crear objetos, usar self, escribir métodos, llamar métodos de un objeto desde otro, y usar __str__ para imprimir.
hotel_modelo.py — Ejemplo de negocio: Hotel
# ═══════════════════════════════════════════════════════
# SISTEMA DE HOTEL — Unidad 1, ejemplo de negocio
# Aprende a leer: cada línea tiene su explicación ↓
# ═══════════════════════════════════════════════════════

class Habitacion:  # Define la clase Habitacion
    """
    Esta es la CLASE Habitacion.
    Una clase es como un MOLDE para crear objetos.
    Imagina que el molde define cómo es cada habitación.
    """

    def __init__(self, numero, tipo, precio_noche):  # Constructor: inicializa el objeto con los datos recibidos
        # __init__ es el CONSTRUCTOR: se ejecuta al crear cada habitación.
        # 'self' representa a ESTA habitación específica (el objeto).
        # Los parámetros son datos que recibimos al crear la habitación.
        self.numero = numero              # Guardamos el número (ej: 101)
        self.tipo = tipo                  # Guardamos el tipo (ej: "suite")
        self.precio_noche = precio_noche  # Precio por noche (ej: 850.0)
        self.disponible = True           # Al inicio, siempre disponible

    def reservar(self):  # Método que reservar
        # Este es un MÉTODO: una acción que puede hacer la habitación.
        if self.disponible:  # Verifica self.disponible antes de continuar
            self.disponible = False      # Marcamos como NO disponible
            return f"✅ Habitación {self.numero} reservada con éxito."  # Retorna el valor calculado al que llamó este método
        else:  # Si la condición anterior no se cumplió
            return f"❌ Habitación {self.numero} ya está ocupada."  # Retorna el valor calculado al que llamó este método

    def liberar(self):  # Método que liberar
        self.disponible = True  # Guarda disponible como atributo de instancia
        return f"🔓 Habitación {self.numero} disponible nuevamente."  # Retorna el valor calculado al que llamó este método

    def __str__(self):  # Define cómo se muestra el objeto al usar print()
        # __str__ define cómo se muestra el objeto cuando usamos print()
        estado = "Disponible" if self.disponible else "Ocupada"  # Asigna un valor inicial o calculado a estado
        return f"Hab. {self.numero} | {self.tipo} | ${self.precio_noche}/noche | {estado}"  # Retorna el valor calculado al que llamó este método


class Persona:  # Define la clase Persona
    # CLASE BASE (padre). Contiene lo que tienen en común clientes y empleados.
    def __init__(self, nombre, email):  # Constructor: inicializa el objeto con los datos recibidos
        self.nombre = nombre  # Guarda nombre como atributo de instancia
        self.email = email  # Guarda email como atributo de instancia


class Cliente(Persona):  # Cliente hereda de Persona — recibe todos sus atributos y métodos
    """
    HERENCIA: Cliente HEREDA de Persona.
    Escrito como: class Cliente(Persona)
    Esto significa que Cliente tiene TODO lo de Persona
    (nombre y email), MÁS sus propias cosas.
    """
    def __init__(self, nombre, email, telefono):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(nombre, email)  # Llama al constructor de Persona
        self.telefono = telefono           # Agrega atributo propio
        self.reservaciones = []            # Lista vacía de reservaciones

    def agregar_reservacion(self, reservacion):  # Método que agregar reservacion
        self.reservaciones.append(reservacion)  # Agrega un elemento al final de self.reservaciones


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  # Guarda cliente como atributo de instancia
        self.habitacion = habitacion  # Guarda habitacion como atributo de instancia
        self.fecha_entrada = fecha_entrada  # Guarda fecha_entrada como atributo de instancia
        self.fecha_salida = fecha_salida  # Guarda fecha_salida como atributo de instancia

    def calcular_costo(self, noches):  # Método que calcular costo
        return self.habitacion.precio_noche * noches  # Retorna el valor calculado al que llamó este método

    def confirmar(self):  # Método que confirmar
        resultado = self.habitacion.reservar()   # Llama al método de Habitacion
        self.cliente.agregar_reservacion(self)   # Agrega a la lista del cliente
        return resultado  # Retorna el valor calculado al que llamó este método


# ══════════════════ PROGRAMA PRINCIPAL ══════════════════
# Aquí creamos objetos (instancias) de nuestras clases

hab101 = Habitacion(101, "Suite Ejecutiva", 1200.0)  # Asigna un valor inicial o calculado a hab101
hab102 = Habitacion(102, "Doble Estándar", 650.0)  # Asigna un valor inicial o calculado a hab102

cliente1 = Cliente("Ana García", "ana@mail.com", "662-100-2000")  # Asigna un valor inicial o calculado a cliente1

reserva = Reservacion(cliente1, hab101, "2026-06-01", "2026-06-04")  # Asigna un valor inicial o calculado a reserva
print(reserva.confirmar())                 # ✅ Habitación 101 reservada
print(hab101)                              # Muestra estado de la habitación
print(f"Costo: ${reserva.calcular_costo(3)}")  # Costo por 3 noches

📊 Tema 1.2 — Diagramas UML con Draw.io

⚠️ ¿Por qué UML? UML (Unified Modeling Language) es como el plano de una construcción: antes de escribir código, dibujamos cómo se verá la estructura del programa. Los reclutadores de empresas te pedirán diagramas UML.

Tipos de diagramas que usaremos

👤

Casos de Uso

Muestra quién usa el sistema (actores) y qué puede hacer (casos). Se dibujan con óvalos y actores palito.

📦

Diagrama de Clases

Muestra las clases, sus atributos, métodos y las relaciones entre ellas (herencia, composición, asociación).

📋

Diagrama de Secuencia

Muestra cómo interactúan los objetos en el tiempo. Perfecto para entender el flujo de una operación.

Pasos para crear diagrama de clases en Draw.io

🖥️ Así luce la interfaz de Draw.io — familiarízate antes de empezar
🔍 Buscar formas
Search shapes…
GENERAL
▭ Rectangle
◯ Ellipse
⬦ Diamond
UML
▤ Class ← busca este
👤 Actor
◷ UseCase
📌 Lienzo — arrastra formas aquí
Estudiante
- nombre: str
- matricula: int
- promedio: float
+ mostrar_info()
+ esta_aprobado()
← doble clic para editar
⚙️ Propiedades
Relleno
Borde
1px solid ▾
Fuente
Helvetica 12 ▾
📄 File
✏️ Edit
🔍 View
⬇️ Export as PNG
☁️ Guardado automático
📦
Ejercicio 1 de 3
Diagrama de Clases
Escenario: Sistema de Estudiantes UTH
💡 ¿Qué vas a lograr? Dibujar 3 clases conectadas: Persona (padre), Estudiante y Profesor (hijos), con sus atributos, métodos y flechas de herencia.
Abrir Draw.io y crear diagrama en blanco

Ve a app.diagrams.net → aparece una ventana emergente → haz clic en "Create New Diagram" → selecciona "Blank Diagram" (diagrama en blanco) → clic en "Create".

📄 Blank Diagram ← elige este
✅ Lienzo vacío listo
Buscar la forma "Class" en el panel izquierdo

En la barra de búsqueda del panel izquierdo escribe la palabra class y presiona Enter. Aparecerá la forma de clase UML (un rectángulo dividido en 3 secciones).

📌 Qué objeto arrastrar:
ClassName
+ attribute: type
+ method(): type
← este shape
Arrastra este rectángulo al lienzo. Cada sección es editable con doble clic: arriba = nombre, medio = atributos, abajo = métodos.
Crear la clase Persona (clase padre)

Arrastra un Class shape al lienzo. Doble clic en la sección superior y escribe Persona. Luego edita cada sección:

Persona
- nombre: str
- edad: int
- correo: str
+ presentarse(): str
+ __str__(): str
Notación UML:
- (menos) = atributo privado
+ (más) = método/atributo público
# (gato) = atributo protegido
Formato: visibilidad nombre: tipo
Crear las clases Estudiante y Profesor

Arrastra 2 Class shapes más al lienzo. Colócalos debajo de Persona. Edítalos así:

Estudiante
- matricula: str
- promedio: float
- semestre: int
+ inscribirse(): void
+ esta_aprobado(): bool
Profesor
- num_empleado: str
- especialidad: str
- salario: float
+ impartir_clase(): void
+ calificar(): float
Dibujar la flecha de herencia (triángulo hueco)

Esto es lo más importante del diagrama de clases. La flecha de herencia dice "es un tipo de". Sigue estos pasos exactos:

📌 Cómo dibujar la flecha de herencia en Draw.io:
Paso A — Clic en la clase hija
Haz clic en Estudiante. Aparecerán puntos azules en los bordes.
Paso B — Arrastra desde el punto azul
Desde el punto azul superior, arrastra hacia la clase Persona (padre).
Paso C — Cambiar estilo de flecha
Clic derecho en la línea → "Edit Style" → busca endArrow → cámbialo a block (triángulo hueco).
🎯 RESULTADO ESPERADO — Diagrama de Clases
Persona
- nombre: str
- edad: int
+ presentarse(): str
Estudiante
- matricula: str
- promedio: float
+ inscribirse(): void
Profesor
- especialidad: str
- salario: float
+ impartir_clase(): void
△ = triángulo hueco = herencia. La flecha apunta siempre hacia el PADRE.
Exportar el diagrama

Cuando tu diagrama esté listo: menú File → Export as → PNG. Se descargará una imagen lista para pegar en tu reporte o tarea.

📄 File
Export as
PNG ✅
o también puedes exportar como XML para editar después
👤
Ejercicio 2 de 3
Diagrama de Casos de Uso
Escenario: Sistema Escolar UTH — ¿quién hace qué?
💡 ¿Qué representa? Los actores (personas/sistemas que usan la app) y las acciones que pueden hacer, todo dentro de un rectángulo que representa el sistema.
🧩 Formas que usarás en Draw.io — ubícalas en el panel izquierdo:
🧍
ACTOR
Busca: actor o umlActor
Es la figura de palito con nombre abajo.
Iniciar sesión
CASO DE USO
Busca: ellipse o useCase
Óvalo con el nombre de la acción.
Sistema Escolar UTH
LÍMITE DEL SISTEMA
Busca: rectangle
Rectángulo grande que rodea los óvalos.
🧍
acción
ASOCIACIÓN
Línea simple (no flecha). Conecta actor con caso de uso. Pasa el cursor por el borde del actor → arrastra.
Dibujar el límite del sistema (rectángulo grande)

Busca Rectangle en el panel y arrastra un rectángulo grande al lienzo. Doble clic en el borde superior y escribe: Sistema Escolar UTH. Todos los casos de uso van DENTRO de este rectángulo.

Agregar los actores FUERA del rectángulo

Busca Actor (o umlActor) y arrastra 2 figuras: una a la izquierda del rectángulo (escribe Estudiante) y otra a la derecha (escribe Administrador). Los actores van siempre fuera del sistema.

Agregar los casos de uso (óvalos) DENTRO del rectángulo

Busca Ellipse y arrastra 4 óvalos dentro del rectángulo. Nómbralos:

Iniciar sesión
Ver calificaciones
Registrar alumno
Generar reporte
Conectar actores con sus casos de uso

Pasa el cursor encima del actor → aparecen puntos azules en los bordes → arrastra desde ahí hasta el óvalo. La línea es simple (sin punta de flecha). Conecta:

🧍 Estudiante ——— Iniciar sesión
🧍 Estudiante ——— Ver calificaciones
🧍 Administrador ——— Iniciar sesión
🧍 Administrador ——— Registrar alumno
🧍 Administrador ——— Generar reporte
🎯 RESULTADO ESPERADO — Diagrama de Casos de Uso
🧍
Estudiante
Sistema Escolar UTH
Iniciar sesión
Ver calificaciones
Registrar alumno
Generar reporte
🧍
Administrador
📋
Ejercicio 3 de 3
Diagrama de Secuencia
Escenario: Flujo de inicio de sesión del estudiante
💡 ¿Qué representa? Cómo los objetos se comunican en el tiempo. El tiempo corre de arriba hacia abajo. Cada objeto tiene una línea vertical (línea de vida) y se mandan mensajes horizontales.
🧩 Formas que usarás para el diagrama de secuencia:
:Estudiante
LIFELINE (Línea de vida)
Busca: lifeline en la búsqueda. Rectángulo arriba + línea punteada abajo.
iniciarSesion(usuario, pass)
MENSAJE (flecha sólida)
Línea con flecha. La etiqueta es el nombre del método llamado.
return "sesión iniciada"
RESPUESTA (flecha punteada)
Línea punteada de regreso. Indica el valor retornado.
ACTIVATION BOX
Rectángulo estrecho sobre la lifeline. Indica cuándo el objeto está activo procesando.
Colocar las 3 lifelines (de izquierda a derecha)

Busca lifeline en el panel. Arrastra 3 lifelines horizontalmente. Nómbralos de izquierda a derecha:

:Estudiante
:Sistema
:BaseDatos
Dibujar los mensajes (flechas horizontales)

Para cada flecha: pasa el cursor por la línea punteada del lifeline → aparecen puntos → arrastra horizontalmente hacia el otro lifeline. Etiqueta cada flecha con el método:

1
:Estudiante
:Sistema
iniciarSesion(usuario, pass)
2
:Sistema
:BaseDatos
verificarCredenciales(usuario, pass)
3
:BaseDatos
:Sistema
return True / False
4
:Sistema
:Estudiante
return "Bienvenido, Ana"
Resultado final del diagrama de secuencia
🎯 RESULTADO ESPERADO — Diagrama de Secuencia
:Estudiante
:Sistema
:BaseDatos
1
iniciarSesion(usuario, pass)
2
verificarCredenciales(u, p)
3
return True
4
return "Bienvenido, Ana"
▶ flechas sólidas = llamadas  |  ◀ - - - flechas punteadas = respuestas  |  tiempo corre hacia abajo
✅ ¡Completaste los 3 ejercicios! Ya sabes hacer Diagrama de Clases, Casos de Uso y Secuencia en Draw.io. Exporta cada uno como PNG y guárdalos con estos nombres: diagrama-clases.png, diagrama-casos-uso.png, diagrama-secuencia.png.

🧩 Tema 1.3 — Patrones de Diseño

💡 ¿Qué es un patrón de diseño? Son soluciones probadas a problemas comunes de programación. En lugar de inventar soluciones desde cero, usas recetas que ya funcionan en miles de proyectos.

Patrón Singleton — "Solo existe uno"

Útil cuando solo debe existir UN objeto de una clase. Ejemplo: la configuración del sistema.

🖼️ Patrón Singleton — Solo puede existir UNA instancia
⛔ cfg1 = ConfiguracionHotel()
⛔ cfg2 = ConfiguracionHotel()
🏨 UNA sola instancia
cfg1 is cfg2
→ True ✅
Mismo objeto en memoria
💡 Analogía: Como el Director General del Hotel UTH Grand — solo puede haber uno en toda la empresa
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
El patrón Singleton: que sin importar cuántas veces llames a ConfiguracionHotel(), Python siempre devuelve el mismo objeto único en memoria.
🚀 ¿QUÉ LOGRAMOS?
Una clase de configuración del hotel que garantiza que cfg1 is cfg2 devuelve True — ambas variables apuntan al mismo objeto.
🧠 ¿QUÉ APRENDEMOS?
Cómo funciona __new__ en Python (se llama antes de __init__), y cuándo usar el Singleton: configuraciones, conexiones a BD, logs del sistema.
patron_singleton.py
class ConfiguracionHotel:  # Define la clase ConfiguracionHotel
    """
    PATRÓN SINGLETON: garantiza que solo exista
    UNA instancia de esta clase en todo el programa.
    Útil para configuraciones, conexiones a BD, etc.
    """
    _instancia = None   # Almacena la única instancia (empieza vacía)

    def __new__(cls):  # Método que new
        # __new__ se llama ANTES de __init__, al crear el objeto.
        # Si no existe instancia, la creamos; si ya existe, devolvemos la misma.
        if cls._instancia is None:  # Verifica cls._instancia is None antes de continuar
            cls._instancia = super().__new__(cls)  # Asigna un valor inicial o calculado a cls._instancia
        return cls._instancia  # Retorna el valor calculado al que llamó este método

    def __init__(self):  # Constructor: inicializa el objeto con los datos recibidos
        self.nombre_hotel = "Hotel UTH Grand"  # Guarda nombre_hotel como atributo de instancia
        self.ciudad = "Hermosillo, Sonora"  # Guarda ciudad como atributo de instancia
        self.max_habitaciones = 50  # Guarda max_habitaciones como atributo de instancia

# Probamos que solo hay UNA instancia:
cfg1 = ConfiguracionHotel()  # Asigna un valor inicial o calculado a cfg1
cfg2 = ConfiguracionHotel()  # Asigna un valor inicial o calculado a cfg2
print(cfg1 is cfg2)   # True — ¡son el mismo objeto!
print(cfg1.nombre_hotel)  # Muestra en consola el resultado

Patrón Factory — "Fábrica de objetos"

🖼️ Patrón Factory — Una fábrica que crea objetos según el tipo
HabitacionFactory.crear("suite")
🛏️ Suite Ejecutiva $1,500
HabitacionFactory.crear("doble")
🛏️🛏️ Doble Estándar $700
💡 Analogía: Como la cocina de un restaurante de Hermosillo — pides "hamburguesa" y la cocina sabe exactamente cómo prepararla
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
El patrón Factory: delegar la creación de objetos a una clase especializada, en lugar de crear objetos directamente con Habitacion(...).
🚀 ¿QUÉ LOGRAMOS?
Una fábrica de habitaciones: pasas el tipo ("suite", "doble", "sencilla") y obtienes un objeto ya configurado con sus valores correctos.
🧠 ¿QUÉ APRENDEMOS?
Cómo usar @staticmethod para métodos que no necesitan self, y el principio de encapsular la lógica de creación para que el resto del código sea más limpio.
patron_factory.py
class HabitacionFactory:  # Define la clase HabitacionFactory
    """
    PATRÓN FACTORY: una clase que se encarga de CREAR otros objetos.
    En lugar de crear objetos directamente con Habitacion(...),
    le pedimos a la fábrica que lo haga según el tipo.
    """
    @staticmethod  # Decorador: este método no necesita self (no depende del objeto)
    def crear(tipo):  # Método que crear
        # Según el tipo, creamos y configuramos la habitación
        if tipo == "suite":  # Verifica tipo == "suite" antes de continuar
            return Habitacion(0, "Suite Ejecutiva", 1500.0)  # Retorna el valor calculado al que llamó este método
        elif tipo == "doble":  # Evalúa otra condición: tipo == "doble"
            return Habitacion(0, "Doble Estándar", 700.0)  # Retorna el valor calculado al que llamó este método
        elif tipo == "sencilla":  # Evalúa otra condición: tipo == "sencilla"
            return Habitacion(0, "Sencilla", 450.0)  # Retorna el valor calculado al que llamó este método
        else:  # Si la condición anterior no se cumplió
            raise ValueError(f"Tipo desconocido: {tipo}")  # Lanza una excepción si hay un error; detiene el flujo normal

# Uso: le pedimos a la fábrica
suite = HabitacionFactory.crear("suite")  # Asigna un valor inicial o calculado a suite
doble = HabitacionFactory.crear("doble")  # Asigna un valor inicial o calculado a doble
print(suite)  # Muestra en consola el resultado
print(doble)  # Muestra en consola el resultado

📋 Criterios de Evaluación — Unidad 1

CriterioEstratégicoAutónomoBásicoReceptivo
Orden/OrganizaciónCódigo impecable, comentado, estructura profesionalBien organizado con comentarios claveOrganizado pero sin comentariosDesorganizado o incompleto
Diagramas UMLDiagramas completos, correctos y bien etiquetadosDiagramas correctos con algunos detalles faltantesDiagramas básicos incompletosIntento de diagrama incorrecto
Conceptos POODomina todos los pilares y los aplica correctamenteAplica correctamente la mayoría de conceptosAplica algunos conceptos básicosConfunde los conceptos
Patrones DiseñoImplementa y explica Singleton, Factory, ObserverImplementa correctamente 2 patronesImplementa 1 patrón con errores menoresNo implementa o implementa incorrectamente
🎬

Videos de Apoyo — Unidad 1: Modelado Orientado a Objetos

Videos seleccionados para reforzar los conceptos de esta unidad. Míralos después de clase para afianzar lo aprendido.

POO
¿Qué es la POO? — Explicación Visual

Introducción a clases, objetos y los 4 pilares con animaciones didácticas

🕐 ~15 minES✓ Recomendado
UML
Diagramas UML — Clases y Relaciones paso a paso

Cómo dibujar diagramas de clases en Draw.io desde cero

🕐 ~20 minES✓ Recomendado
Patrones
Patrones de Diseño — Singleton y Factory

Implementación práctica de los patrones más usados en la industria

🕐 ~25 minES✓ Recomendado
Python
Python desde Cero — Instalación y Primer Programa

Instala Python y PyCharm, ejecuta tu primer script paso a paso

🕐 ~18 minES✓ Recomendado
🏛 4 Pilares
Los 4 Pilares de la POO — Explicación Completa

Abstracción, Encapsulamiento, Herencia y Polimorfismo explicados con ejemplos visuales y código Python

🕐 ~22 minES✓ Recomendado
🏛 VS Code
Ejecutar Python en Visual Studio Code — Explicación Completa

Aprende a configurar y ejecutar Python en Visual Studio Code paso a paso

🕐 ~22 minES✓ Recomendado
🎥

Videos de Clase — Unidad 1

Grabaciones de las sesiones presenciales. Si faltaste o quieres repasar, estas grabaciones son tu mejor aliado. Se actualizan después de cada clase.

Sesión 1
Clase 1 — Introducción a POO y Python
DSM3-1

Conceptos básicos: clases, objetos, atributos, métodos

S1📅 Confirmado🕐 11 Mayo 2026
Sesión 1
Clase 1 — Introducción a POO y Python
DSM3-2

Conceptos básicos: clases, objetos, atributos, métodos

S1📅 Confirmado🕐 11 Mayo 2026
Sesión 2
Clase 2 — Introducción a POO y Python
DSM3-1

Conceptos básicos: clases, objetos, atributos, métodos

S2📅 Confirmado🕐 12 Mayo 2026
Sesión 2
Clase 2 — Introducción a POO y Python
DSM3-2

Conceptos básicos: clases, objetos, atributos, métodos

S2📅 Confirmado🕐 12 Mayo 2026
Sesión 3
Clase 3 — Introducción a POO y Python
DSM3-1

Conceptos básicos: clases, objetos, atributos, métodos

S3📅 Confirmado🕐 18 Mayo 2026
Sesión 3
Clase 3 — Introducción a POO y Python
DSM3-2

Conceptos básicos: clases, objetos, atributos, métodos

S3📅 Confirmado🕐 18 Mayo 2026
Sesión 4
Clase 4 — Introducción a POO y Python
DSM3-1

Conceptos básicos: clases, objetos, atributos, métodos

S4📅 Confirmado🕐 19 Mayo 2026
Sesión 4
Clase 4 — Introducción a POO y Python
DSM3-2

Conceptos básicos: clases, objetos, atributos, métodos

S4📅 Confirmado🕐 19 Mayo 2026
Sesión 5
Clase 5 — Introducción a POO y Python
DSM3-1

Conceptos básicos: clases, objetos, atributos, métodos, diagramas y patrones de diseño

S5📅 Confirmado🕐 25 Mayo 2026
Sesión 5
Clase 5 — Introducción a POO y Python
DSM3-2

Conceptos básicos: clases, objetos, atributos, métodos, diagramas y patrones de diseño

S5📅 Confirmado🕐 25 Mayo 2026
💡 ¿Cómo aprovecharlos? Pausa el video cada vez que el profesor escribe código, escríbelo tú mismo y luego compara. Los errores que cometes y resuelves son los que más aprendes.
📘 Ir a la Práctica Guiada — Unidad 1 → 📝 Ejercicios 01 & 02 — Videoteca & Nómina → 📦 Descargar Apoyo — Funciones ↓ 🧠 Guía Completa — if / elif / else →

📖 Material de Apoyo — Guía Didáctica POO

📚
Guía Didáctica Interactiva

Sistema de Gestión Hotelera — POO Línea por Línea

Guía completa con explicación línea por línea de cada bloque de código del sistema de hotel. Incluye anatomía de clases, atributos públicos/privados/protegidos, herencia, plantillas rellenables y cómo crear snippets en VS Code.

🏛️ Clases & Objetos 🔑 self & __init__ 🧬 Herencia 🔒 Público/Privado 📋 Plantillas ⚡ Snippets VS Code
Secciones 1–5
La Clase Habitacion · Métodos · Herencia · Orquestación · Flujo de datos
Sección A — Conceptos Visuales
Blueprint de clases · Atributos vs Métodos · Ejemplos por clase
Sección B — Cómo Piensa Python
5 pasos: leer clase → instanciar → atributos → encadenamiento → GC
Sección C — Acceso a Atributos
Público · Protegido · Privado · Name mangling · Getters/Setters
Sección D — Plantillas
Plantilla maestra rellenable · Ejemplo Producto · 4 snippets VS Code
Sección E — Cómo Crear Snippets
Conversión [corchete] → ${n:} · Simulación paso a paso · Tab stops

🎯 Ejercicio Integrador — Guía Completa para Examen Unidad 1

🏦
📝 EJERCICIO EXAMEN — UNIDAD 1 ✅ CASO REAL ⏱ ~60 MIN

🏦 Sistema de Gestión Bancaria — BancoUTH

Partirás de tres diagramas UML para diseñar e implementar en Python un sistema bancario completo. Este ejercicio cubre todos los conceptos de la Unidad 1. Cada sección está explicada línea por línea.

🔷
Abstracción
Clase abstracta Cuenta
🧬
Herencia
Ahorro y Corriente
🎭
Polimorfismo
calcular_interes()
🔒
Encapsulamiento
__saldo privado

📊 Rúbrica de Evaluación — 100 puntos

# Sección evaluada ¿Qué se revisa? Puntos
1 class Transaccion Atributos correctos, constructor __init__, método __str__ 8
2 class Cuenta (ABC) Abstracción con ABC, atributo privado __saldo, protegido _historial, métodos abstractos, getter 22
3 class CuentaAhorro Herencia de Cuenta, super().__init__(), retirar() con reglas, calcular_interes() 15
4 class CuentaCorriente Herencia de Cuenta, línea de crédito, retirar() con sobregiro, calcular_interes() diferente 15
5 class Cliente Atributo privado __dni, lista de cuentas, métodos agregar_cuenta y obtener_resumen 15
6 class Banco Lista de clientes, registrar_cliente, reporte_clientes iterando con polimorfismo 10
7 Programa main Instanciación correcta de objetos, demostración de los 4 pilares, salida en consola correcta 15
8 Calidad del código Nombres descriptivos, convenciones PEP8, lógica sin errores de ejecución 15
TOTAL 100
90–100
Excelente
80–89
Muy Bueno
70–79
Bueno
60–69
Suficiente
< 60
Insuficiente

📋 Contexto del Problema

El BancoUTH necesita digitalizar su sistema de cuentas. Ofrece dos tipos que comparten comportamiento base pero difieren en reglas de negocio:

💰 Cuenta de Ahorro
  • Genera intereses mensuales (tasa 3 %)
  • Máximo 5 retiros por mes
  • No permite saldo negativo
  • Interés = saldo × tasa
💳 Cuenta Corriente
  • Línea de crédito de $5 000
  • Cargo de mantenimiento $50/mes
  • Puede tener saldo negativo (hasta el límite)
  • "Interés" = cargo fijo mensual
📌 Regla clave: Ambas cuentas comparten depositar y consultar saldo, pero difieren en cómo retiran y calculan intereses. El saldo nunca debe modificarse directamente — siempre mediante métodos. Esto es encapsulamiento.
1

📦 Diagrama de Clases UML

Estructura completa del sistema — léelo antes de escribir código

«abstract» Cuenta + numero_cuenta : str - __saldo : float + titular : str # _historial : list + depositar(monto) # _modificar_saldo(cant) + get_saldo() : float + retirar(monto) {abstract} + calcular_interes() {abstract} hereda hereda «extends Cuenta» CuentaAhorro + tasa_interes : float + max_retiros : int = 5 + retiros_mes : int + calcular_interes() + retirar(monto) + resetear_retiros() + mostrar_estado() ↑ implementa los 2 abstractos «extends Cuenta» CuentaCorriente + limite_credito : float + cargo_mensual : float + calcular_interes() + retirar(monto) + aplicar_cargo() + mostrar_estado() ↑ implementa los 2 abstractos Cliente + nombre : str - __dni : str + cuentas : list + agregar_cuenta() + mostrar() Transaccion + tipo : str + monto : float + fecha : str + mostrar() Banco + nombre : str + clientes : list + registrar_cliente() + reporte() tiene * posee * crea
– privado
Solo desde dentro de la clase. __saldo, __dni. Python usa name mangling.
# protegido
Clase y subclases. _historial, _modificar_saldo().
+ público
Accesible desde cualquier parte del programa.
△ Herencia
Triángulo hueco → "es un tipo de". Apunta al padre.
- - → Asociación
Línea punteada → "tiene/usa". Banco tiene Clientes.
«abstract»
No puede instanciarse. Obliga a implementar los métodos marcados.
2

👤 Diagrama de Casos de Uso

¿Quién hace qué? — La visión del usuario del sistema

«sistema» BancoUTH Cliente Cajero Abrir Cuenta (nueva cuenta bancaria) Depositar Dinero (suma al saldo) Retirar Dinero (resta del saldo) Consultar Saldo (get_saldo) Calcular Interés (polimorfismo) Generar Reporte (todos los clientes) «include»
📖 Cómo leer: Las figuras humanas son actores (quién usa el sistema). Las elipses son casos de uso (qué puede hacer). Las líneas muestran qué actor realiza qué función. «include» = un caso de uso invoca automáticamente a otro (Retirar siempre verifica disponibilidad).
3

🔄 Diagrama de Secuencia — "Realizar un Retiro"

Cómo se comunican los objetos en tiempo de ejecución — se lee de arriba a abajo

:Cliente :Cajero :Sistema :Cuenta :Transaccion 1: solicitar_retiro(monto=2000) 2: procesar_retiro("AH-001", 2000) 3: retirar(2000) 4: _verificar() 5: new("retiro", 2000) objeto Transaccion creado 6: True (retiro exitoso) 7: operacion_exitosa 8: mostrar_comprobante() tiempo →
📖 Cómo leer: El tiempo avanza hacia abajo. Las cajas sobre cada línea de vida = objeto activo. Flechas sólidas = llamadas a método. Flechas punteadas verdes = retornos. La auto-flecha ④ = el objeto llama a un método interno propio.

🐍 Construyendo el Código — Paso a Paso, Línea por Línea

🗺️ Orden de construcción (sigue exactamente este orden):
TransaccionCuenta (ABC)CuentaAhorroCuentaCorrienteClienteBancomain
De adentro hacia afuera: primero las clases sin dependencias, luego las que las usan.
0
🔧 Importaciones — Herramientas necesarias

Antes de cualquier clase, importamos los módulos. abc nos da la abstracción; datetime registrará la hora exacta de cada transacción.

banco_uth.py
# ═══════════════════════════════════════════════════════
#  SISTEMA BANCARIO — BancoUTH
#  Ejercicio Integrador Unidad 1 — POO en Python
# ═══════════════════════════════════════════════════════

from abc import ABC, abstractmethod  # ABC = Abstract Base Class
from datetime import datetime          # para la fecha/hora automática
ABC — molde que define métodos obligatorios. No puede instanciarse directamente. Es la base de la abstracción.
@abstractmethod — decorador que obliga a las subclases a implementar ese método. Si no lo hacen, Python lanza TypeError al crear el objeto.
1
💾 Clase Transaccion — Sin herencia, la más sencilla

Del diagrama de clases (caja inferior-centro). La construimos primero porque Cuenta la necesitará para registrar movimientos. Proviene directamente del diagrama de secuencia paso ⑤.

banco_uth.py — clase Transaccion
class Transaccion:                              # sin herencia → clase base simple
    """Registra un movimiento bancario."""       # docstring: buena práctica

    def __init__(self, tipo: str, monto: float):  # Constructor: inicializa el objeto con los datos recibidos
        self.tipo  = tipo                          # "deposito" o "retiro"
        self.monto = monto                         # cantidad de dinero
        self.fecha = datetime.now().strftime(     # fecha/hora automática al crear
            "%d/%m/%Y %H:%M"                   # formato: 15/05/2026 14:30
        )  # Cierra la estructura abierta en las líneas anteriores

    def mostrar(self):                             # método público (+)
        print(f"  [{self.fecha}]  {self.tipo.upper():10}  ${self.monto:,.2f}")  # Muestra en consola el resultado
        #         └─ fecha ─┘   └─ tipo alineado ─┘   └─ monto con $ ─┘
🔎 Del diagrama al código: La caja Transaccion del diagrama tiene atributos tipo, monto, fecha y método mostrar(). Cada línea del __init__ mapea 1 a 1 con el diagrama. fecha no aparece en el diagrama — es un detalle de implementación que añadimos al codificar (el diagrama es un modelo, no una especificación exacta).
2
🔷 Clase Abstracta Cuenta — Abstracción + Encapsulamiento

Caja superior del diagrama (con «abstract»). Define el contrato que toda cuenta debe cumplir. El __saldo privado es el encapsulamiento. Los @abstractmethod son la abstracción.

⚠️ Punto clave del examen: Cuenta(ABC) NO puede instanciarse. Escribir c = Cuenta("001","Ana") lanza TypeError: Can't instantiate abstract class Cuenta. Solo las subclases concretas pueden crear objetos.
banco_uth.py — clase Cuenta (abstracta)
class Cuenta(ABC):                               # hereda ABC → se vuelve abstracta
    """Clase base abstracta para todo tipo de cuenta bancaria."""  # Continúa la lógica del ejemplo paso a paso

    def __init__(self, numero: str, titular: str, saldo_inicial: float = 0.0):  # Constructor: inicializa el objeto con los datos recibidos
        self.numero_cuenta = numero       # (+) público: cualquiera puede leerlo
        self.titular       = titular       # (+) público
        self.__saldo       = saldo_inicial # (–) PRIVADO: name mangling → _Cuenta__saldo
        self._historial    = []            # (#) PROTEGIDO: clase y subclases lo usan

        # ── Encapsulamiento: getter ─────────────────────────────────────
    def get_saldo(self) -> float:            # única puerta pública al saldo
        return self.__saldo               # accede al privado desde adentro ✓

        # ── Método protegido para subclases ────────────────────────────
    def _modificar_saldo(self, cantidad: float):  # Método que modificar saldo
        self.__saldo += cantidad           # las subclases jamás tocan __saldo directo

        # ── Método concreto: depositar (igual para todos) ───────────────
    def depositar(self, monto: float):  # Método que depositar
        if monto <= 0:                       # validación: monto debe ser positivo
            print("❌ El monto debe ser positivo")  # Muestra en consola el resultado
            return  # Termina el método y regresa el control al llamador
        self.__saldo += monto               # modifica el privado desde adentro ✓
        t = Transaccion("deposito", monto) # ← paso ⑤ del diagrama de secuencia
        self._historial.append(t)           # usa atributo protegido ✓
        print(f"✅ Depósito ${monto:,.2f}. Saldo: ${self.__saldo:,.2f}")  # Muestra en consola el resultado

        # ── Métodos ABSTRACTOS: las subclases DEBEN implementarlos ──────
    @abstractmethod  # Decorador: obliga a las subclases a implementar este método
    def retirar(self, monto: float):      # sin cuerpo → cada subclase tiene sus reglas
        pass  # Cuerpo vacío por ahora; aquí irá la lógica en las subclases

    @abstractmethod  # Decorador: obliga a las subclases a implementar este método
    def calcular_interes(self) -> float:  # ← aquí vive el POLIMORFISMO
        pass                                # cada tipo calcula distinto

        # ── Método concreto: ver historial ─────────────────────────────
    def ver_historial(self):  # Método que ver historial
        print(f"\n📋 Historial [{self.numero_cuenta}] — {self.titular}:")  # Muestra en consola el resultado
        for t in self._historial:          # accede al protegido ✓
            t.mostrar()  # Completa la llamada o expresión iniciada anteriormente
🔒 __saldo (privado)
Python lo renombra internamente a _Cuenta__saldo. Desde fuera de la clase, cuenta.__saldo lanza AttributeError. Solo get_saldo() lo expone.
# _historial (protegido)
Un solo _ es una convención. Indica "solo para uso interno y subclases". Python no lo bloquea técnicamente, pero es una señal para el programador.
_modificar_saldo()
Método protegido que permite a las subclases ajustar el saldo sin romper el encapsulamiento. Es la solución limpia al problema de name mangling en herencia.
3
💰 Clase CuentaAhorro — Herencia + Polimorfismo

Caja inferior-izquierda del diagrama. La flecha de herencia dice "CuentaAhorro ES UNA Cuenta". Debe implementar obligatoriamente los 2 métodos abstractos heredados.

banco_uth.py — clase CuentaAhorro
class CuentaAhorro(Cuenta):                    # ← la flecha de herencia del diagrama
    """Cuenta con interés mensual y límite de retiros."""  # Continúa la lógica del ejemplo paso a paso

    def __init__(self, numero: str, titular: str,  # Constructor: inicializa el objeto con los datos recibidos
                 saldo_inicial: float = 0.0,  # Continúa la lógica del ejemplo paso a paso
                 tasa_interes: float = 0.03):    # 3% por defecto

        super().__init__(numero, titular, saldo_inicial)  # Llama al constructor de la clase padre para inicializar los atributos heredados
                # ↑ llama al __init__ de Cuenta
                #   inicializa: numero_cuenta, titular, __saldo, _historial
                #   sin super(), el objeto no tendría esos atributos base

        self.tasa_interes = tasa_interes       # atributo propio (del diagrama)
        self.max_retiros  = 5                  # regla de negocio del banco
        self.retiros_mes  = 0                  # contador: inicia en 0

        # ── POLIMORFISMO: implementación específica de calcular_interes ─
    def calcular_interes(self) -> float:  # Método que calcular interes
        interes = self.get_saldo() * self.tasa_interes  # Asigna un valor inicial o calculado a interes
                # Ahorro genera GANANCIAS: $10,000 × 0.03 = $300
        print(f"💹 Interés: ${interes:,.2f} (tasa {self.tasa_interes*100:.1f}%/mes)")  # Muestra en consola el resultado
        return interes                         # positivo = ganancia

        # ── OVERRIDE: retirar con reglas propias de ahorro ─────────────
    def retirar(self, monto: float):  # Método que retirar
        if self.retiros_mes >= self.max_retiros: # regla 1: límite de retiros
            print(f"❌ Límite de {self.max_retiros} retiros/mes alcanzado")  # Muestra en consola el resultado
            return  # Termina el método y regresa el control al llamador
        if monto > self.get_saldo():           # regla 2: sin saldo negativo
            print(f"❌ Saldo insuficiente. Tienes ${self.get_saldo():,.2f}")  # Muestra en consola el resultado
            return  # Termina el método y regresa el control al llamador
        self._modificar_saldo(-monto)          # método protegido de Cuenta ✓
        self.retiros_mes += 1                  # incrementa el contador del mes
        self._historial.append(Transaccion("retiro", monto))  # Agrega un elemento al final de self._historial
        print(f"✅ Retiro ${monto:,.2f}. Retiros: {self.retiros_mes}/{self.max_retiros}")  # Muestra en consola el resultado

    def resetear_retiros(self):               # método exclusivo de CuentaAhorro
        self.retiros_mes = 0  # Guarda retiros_mes como atributo de instancia
        print("🔄 Retiros del mes reiniciados (nuevo mes)")  # Muestra en consola el resultado

    def mostrar_estado(self):  # Método que mostrar estado
        print(f"  💰 AHORRO #{self.numero_cuenta} | {self.titular}")  # Muestra en consola el resultado
        print(f"     Saldo: ${self.get_saldo():,.2f} | Tasa: {self.tasa_interes*100:.1f}%")  # Muestra en consola el resultado
        print(f"     Retiros: {self.retiros_mes}/{self.max_retiros} este mes")  # Muestra en consola el resultado
🎭 Polimorfismo en acción: calcular_interes() en CuentaAhorro retorna saldo × tasa (positivo = ganancia). El mismo método en CuentaCorriente retornará -cargo_mensual (negativo = costo). Mismo nombre de método, comportamiento completamente diferente según el tipo real del objeto.
4
💳 Clase CuentaCorriente — Mismo contrato, diferente lógica

Caja inferior-derecha del diagrama. Segunda hija de Cuenta. El polimorfismo es evidente: calcular_interes() hace algo totalmente diferente que en CuentaAhorro.

banco_uth.py — clase CuentaCorriente
class CuentaCorriente(Cuenta):                 # segunda hija de Cuenta
    """Cuenta con línea de crédito y cargo mensual."""  # Continúa la lógica del ejemplo paso a paso

    def __init__(self, numero: str, titular: str,  # Constructor: inicializa el objeto con los datos recibidos
                 saldo_inicial: float = 0.0,  # Continúa la lógica del ejemplo paso a paso
                 limite_credito: float = 5000.0):  # Completa la firma o la condición iniciada en la línea anterior

        super().__init__(numero, titular, saldo_inicial) # reutiliza Cuenta.__init__
        self.limite_credito = limite_credito           # puede retirar hasta –$5,000
        self.cargo_mensual  = 50.0                    # costo de mantenimiento

        # ── POLIMORFISMO: mismo nombre, distinto resultado ──────────────
    def calcular_interes(self) -> float:  # Método que calcular interes
                # Corriente NO genera ganancia → tiene un COSTO mensual
        print(f"💳 Cargo mensual aplicable: ${self.cargo_mensual:,.2f}")  # Muestra en consola el resultado
        return -self.cargo_mensual               # negativo = costo
                # ← MISMO NOMBRE que CuentaAhorro, comportamiento DIFERENTE

        # ── OVERRIDE: retirar con línea de crédito ─────────────────────
    def retirar(self, monto: float):  # Método que retirar
        disponible = self.get_saldo() + self.limite_credito  # Asigna un valor inicial o calculado a disponible
                   # ↑ puede usar: saldo actual + línea de crédito
        if monto > disponible:  # Verifica monto > disponible antes de continuar
            print(f"❌ Excede límite disponible de ${disponible:,.2f}")  # Muestra en consola el resultado
            return  # Termina el método y regresa el control al llamador
        self._modificar_saldo(-monto)            # usa método protegido ✓
        self._historial.append(Transaccion("retiro", monto))  # Agrega un elemento al final de self._historial
        print(f"✅ Retiro ${monto:,.2f}. Saldo: ${self.get_saldo():,.2f}")  # Muestra en consola el resultado

    def aplicar_cargo(self):                  # exclusivo de CuentaCorriente
        self._modificar_saldo(-self.cargo_mensual)  # Completa la llamada o expresión iniciada anteriormente
        self._historial.append(Transaccion("cargo_mant", self.cargo_mensual))  # Agrega un elemento al final de self._historial
        print(f"💳 Cargo de mantenimiento: -${self.cargo_mensual}")  # Muestra en consola el resultado

    def mostrar_estado(self):  # Método que mostrar estado
        print(f"  💳 CORRIENTE #{self.numero_cuenta} | {self.titular}")  # Muestra en consola el resultado
        print(f"     Saldo: ${self.get_saldo():,.2f} | Crédito: ${self.limite_credito:,.2f}")  # Muestra en consola el resultado
        print(f"     Cargo mensual: ${self.cargo_mensual:,.2f}")  # Muestra en consola el resultado
5
👤 Clase Cliente — Encapsulamiento de datos personales

Caja inferior-izquierda. El DNI es privado. La lista cuentas representa la asociación "Cliente posee * Cuentas" del diagrama de clases. Un cliente puede tener varios tipos de cuenta.

banco_uth.py — clase Cliente
class Cliente:                                  # sin herencia
    """Representa a un cliente del banco."""  # Continúa la lógica del ejemplo paso a paso

    def __init__(self, nombre: str, dni: str):  # Constructor: inicializa el objeto con los datos recibidos
        self.nombre  = nombre                      # (+) público
        self.__dni   = dni                         # (–) privado: dato sensible
        self.cuentas = []                          # lista de objetos Cuenta

    def get_dni(self) -> str:                   # getter del privado
        return self.__dni  # Retorna el valor calculado al que llamó este método

    def agregar_cuenta(self, cuenta: Cuenta):  # acepta cualquier tipo de Cuenta
        self.cuentas.append(cuenta)              # polimorfismo: Ahorro o Corriente
        print(f"✅ Cuenta {cuenta.numero_cuenta} asignada a {self.nombre}")  # Muestra en consola el resultado

    def mostrar_info(self):  # Método que mostrar info
        print(f"\n{'═'*46}")  # Muestra en consola el resultado
        print(f"  👤 CLIENTE  : {self.nombre}")  # Muestra en consola el resultado
        print(f"  🪪 DNI      : ***{self.__dni[-4:]}")    # oculta primeros dígitos
        print(f"  🏦 Cuentas  : {len(self.cuentas)}")  # Muestra en consola el resultado
        for cuenta in self.cuentas:  # Itera sobre cada cuenta en self.cuentas
            cuenta.mostrar_estado()               # polimorfismo: cada cuenta sabe mostrarse
6
🏦 Clase Banco — El orquestador del sistema

Caja inferior-derecha. Gestiona la colección de clientes (asociación "tiene *" del diagrama). Es el objeto de mayor nivel jerárquico del sistema.

banco_uth.py — clase Banco
class Banco:  # Define la clase Banco
    """Gestiona clientes y sus cuentas bancarias."""  # 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 de instancia
        self.clientes = []                         # lista de objetos Cliente

    def registrar_cliente(self, cliente: Cliente):  # Método que registrar cliente
        self.clientes.append(cliente)  # Agrega un elemento al final de self.clientes
        print(f"✅ '{cliente.nombre}' registrado en {self.nombre}")  # Muestra en consola el resultado

    def buscar_cliente(self, nombre: str):  # Método que buscar cliente
        for c in self.clientes:  # Itera sobre cada c en self.clientes
            if c.nombre.lower() == nombre.lower():  # Verifica c.nombre.lower() == nombre.lower() antes de continuar
                return c  # Retorna el valor calculado al que llamó este método
        return None  # Retorna el valor calculado al que llamó este método

    def reporte_clientes(self):  # Método que reporte clientes
        total = sum(                             # suma saldos de todas las cuentas
            c.get_saldo()                       # get_saldo() = encapsulamiento
            for cli in self.clientes  # Itera sobre cada elemento de la colección indicada
            for c in cli.cuentas  # Itera sobre cada elemento de la colección indicada
        )  # Cierra la estructura abierta en las líneas anteriores
        print(f"\n{'═'*46}")  # Muestra en consola el resultado
        print(f"  🏦 {self.nombre}")  # Muestra en consola el resultado
        print(f"  Total clientes  : {len(self.clientes)}")  # Muestra en consola el resultado
        print(f"  Saldo gestionado: ${total:,.2f}")  # Muestra en consola el resultado
        print(f"{'═'*46}")  # Muestra en consola el resultado
7
▶️ Programa Principal — Los 4 pilares en acción

El bloque if __name__ == "__main__": es el punto de entrada. Aquí demostramos los 4 pilares de POO completos, exactamente como fluye el diagrama de secuencia.

banco_uth.py — programa principal
# ══════════════════════ PROGRAMA PRINCIPAL ══════════════════════
if __name__ == "__main__":  # Se ejecuta solo cuando este archivo se corre directamente (no cuando se importa)

        # 1. Crear el banco (objeto orquestador)
    banco = Banco("BancoUTH")  # Asigna un valor inicial o calculado a banco

        # 2. Crear clientes (encapsulamiento: __dni privado)
    ana  = Cliente("Ana García",   "12345678")  # Asigna un valor inicial o calculado a ana
    luis = Cliente("Luis Martínez", "87654321")  # Asigna un valor inicial o calculado a luis

        # 3. Crear cuentas — instancias de SUBCLASES (nunca de Cuenta directa)
    ahorro1    = CuentaAhorro("AH-001", "Ana García",   saldo_inicial=10000.0)  # Asigna un valor inicial o calculado a ahorro1
    corriente1 = CuentaCorriente("CC-001", "Ana García", saldo_inicial=2000.0)  # Asigna un valor inicial o calculado a corriente1
    ahorro2    = CuentaAhorro("AH-002", "Luis Martínez",  # Asigna un valor inicial o calculado a ahorro2
                               saldo_inicial=5000.0, tasa_interes=0.05)  # Asigna un valor inicial o calculado a saldo_inicial

        # 4. Asignar cuentas (asociación del diagrama)
    ana.agregar_cuenta(ahorro1)  # Completa la llamada o expresión iniciada anteriormente
    ana.agregar_cuenta(corriente1)  # Completa la llamada o expresión iniciada anteriormente
    luis.agregar_cuenta(ahorro2)  # Completa la llamada o expresión iniciada anteriormente

        # 5. Registrar en el banco
    banco.registrar_cliente(ana)  # Completa la llamada o expresión iniciada anteriormente
    banco.registrar_cliente(luis)  # Completa la llamada o expresión iniciada anteriormente

        # 6. Operaciones bancarias (diagrama de secuencia)
    print("\n💼 ── OPERACIONES ──────────────────────")  # Muestra en consola el resultado
    ahorro1.depositar(5000)              # → saldo ahora $15,000
    ahorro1.retirar(2000)               # → saldo $13,000, retiros=1/5
    corriente1.depositar(1000)          # → saldo $3,000
    corriente1.retirar(6000)            # usa línea de crédito → saldo −$3,000
    corriente1.aplicar_cargo()           # cargo mensual $50 → saldo −$3,050

        # 7. POLIMORFISMO EN ACCIÓN ─────────────────────────────────────
    print("\n🎭 ── POLIMORFISMO: mismo método, diferente resultado ──")  # Muestra en consola el resultado
    cuentas = [ahorro1, corriente1, ahorro2]  # lista de tipo Cuenta
    for cuenta in cuentas:  # Itera sobre cada cuenta en cuentas
        print(f"\n  [{type(cuenta).__name__}] {cuenta.numero_cuenta}:")  # Muestra en consola el resultado
        cuenta.calcular_interes()         # mismo mensaje → diferente respuesta

        # 8. Ver historial y estado
    ahorro1.ver_historial()  # Completa la llamada o expresión iniciada anteriormente
    corriente1.ver_historial()  # Completa la llamada o expresión iniciada anteriormente
    print()  # Muestra en consola el resultado
    ana.mostrar_info()  # Completa la llamada o expresión iniciada anteriormente
    luis.mostrar_info()  # Completa la llamada o expresión iniciada anteriormente

        # 9. Reporte del banco
    banco.reporte_clientes()  # Completa la llamada o expresión iniciada anteriormente

📟 Salida Esperada en Consola

# — salida en terminal —
Cuenta AH-001 asignada a Ana García
Cuenta CC-001 asignada a Ana García
Cuenta AH-002 asignada a Luis Martínez
'Ana García' registrado en BancoUTH
'Luis Martínez' registrado en BancoUTH
💼 ── OPERACIONES ──────────────────────
Depósito $5,000.00. Saldo: $15,000.00
Retiro $2,000.00. Retiros: 1/5
Depósito $1,000.00. Saldo: $3,000.00
Retiro $6,000.00. Saldo: $-3,000.00
💳 Cargo de mantenimiento: -$50.0
🎭 ── POLIMORFISMO: mismo método, diferente resultado ──
[CuentaAhorro] AH-001:
💹 Interés: $390.00 (tasa 3.0%/mes)
[CuentaCorriente] CC-001:
💳 Cargo mensual aplicable: $50.00
[CuentaAhorro] AH-002:
💹 Interés: $150.00 (tasa 5.0%/mes)
📋 Historial [AH-001] — Ana García:
[15/05/2026 14:30] DEPOSITO $5,000.00
[15/05/2026 14:30] RETIRO $2,000.00
📋 Historial [CC-001] — Ana García:
[15/05/2026 14:30] DEPOSITO $1,000.00
[15/05/2026 14:30] RETIRO $6,000.00
[15/05/2026 14:30] CARGO_MANT $50.00

✅ Checklist del Examen — Verifica que tu código lo tenga todo

🔷 Abstracción
  • class Cuenta(ABC):
  • Al menos 2 métodos con @abstractmethod
  • No puedes crear Cuenta() directo ✓
🧬 Herencia
  • CuentaAhorro(Cuenta) y CuentaCorriente(Cuenta)
  • super().__init__(...) en ambas ✓
  • Subclases con atributos y métodos propios ✓
🎭 Polimorfismo
  • calcular_interes() en ambas subclases ✓
  • Resultado diferente según el tipo ✓
  • Bucle for cuenta in cuentas: que llama al mismo método ✓
🔒 Encapsulamiento
  • __saldo privado en Cuenta
  • get_saldo() como único acceso externo ✓
  • _modificar_saldo() protegido para subclases ✓
📊 Diagramas UML
  • Diagrama de clases con visibilidades (+/–/#) ✓
  • Diagrama de casos de uso con actores ✓
  • Diagrama de secuencia con mensajes numerados ✓
🐍 Python correcto
  • from abc import ABC, abstractmethod
  • super().__init__() en cada subclase ✓
  • if __name__ == "__main__": al final ✓
💡 Tip final: En el examen, lee el diagrama de clases de arriba a abajo para el orden de construcción. Lee el diagrama de secuencia para entender cómo conectar los objetos. Lee el diagrama de casos de uso para saber qué métodos necesitas implementar.
EVALUACIÓN ALTERNATIVA 100 PUNTOS TOTALES 6 EJERCICIOS

🧩 Evaluación por Módulos — 6 Ejercicios Independientes

Cada ejercicio es independiente, cubre un concepto específico y tiene sus propios diagramas. Se pueden aplicar en sesiones distintas o como un examen por bloques. Los diagramas ya están dados — el alumno solo debe escribir el código.

15
Ej 1 · Biblioteca
15
Ej 2 · Veterinaria
20
Ej 3 · Nómina
15
Ej 4 · Tienda
20
Ej 5 · Universidad
15
Ej 6 · Figuras
EJERCICIO 1 · HERENCIA BÁSICA

📚 Biblioteca Digital

15 pts
📋 Descripción: Una biblioteca maneja dos tipos de libros: físicos y digitales. Ambos comparten título, autor e ISBN, pero difieren en cómo se prestan. El libro físico tiene número de copias disponibles; el digital tiene un límite de descargas simultáneas. Implementa la herencia para reutilizar el código común.
📐 Diagrama de Clases
Libro - titulo: str - autor: str - isbn: str + prestar(): str LibroFisico - copias: int + prestar(): str + devolver() LibroDigital - max_descargas: int + prestar(): str + get_formato(): str
👤 Casos de Uso
Usuario Sistema Biblioteca Buscar Libro Prestar Libro Devolver Libro Ver Disponibilidad
🔄 Diagrama de Secuencia
Usuario :Usuario Biblioteca :Biblioteca Libro :Libro buscar(isbn) get_info() titulo, autor prestar(isbn) prestar() confirmación "Préstamo OK"

✏️ Lo que debes implementar:

  • Clase Libro con atributos privados y constructor
  • Clases LibroFisico y LibroDigital que hereden de Libro
  • Método prestar() con comportamiento distinto en cada subclase
  • LibroFisico.devolver() que recupere una copia
  • main que cree un libro de cada tipo y llame prestar()
💻 CÓDIGO Guía línea por línea
1 Clase base Libro — encapsulamiento de atributos
biblioteca.py — clase Libro
class Libro:                              # Clase padre — código compartido por AMBOS tipos
    def __init__(self, titulo, autor, isbn):  # Constructor: inicializa el objeto con los datos recibidos
        self.__titulo = titulo            # __ = PRIVADO: imposible hacer libro.__titulo
        self.__autor  = autor             # Encapsulamiento: datos protegidos de cambios
        self.__isbn   = isbn              # ISBN = identificador único e inmutable

    def get_titulo(self):               # Getter: única vía controlada para leer __titulo
        return self.__titulo  # Retorna el valor calculado al que llamó este método

    def get_isbn(self):  # Método que get isbn
        return self.__isbn  # Retorna el valor calculado al que llamó este método

    def __str__(self):                   # Python llama esto cuando haces print(libro)
        return f"[{self.__isbn}] {self.__titulo} - {self.__autor}"  # Retorna el valor calculado al que llamó este método
¿Por qué así? El diagrama muestra - titulo, - autor, - isbn con el símbolo (privado). En Python eso se escribe self.__atributo. Python aplica name mangling: internamente lo guarda como _Libro__titulo, así ningún código externo puede modificarlo sin pasar por el getter. Eso es encapsulamiento.
2 LibroFisico y LibroDigital — herencia + polimorfismo
biblioteca.py — subclases
class LibroFisico(Libro):                # (Libro) = hereda de Libro — flecha del diagrama
    def __init__(self, titulo, autor, isbn, copias):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(titulo, autor, isbn)  # Ejecuta Libro.__init__ — no repite código
        self.__copias = copias            # Atributo PROPIO de LibroFisico

    def prestar(self):                    # Sobreescribe prestar() → POLIMORFISMO
        if self.__copias > 0:             # Regla de negocio: solo presta con stock
            self.__copias -= 1            # Descuenta del inventario físico
            return f"Préstamo OK: {self.get_titulo()} (quedan {self.__copias})"  # Retorna el valor calculado al que llamó este método
        return "Sin copias disponibles"  # Retorna el valor calculado al que llamó este método

    def devolver(self):                   # Método exclusivo — LibroDigital NO lo tiene
        self.__copias += 1  # Continúa la lógica del ejemplo paso a paso
        return f"Devolución OK. Copias: {self.__copias}"  # Retorna el valor calculado al que llamó este método


class LibroDigital(Libro):              # También hereda de Libro
    def __init__(self, titulo, autor, isbn, max_descargas):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(titulo, autor, isbn)  # Llama al constructor de la clase padre para inicializar los atributos heredados
        self.__max_descargas     = max_descargas  # Tope configurado al instanciar
        self.__descargas_activas = 0            # Empieza en 0; sube al prestar

    def prestar(self):                    # Mismo nombre, lógica diferente → polimorfismo
        if self.__descargas_activas < self.__max_descargas:  # Verifica self.__descargas_activas < self.__max_descargas antes de continuar
            self.__descargas_activas += 1  # Continúa la lógica del ejemplo paso a paso
            return f"Descarga OK: {self.get_titulo()} ({self.__descargas_activas}/{self.__max_descargas})"  # Retorna el valor calculado al que llamó este método
        return "Límite de descargas alcanzado"  # Retorna el valor calculado al que llamó este método

    def get_formato(self):  # Método que get formato
        return "PDF/EPUB"  # Retorna el valor calculado al que llamó este método
¿Por qué así? super().__init__() garantiza que el constructor de Libro se ejecute primero — inicializa __titulo, __autor e __isbn. Ambas subclases declaran prestar() con la misma firma pero diferente cuerpo — eso es polimorfismo: el mismo mensaje produce respuestas distintas según el tipo real del objeto.
3 Programa principal — herencia y polimorfismo en acción
biblioteca.py — main
if __name__ == "__main__":               # Solo corre si ejecutas ESTE archivo directo
    lf = LibroFisico("POO con Python", "J. García", "978-001", 3)  # 3 copias físicas
    ld = LibroDigital("Clean Code",    "R. Martin",  "978-002", 2)  # máx 2 descargas

    # Misma llamada .prestar() — Python decide qué versión ejecutar en tiempo real
    print(lf.prestar())     # → LibroFisico.prestar(): descuenta copia, quedan 2
    print(lf.prestar())     # → quedan 1
    print(lf.devolver())    # → devuelve, quedan 2
    print(ld.prestar())     # → LibroDigital.prestar(): activa descarga 1/2
    print(ld.prestar())     # → 2/2 activas
    print(ld.prestar())     # → rechazado: límite alcanzado
¿Por qué así? if __name__ == "__main__": es un guardián: si este archivo se importa desde otro módulo, este bloque no se ejecuta. Fíjate que lf.prestar() y ld.prestar() se escriben exactamente igual. Python decide en tiempo de ejecución cuál versión corre — eso se llama enlace dinámico (dynamic dispatch) y es la esencia del polimorfismo.

🖥️ Salida esperada en consola:

Préstamo OK: POO con Python (quedan 2)
Préstamo OK: POO con Python (quedan 1)
Devolución OK. Copias: 2
Descarga OK: Clean Code (1/2)
Descarga OK: Clean Code (2/2)
Límite de descargas alcanzado
EJERCICIO 2 · POLIMORFISMO

🐾 Clínica Veterinaria

15 pts
📋 Descripción: Una clínica veterinaria atiende distintas especies. Cada animal tiene nombre, edad y dueño, pero cada especie emite un sonido diferente y requiere una dieta distinta. Usa una clase abstracta Animal con métodos abstractos hacer_sonido() y tipo_dieta() para implementar polimorfismo.
📐 Diagrama de Clases
«abstract» Animal - nombre: str - edad: int - dueño: str + hacer_sonido()* + tipo_dieta()* Perro - raza: str + hacer_sonido() + tipo_dieta() Gato - indoor: bool + hacer_sonido() + tipo_dieta()
👤 Casos de Uso
Veterinario Sistema Veterinaria Registrar Animal Consultar Dieta Emitir Sonido Ver Ficha Médica
🔄 Diagrama de Secuencia
:Veterinario :Clinica :Animal registrar(animal) hacer_sonido() "Guau/Miau" consultar_dieta() tipo_dieta() "Carnívoro" resultado dieta

✏️ Lo que debes implementar:

  • Clase abstracta Animal con métodos abstractos hacer_sonido() y tipo_dieta()
  • Clases Perro y Gato que concreten ambos métodos
  • main con una lista mixta de animales y un ciclo que llame hacer_sonido() en cada uno (polimorfismo)
💻 CÓDIGO Guía línea por línea
1 Clase abstracta Animal — abstracción
veterinaria.py — Animal (ABC)
from abc import ABC, abstractmethod      # ABC = Abstract Base Class del módulo abc

class Animal(ABC):                         # (ABC) → clase ABSTRACTA: no se puede instanciar
    def __init__(self, nombre, edad):  # Constructor: inicializa el objeto con los datos recibidos
        self.__nombre = nombre              # Privado → encapsulamiento
        self.__edad   = edad  # Guarda __edad como atributo de instancia

    def get_nombre(self): return self.__nombre  # Getter necesario para leer __nombre
    def get_edad(self):   return self.__edad  # Método que get edad

    @abstractmethod                          # @abstractmethod → OBLIGA a las subclases a definirlo
    def hacer_sonido(self): pass            # pass = cuerpo vacío; la subclase pone el cuerpo real

    @abstractmethod
    def tipo_dieta(self): pass              # Idem: Perro = "Carnívoro", Gato = "Omnívoro"

    def ficha(self):                         # Método concreto — se HEREDA sin cambios
        return f"🐾 {self.__nombre} ({self.__edad} años) | {self.tipo_dieta()}"  # Retorna el valor calculado al que llamó este método
¿Por qué así? @abstractmethod convierte hacer_sonido en un contrato: si creas una subclase que no define ese método, Python lanza TypeError al intentar instanciarla. Eso es abstracción: la clase base define qué debe hacerse, pero no cómo. También note que ficha() llama self.tipo_dieta() — aunque Animal no sabe cómo calcularlo, sabe que alguien lo implementará.
2 Perro y Gato — concreción de los métodos abstractos
veterinaria.py — subclases concretas
class Perro(Animal):                      # Hereda de Animal; DEBE implementar ambos @abstractmethod
    def __init__(self, nombre, edad, raza):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(nombre, edad)        # Delega nombre y edad al padre
        self.__raza = raza                  # Atributo exclusivo de Perro

    def hacer_sonido(self):               # Implementa el contrato → ya no es abstracto
        return "¡Guau! 🐶"  # Retorna el valor calculado al que llamó este método

    def tipo_dieta(self):  # Método que tipo dieta
        return "Carnívoro"  # Retorna el valor calculado al que llamó este método

    def get_raza(self): return self.__raza  # Método que get raza


class Gato(Animal):                       # Otra subclase concreta — misma jerarquía
    def __init__(self, nombre, edad, indoor):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(nombre, edad)  # Llama al constructor de la clase padre para inicializar los atributos heredados
        self.__indoor = indoor              # True = gato de interior

    def hacer_sonido(self):               # Misma firma, diferente cuerpo → polimorfismo
        return "¡Miau! 🐱"  # Retorna el valor calculado al que llamó este método

    def tipo_dieta(self):  # Método que tipo dieta
        return "Omnívoro"  # Retorna el valor calculado al que llamó este método

    def es_indoor(self):  # Método que es indoor
        return "Interior" if self.__indoor else "Exterior"  # Retorna el valor calculado al que llamó este método
¿Por qué así? Si quitaras hacer_sonido de Perro, Python lanzaría: TypeError: Can't instantiate abstract class Perro with abstract method hacer_sonido. El decorador @abstractmethod es el guardián. Note que tanto Perro como Gato implementan hacer_sonido con diferente respuesta — esto es polimorfismo respaldado por herencia.
3 Programa principal — lista polimórfica
veterinaria.py — main
if __name__ == "__main__":  # Se ejecuta solo cuando este archivo se corre directamente (no cuando se importa)
    animales = [                            # Lista con TIPOS DIFERENTES → lista polimórfica
        Perro("Rex",  4, "Labrador"),  # Continúa la lógica del ejemplo paso a paso
        Gato("Luna", 2, True),  # Continúa la lógica del ejemplo paso a paso
        Perro("Max",  6, "Poodle"),  # Continúa la lógica del ejemplo paso a paso
    ]  # Cierra la estructura abierta en las líneas anteriores

    for a in animales:                       # Un solo ciclo atiende Perros Y Gatos
        print(a.ficha())                    # ficha() está en Animal — se hereda directamente
        print(f"  Sonido: {a.hacer_sonido()}")  # Python despacha la versión correcta
        print(f"  Dieta:  {a.tipo_dieta()}")  # Muestra en consola el resultado
        print()  # Muestra en consola el resultado
¿Por qué así? La clave del polimorfismo está en el ciclo for a in animales. El código no pregunta if type(a) == Perro: — simplemente llama a.hacer_sonido() y Python decide qué versión ejecutar. Eso hace el código extensible: si mañana agregas clase Conejo(Animal), el ciclo la maneja sin cambios.

🖥️ Salida esperada en consola:

🐾 Rex (4 años) | Carnívoro
  Sonido: ¡Guau! 🐶
  Dieta:  Carnívoro

🐾 Luna (2 años) | Omnívoro
  Sonido: ¡Miau! 🐱
  Dieta:  Omnívoro

🐾 Max (6 años) | Carnívoro
  Sonido: ¡Guau! 🐶
  Dieta:  Carnívoro
EJERCICIO 3 · ABSTRACCIÓN + ENCAPSULAMIENTO

💼 Sistema de Nómina

20 pts
📋 Descripción: Una empresa tiene dos tipos de empleados: fijos (sueldo mensual garantizado) y por horas (pago según horas trabajadas × tarifa). Ambos comparten nombre, ID y departamento, pero el cálculo del salario es diferente. El salario bruto es privado; solo se accede mediante getters. Usa clase abstracta y encapsulamiento estricto.
📐 Diagrama de Clases
«abstract» Empleado - __nombre: str - __id_empleado: str - __departamento: str + get_nombre(): str + calcular_salario()* + generar_recibo() EmpleadoFijo - __sueldo_base: float + calcular_salario() + get_sueldo() EmpleadoPorHoras - __horas: float - __tarifa: float + calcular_salario()
👤 Casos de Uso
RRHH Sistema Nómina Registrar Empleado Calcular Salario Generar Recibo Ver Nómina Total
🔄 Diagrama de Secuencia
:RRHH :Nomina :Empleado agregar(empleado) calcular_nomina() calcular_salario() monto: float generar_recibo() recibo: str total nómina

✏️ Lo que debes implementar:

  • Clase abstracta Empleado con atributos privados y sus getters
  • EmpleadoFijo.calcular_salario() → retorna __sueldo_base
  • EmpleadoPorHoras.calcular_salario() → retorna __horas * __tarifa
  • Método generar_recibo() en la clase base usando calcular_salario()
  • main que calcule la nómina total iterando sobre una lista de empleados mixtos
💻 CÓDIGO Guía línea por línea
1 Clase abstracta Empleado — contrato de nómina
nomina.py — Empleado (ABC)
from abc import ABC, abstractmethod  # Importa ABC, abstractmethod del módulo abc

class Empleado(ABC):  # Empleado hereda de ABC — recibe todos sus atributos y métodos
    def __init__(self, nombre, puesto):  # Constructor: inicializa el objeto con los datos recibidos
        self.__nombre = nombre              # Privado — encapsulamiento
        self.__puesto = puesto  # Guarda __puesto como atributo de instancia

    def get_nombre(self): return self.__nombre  # Método que get nombre
    def get_puesto(self): return self.__puesto  # Método que get puesto

    @abstractmethod                          # Cada tipo de empleado calcula diferente
    def calcular_salario(self): pass  # Método que calcular salario

    def generar_recibo(self):               # Método concreto: usa calcular_salario() sin saber CÓMO
        salario = self.calcular_salario()  # Polimorfismo: llama la versión de la subclase
        return (  # Retorna el valor calculado al que llamó este método
            f"┌─────────────────────────────┐\n"  # Continúa la lógica del ejemplo paso a paso
            f"│  RECIBO: {self.__nombre:<19}│\n"  # Continúa la lógica del ejemplo paso a paso
            f"│  Puesto: {self.__puesto:<19}│\n"  # Continúa la lógica del ejemplo paso a paso
            f"│  Salario: ${salario:>18,.2f} │\n"  # Continúa la lógica del ejemplo paso a paso
            f"└─────────────────────────────┘"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores
¿Por qué así? generar_recibo() llama self.calcular_salario() aunque Empleado no sabe si es fijo o por horas. Python usa dynamic dispatch: en tiempo de ejecución busca calcular_salario en la subclase real. Este patrón se llama Template Method — el esqueleto del algoritmo está en la base, los detalles en las subclases.
2 EmpleadoFijo y EmpleadoPorHoras
nomina.py — subclases
class EmpleadoFijo(Empleado):  # EmpleadoFijo hereda de Empleado — recibe todos sus atributos y métodos
    def __init__(self, nombre, puesto, salario_mensual):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(nombre, puesto)  # Llama al constructor de la clase padre para inicializar los atributos heredados
        self.__salario_mensual = salario_mensual  # Monto fijo sin importar horas

    def calcular_salario(self):           # Implementa el @abstractmethod: retorna fijo
        return self.__salario_mensual  # Retorna el valor calculado al que llamó este método


class EmpleadoPorHoras(Empleado):  # EmpleadoPorHoras hereda de Empleado — recibe todos sus atributos y métodos
    def __init__(self, nombre, puesto, horas, tarifa):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(nombre, puesto)  # Llama al constructor de la clase padre para inicializar los atributos heredados
        self.__horas  = horas               # Horas trabajadas en el mes
        self.__tarifa = tarifa              # Precio por hora (ej. $120)

    def calcular_salario(self):           # Implementa el contrato: horas × tarifa
        return self.__horas * self.__tarifa  # Retorna el valor calculado al que llamó este método
¿Por qué así? Dos lógicas completamente distintas llevan el mismo nombre de método: eso es polimorfismo. EmpleadoFijo.calcular_salario() devuelve directamente el monto fijo; EmpleadoPorHoras.calcular_salario() multiplica. El código cliente (main) no necesita saber cuál es cuál.
3 Programa principal — nómina total
nomina.py — main
if __name__ == "__main__":  # Se ejecuta solo cuando este archivo se corre directamente (no cuando se importa)
    empleados = [  # Asigna un valor inicial o calculado a empleados
        EmpleadoFijo("Ana López",   "Gerente",  25000),  # Continúa la lógica del ejemplo paso a paso
        EmpleadoPorHoras("Luis Ramos", "Diseñador", 80, 115),  # 80h × $115 = $9200
    ]  # Cierra la estructura abierta en las líneas anteriores

    total = 0  # Asigna un valor inicial o calculado a total
    for e in empleados:  # Itera sobre cada e en empleados
        print(e.generar_recibo())           # Llama al método de la BASE con lógica de la SUBCLASE
        total += e.calcular_salario()     # Acumula: $25,000 + $9,200
        print()  # Muestra en consola el resultado

    print(f"{'═'*31}")  # Muestra en consola el resultado
    print(f"  NÓMINA TOTAL: ${total:>14,.2f}")  # Muestra en consola el resultado
    print(f"{'═'*31}")  # Muestra en consola el resultado
¿Por qué así? La variable total acumula sin condicionales. e.calcular_salario() en el ciclo puede ser un empleado fijo o por horas — no importa. f"{'═'*31}" multiplica el carácter por 31 para crear la línea decorativa. La especificación de formato :>14,.2f alinea a la derecha, 14 caracteres, miles separados por coma, 2 decimales.

🖥️ Salida esperada en consola:

┌─────────────────────────────┐
│ RECIBO: Ana López          │
│ Puesto: Gerente            │
│ Salario: $         25,000.00 │
└─────────────────────────────┘

┌─────────────────────────────┐
│ RECIBO: Luis Ramos         │
│ Puesto: Diseñador          │
│ Salario: $          9,200.00 │
└─────────────────────────────┘

═══════════════════════════════
NÓMINA TOTAL: $       34,200.00
═══════════════════════════════
EJERCICIO 4 · COMPOSICIÓN

🛒 Tienda Online

15 pts
📋 Descripción: Una tienda en línea permite a clientes agregar productos a un carrito y calcular el total. Un Cliente tiene un Carrito, y el carrito contiene una lista de Productos. El precio de cada producto es privado. Esta relación es de composición: el carrito no existe sin el cliente.
📐 Diagrama de Clases
Producto - __nombre: str - __precio: float - stock: int + get_precio(): float + get_nombre(): str Carrito - productos: list + agregar(prod) + calcular_total() + mostrar() Cliente - __nombre - __email + comprar() + get_nombre() 1..*
👤 Casos de Uso
Cliente Sistema Tienda Buscar Producto Agregar al Carrito Ver Total Confirmar Compra
🔄 Diagrama de Secuencia
:Cliente :Carrito :Producto agregar(prod) get_precio() precio: float calcular_total() get_precio() ×N suma precios total: float

✏️ Lo que debes implementar:

  • Clase Producto con __precio privado y getter get_precio()
  • Clase Carrito con lista de productos y método calcular_total()
  • Clase Cliente que posea un Carrito como atributo (composición)
  • main que simule agregar 3 productos y mostrar el total de la compra
💻 CÓDIGO Guía línea por línea
1 Clase Producto — encapsulamiento de precio e inventario
tienda.py — Producto
class Producto:  # Define la clase Producto
    def __init__(self, nombre, precio, stock):  # Constructor: inicializa el objeto con los datos recibidos
        self.__nombre = nombre              # Privado — se protege de cambios externos
        self.__precio = precio              # Precio unitario — nadie puede cambiarlo sin set_precio()
        self.__stock  = stock               # Unidades disponibles en almacén

    def get_nombre(self): return self.__nombre  # Método que get nombre
    def get_precio(self): return self.__precio  # Método que get precio

    def disponible(self):                  # Pregunta pública: ¿hay stock?
        return self.__stock > 0  # Retorna el valor calculado al que llamó este método

    def reducir_stock(self):  # Método que reducir stock
        if self.disponible():               # Reutiliza el método público
            self.__stock -= 1  # Continúa la lógica del ejemplo paso a paso
            return True  # Retorna el valor calculado al que llamó este método
        return False                         # False si no hay stock — el carrito maneja el caso

    def __str__(self):  # Define cómo se muestra el objeto al usar print()
        return f"{self.__nombre} — ${self.__precio:,.2f} (stock:{self.__stock})"  # Retorna el valor calculado al que llamó este método
¿Por qué así? __precio es privado porque el precio de un producto no debe cambiar arbitrariamente desde fuera — por eso solo hay getter, no setter. reducir_stock llama a disponible() en lugar de comparar directamente — reutilización interna de código y punto único de verdad para la regla de negocio.
2 Carrito y Cliente — composición
tienda.py — Carrito + Cliente
class Carrito:  # Define la clase Carrito
    def __init__(self):  # Constructor: inicializa el objeto con los datos recibidos
        self.__items = []                   # Lista de objetos Producto — empieza vacía

    def agregar(self, producto):            # Recibe un Producto, no un string
        if producto.reducir_stock():        # Delega la validación a Producto
            self.__items.append(producto)  # Agrega un elemento al final de self.__items
            return f"+ Agregado: {producto.get_nombre()}"  # Retorna el valor calculado al que llamó este método
        return f"Sin stock: {producto.get_nombre()}"  # Retorna el valor calculado al que llamó este método

    def calcular_total(self):  # Método que calcular total
        return sum(p.get_precio() for p in self.__items)  # sum() con generator expression

    def ver_resumen(self):  # Método que ver resumen
        lineas = [f"  · {p.get_nombre()}: ${p.get_precio():,.2f}" for p in self.__items]  # Asigna un valor inicial o calculado a lineas
        return "\n".join(lineas) + f"\n  TOTAL: ${self.calcular_total():,.2f}"  # Retorna el valor calculado al que llamó este método


class Cliente:                            # COMPOSICIÓN: Cliente "tiene" un Carrito
    def __init__(self, nombre):  # Constructor: inicializa el objeto con los datos recibidos
        self.__nombre  = nombre  # Guarda __nombre como atributo de instancia
        self.__carrito = Carrito()          # El carrito nace CON el cliente — no se pasa externo
                                             # Eso es COMPOSICIÓN: ciclo de vida compartido
    def comprar(self, producto):  # Método que comprar
        resultado = self.__carrito.agregar(producto)  # Asigna un valor inicial o calculado a resultado
        print(f"[{self.__nombre}] {resultado}")  # Muestra en consola el resultado

    def ver_carrito(self):  # Método que ver carrito
        print(f"\n🛒 Carrito de {self.__nombre}:\n{self.__carrito.ver_resumen()}")  # Muestra en consola el resultado
¿Por qué así? self.__carrito = Carrito() dentro de Cliente.__init__ crea el carrito al instanciar el cliente — eso es composición: si el cliente desaparece, el carrito también. Si fuera agregación, recibirías el carrito como parámetro (def __init__(self, nombre, carrito)) y podría existir sin el cliente. sum(p.get_precio() for p in self.__items) usa una generator expression — más eficiente que una lista porque no crea un arreglo temporal.
3 Programa principal — simulación de compra
tienda.py — main
if __name__ == "__main__":  # Se ejecuta solo cuando este archivo se corre directamente (no cuando se importa)
    laptop   = Producto("Laptop",   18000, 2)  # 2 unidades en almacén
    mouse    = Producto("Mouse",     350,  5)  # Asigna un valor inicial o calculado a mouse
    teclado  = Producto("Teclado",   700,  1)  # Solo 1 unidad

    cliente = Cliente("Pedro Ruiz")           # Crea cliente — carrito nace aquí

    cliente.comprar(laptop)                    # Laptop se suma al carrito (stock 2→1)
    cliente.comprar(mouse)  # Completa la llamada o expresión iniciada anteriormente
    cliente.comprar(teclado)  # Completa la llamada o expresión iniciada anteriormente

    cliente.ver_carrito()                      # Muestra resumen + TOTAL
¿Por qué así? Todo el flujo está encapsulado: main solo habla con Cliente. El cliente habla con Carrito. El carrito habla con Producto. Nunca main accede directamente a __items o __stock — cada clase es responsable de sus propios datos. Ese principio se llama Ley de Demeter: habla solo con tus amigos inmediatos.

🖥️ Salida esperada en consola:

[Pedro Ruiz] + Agregado: Laptop
[Pedro Ruiz] + Agregado: Mouse
[Pedro Ruiz] + Agregado: Teclado

🛒 Carrito de Pedro Ruiz:
  · Laptop: $18,000.00
  · Mouse: $350.00
  · Teclado: $700.00
  TOTAL: $19,050.00
EJERCICIO 5 · HERENCIA MULTINIVEL

🎓 Sistema Universitario

20 pts
📋 Descripción: Una universidad tiene personas, de las cuales unas son estudiantes y otras son profesores. Un estudiante tiene matrícula y carrera; un profesor tiene número de empleado y puede impartir cursos. Ambos heredan de Persona. Aplica encapsulamiento en todos los atributos identificadores y demuestra herencia de dos niveles.
📐 Diagrama de Clases
Persona - __nombre: str - __edad: int - __curp: str + get_nombre(): str + presentarse(): str Estudiante - __matricula: str - carrera: str + estudiar(materia) Profesor - __num_empleado: str - especialidad: str + impartir(curso)
👤 Casos de Uso
Estudiante Profesor Sistema Universidad Inscribirse Estudiar Materia Impartir Curso Ver Directorio
🔄 Diagrama de Secuencia
:Estudiante :Universidad :Profesor inscribirse(carrera) matricula asignada estudiar("POO") impartir("POO") "Clase impartida" presentarse() "Hola, soy …"

✏️ Lo que debes implementar:

  • Clase Persona con atributos privados __nombre, __edad, __curp y método presentarse()
  • Clase Estudiante heredando de Persona; agrega __matricula y estudiar(materia)
  • Clase Profesor heredando de Persona; agrega __num_empleado y impartir(curso)
  • main que cree uno de cada tipo, llame presentarse() en ambos y demuestre los métodos propios
💻 CÓDIGO Guía línea por línea
1 Clase base Persona — código compartido
universidad.py — Persona
class Persona:                            # Clase padre genérica — NO abstracta
    def __init__(self, nombre, edad, correo):  # Constructor: inicializa el objeto con los datos recibidos
        self.__nombre = nombre              # __ = privado; solo accesible por getters
        self.__edad   = edad  # Guarda __edad como atributo de instancia
        self.__correo = correo  # Guarda __correo como atributo de instancia

    def get_nombre(self): return self.__nombre  # Método que get nombre
    def get_edad(self):   return self.__edad  # Método que get edad
    def get_correo(self): return self.__correo  # Método que get correo

    def presentarse(self):                  # Método heredable — ambas subclases lo usan
        return (  # Retorna el valor calculado al que llamó este método
            f"Hola, soy {self.__nombre}, {self.__edad} años.\n"  # Continúa la lógica del ejemplo paso a paso
            f"Correo: {self.__correo}"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores
¿Por qué así? Persona no es abstracta porque sí podría existir una persona genérica. Los atributos son privados — si Estudiante intenta hacer self.__nombre heredado, Python no lo encuentra (name mangling: sería _Persona__nombre). Por eso get_nombre() es la única vía. Esto refuerza el encapsulamiento incluso dentro de la jerarquía.
2 Estudiante y Profesor — especialización por herencia
universidad.py — subclases
class Estudiante(Persona):               # Extiende Persona con datos académicos
    def __init__(self, nombre, edad, correo, matricula):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(nombre, edad, correo) # Persona maneja nombre, edad, correo
        self.__matricula = matricula         # Privado exclusivo de Estudiante

    def get_matricula(self): return self.__matricula  # Método que get matricula

    def estudiar(self, materia):             # Comportamiento específico del rol
        return f"📚 {self.get_nombre()} estudia {materia} [Matrícula: {self.__matricula}]"  # Retorna el valor calculado al que llamó este método


class Profesor(Persona):                 # Otra especialización — distinto rol
    def __init__(self, nombre, edad, correo, num_empleado, departamento):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(nombre, edad, correo)  # Llama al constructor de la clase padre para inicializar los atributos heredados
        self.__num_empleado  = num_empleado  # Guarda __num_empleado como atributo de instancia
        self.__departamento  = departamento  # Guarda __departamento como atributo de instancia

    def impartir(self, curso):              # Comportamiento específico del rol
        return f"🎓 Prof. {self.get_nombre()} imparte '{curso}' | {self.__departamento}"  # Retorna el valor calculado al que llamó este método

    def get_num_empleado(self): return self.__num_empleado  # Método que get num empleado
¿Por qué así? Nota que estudiar() e impartir() son métodos nuevos, no sobreescrituras — eso es extensión por herencia. La subclase agrega comportamiento sin tocar la base. self.get_nombre() (con un guion bajo conceptual) usa el getter público porque __nombre de Persona no es directamente accesible desde la subclase.
3 Programa principal
universidad.py — main
if __name__ == "__main__":  # Se ejecuta solo cuando este archivo se corre directamente (no cuando se importa)
    est = Estudiante("Sara Vega", 20, "sara@uth.mx", "A00123")  # Asigna un valor inicial o calculado a est
    pro = Profesor("Dr. Mora", 45, "mora@uth.mx", "P0099", "Ingeniería")  # Asigna un valor inicial o calculado a pro

    print("── ESTUDIANTE ──")  # Muestra en consola el resultado
    print(est.presentarse())               # Método heredado de Persona — sin cambios
    print(est.estudiar("POO"))            # Método PROPIO de Estudiante

    print("\n── PROFESOR ──")  # Muestra en consola el resultado
    print(pro.presentarse())               # Mismo método heredado — polimorfismo de interfaz
    print(pro.impartir("Estructuras de Datos"))  # Método PROPIO de Profesor

    # isinstance() verifica la jerarquía en tiempo de ejecución
    print(f"\n¿est es Persona? {isinstance(est, Persona)}")  # → True
    print(f"¿pro es Estudiante? {isinstance(pro, Estudiante)}")  # → False
¿Por qué así? isinstance(est, Persona) devuelve True porque Estudiante hereda de Persona — toda la cadena de herencia es reconocida. isinstance(pro, Estudiante) devuelve False porque Profesor y Estudiante son hermanos, no padre-hijo. Esta función es útil para validar tipos en métodos que aceptan cualquier Persona.

🖥️ Salida esperada en consola:

── ESTUDIANTE ──
Hola, soy Sara Vega, 20 años.
Correo: sara@uth.mx
📚 Sara Vega estudia POO [Matrícula: A00123]

── PROFESOR ──
Hola, soy Dr. Mora, 45 años.
Correo: mora@uth.mx
🎓 Prof. Dr. Mora imparte 'Estructuras de Datos' | Ingeniería

¿est es Persona? True
¿pro es Estudiante? False
EJERCICIO 6 · POLIMORFISMO CLÁSICO

📐 Figuras Geométricas

15 pts
📋 Descripción: Un sistema de geometría maneja distintas figuras: círculo, rectángulo y triángulo. Todas comparten la obligación de calcular su área y perímetro, pero cada una lo hace con su propia fórmula. Usa una clase abstracta Figura para definir el contrato y deja que cada subclase lo implemente.
📐 Diagrama de Clases
«abstract» Figura - color: str + area()* : float + perimetro()* : float + describir(): str Circulo - radio: float + area() + perimetro() Rectangulo - base: float - altura: float + area() + perimetro() Triangulo - base: float - altura: float + area() + perimetro()
👤 Casos de Uso
Usuario Sistema Geometría Crear Figura Calcular Área Calcular Perímetro Describir Figura
🔄 Diagrama de Secuencia
:Usuario :Geometria :Figura agregar(figura) calcular_areas() area() [polimorfismo] valor según figura describir_todas() describir() "Figura: …" reporte completo

✏️ Lo que debes implementar:

  • Clase abstracta Figura con métodos abstractos area() y perimetro()
  • Método concreto describir() en Figura que llame area() y perimetro()
  • Circulo: área = π·r², perímetro = 2π·r
  • Rectangulo: área = base·altura, perímetro = 2·(base+altura)
  • Triangulo: área = (base·altura)/2 — asume triángulo rectángulo
  • main con lista de 3 figuras distintas; ciclo que llame describir() en cada una
💻 CÓDIGO Guía línea por línea
1 Clase abstracta Figura — contrato geométrico
figuras.py — Figura (ABC)
from abc import ABC, abstractmethod  # Importa ABC, abstractmethod del módulo abc
import math                               # Para math.pi (3.14159...) y math.sqrt()

class Figura(ABC):                         # Clase abstracta: define contrato SIN implementar
    def __init__(self, color):  # Constructor: inicializa el objeto con los datos recibidos
        self.__color = color                # El color es común a TODAS las figuras

    def get_color(self): return self.__color  # Método que get color

    @abstractmethod                          # OBLIGATORIO en cada subclase concreta
    def area(self): pass  # Método que area

    @abstractmethod
    def perimetro(self): pass  # Método que perimetro

    def describir(self):                    # Método concreto: arma el texto usando area()/perimetro()
        return (  # Retorna el valor calculado al que llamó este método
            f"[{self.__class__.__name__}] Color: {self.__color}\n"  # __class__.__name__ = "Circulo"
            f"  Área:     {self.area():.2f}\n"        # :.2f = 2 decimales
            f"  Perímetro:{self.perimetro():.2f}"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores
¿Por qué así? self.__class__.__name__ es un atributo mágico que devuelve el nombre de la clase real del objeto en tiempo de ejecución — si el objeto es un Circulo, devuelve "Circulo". describir() llama area() y perimetro() sin saber cómo se calculan — eso es abstracción pura: la base define el patrón, las subclases los datos.
2 Circulo, Rectangulo y Triangulo — implementaciones concretas
figuras.py — subclases concretas
class Circulo(Figura):  # Circulo hereda de Figura — recibe todos sus atributos y métodos
    def __init__(self, color, radio):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(color)              # Pasa color a Figura.__init__
        self.__radio = radio  # Guarda __radio como atributo de instancia

    def area(self):                        # π · r²
        return math.pi * self.__radio ** 2  # ** = potencia en Python

    def perimetro(self):                   # 2 · π · r
        return 2 * math.pi * self.__radio  # Retorna el valor calculado al que llamó este método


class Rectangulo(Figura):  # Rectangulo hereda de Figura — recibe todos sus atributos y métodos
    def __init__(self, color, base, altura):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(color)  # Llama al constructor de la clase padre para inicializar los atributos heredados
        self.__base   = base  # Guarda __base como atributo de instancia
        self.__altura = altura  # Guarda __altura como atributo de instancia

    def area(self):  # Método que area
        return self.__base * self.__altura  # base · altura

    def perimetro(self):  # Método que perimetro
        return 2 * (self.__base + self.__altura)  # 2 · (b + h)


class Triangulo(Figura):                # Triángulo rectángulo (base, altura, hipotenusa)
    def __init__(self, color, base, altura):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(color)  # Llama al constructor de la clase padre para inicializar los atributos heredados
        self.__base   = base  # Guarda __base como atributo de instancia
        self.__altura = altura  # Guarda __altura como atributo de instancia
        self.__hip    = math.sqrt(base**2 + altura**2)  # Pitágoras: √(b²+h²)

    def area(self):  # Método que area
        return (self.__base * self.__altura) / 2  # Retorna el valor calculado al que llamó este método

    def perimetro(self):  # Método que perimetro
        return self.__base + self.__altura + self.__hip  # 3 lados del rectángulo
¿Por qué así? Las tres clases tienen area() y perimetro() con la misma firma pero fórmulas completamente distintas — polimorfismo. math.sqrt(base**2 + altura**2) calcula la hipotenusa usando el teorema de Pitágoras. math.pi es la constante π con 15 decimales de precisión — mucho más preciso que escribir 3.14159.
3 Programa principal — ciclo polimórfico
figuras.py — main
if __name__ == "__main__":  # Se ejecuta solo cuando este archivo se corre directamente (no cuando se importa)
    figuras = [                             # Lista polimórfica: 3 tipos, un mismo ciclo
        Circulo("rojo",  5),                # radio = 5
        Rectangulo("azul", 4, 8),           # base=4, altura=8
        Triangulo("verde", 6, 8),           # base=6, altura=8 → hip = √(36+64) = 10
    ]  # Cierra la estructura abierta en las líneas anteriores

    for f in figuras:  # Itera sobre cada f en figuras
        print(f.describir())               # Un método llama area()+perimetro() de cada tipo
        print("──────────────────────")  # Muestra en consola el resultado

    # Bonus: figura de mayor área sin saber el tipo
    mayor = max(figuras, key=lambda f: f.area())  # key = función comparadora
    print(f"Mayor área: {mayor.__class__.__name__} con {mayor.area():.2f}")  # Muestra en consola el resultado
¿Por qué así? max(figuras, key=lambda f: f.area()) encuentra la figura con mayor área usando una función lambda como criterio. Nunca pregunta qué tipo es — solo llama .area(). Eso es polimorfismo en su forma más elegante: el algoritmo max() puede trabajar con cualquier objeto que tenga area(), sin importar su tipo real.

🖥️ Salida esperada en consola:

[Circulo] Color: rojo
  Área:     78.54
  Perímetro:31.42
──────────────────────
[Rectangulo] Color: azul
  Área:     32.00
  Perímetro:24.00
──────────────────────
[Triangulo] Color: verde
  Área:     24.00
  Perímetro:24.00
──────────────────────
Mayor área: Circulo con 78.54

🐍 Unidad 2 — Sintaxis y Documentación POO en Python

Aprenderás la sintaxis completa de Python orientado a objetos: clases, objetos, herencia múltiple, polimorfismo, encapsulamiento y manejo de eventos con ejemplos de negocios reales.

⏱ 14 horas📊 Exámen y Prácticas en clase - Calificación🛠 PyCharm + Python

📋 Temas de la Unidad

🏛️ LOS 4 PILARES DE LA POO — Orden de Aprendizaje

🎭
PILAR 1
Abstracción
Define QUÉ debe hacer un objeto
🔒
PILAR 2
Encapsulamiento
Protege los datos internos
🧬
PILAR 3
Herencia
Reutiliza y extiende clases
🎪
PILAR 4
Polimorfismo
Un método, múltiples formas

💡 Cada pilar se apoya en el anterior. No saltes pasos — el orden importa.

🧱

2.1 Clases y Objetos

Fundamento de la POO: definición, constructores, atributos de clase vs instancia.

🎭

2.2 Abstracción

Pilar 1 — Clases abstractas, ABC, métodos abstractos. Define el contrato.

🔒

2.3 Encapsulamiento

Pilar 2 — Atributos privados, getters, setters, @property.

🧬

2.4 Herencia

Pilar 3 — Herencia simple y múltiple, super(), reutilización de código.

🎪

2.5 Polimorfismo

Pilar 4 — Override, duck typing, mismo método con diferente comportamiento.

📖

2.6 Documentación

Docstrings, comentarios profesionales, pydoc.

📝 Referencia Completa — Sintaxis de Clases

ElementoSintaxisDescripción
Clase simpleclass Producto:Clase sin herencia
Clase con herenciaclass Ropa(Producto):Hereda de Producto
Constructordef __init__(self, n, p):Se llama al crear el objeto
Atributo de claseiva = 0.16 (fuera de __init__)Compartido por todos los objetos
Atributo de instanciaself.nombre = nÚnico para cada objeto
Método regulardef calcular(self):Necesita self
Método de clase@classmethod / def m(cls):Accede a atributos de clase
Método estático@staticmethod / def m():No necesita self ni cls
Propiedad@property / def precio(self):Accede como atributo
Setter@precio.setterValida al asignar valor
Reprdef __repr__(self):Representación técnica del objeto
Comparacióndef __eq__(self, otro):Define cuándo dos objetos son iguales
Longituddef __len__(self):Define len(objeto)
🏪 Ejemplo de Negocio: Sistema de Inventario — Tienda de Ropa

Modelaremos un inventario para una tienda de ropa de Hermosillo. Tendremos Productos con categorías (Ropa, Calzado, Accesorios) usando herencia. Cada producto calcula su precio con IVA usando encapsulamiento y propiedades.

🖼️ Diagrama de Herencia — Jerarquía de Clases del Inventario
📦 CLASE BASE: Producto
• nombre, __precio_base, __stock
• iva = 0.16 (atributo de clase)
+ precio_con_iva() + vender() + descripcion()
┬───────────┬
👔 CLASE HIJA: Ropa
+ talla, color
+ descripcion() ← sobreescribe
👟 CLASE HIJA: Calzado
+ numero, material
+ descripcion() ← sobreescribe
💡 Analogía: Como en "Moda Hermosillo" — todos los productos comparten precio e IVA, pero ropa tiene talla y calzado tiene número
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
Los 4 pilares en un solo sistema: herencia (Ropa/Calzado heredan de Producto), encapsulamiento (__precio_base), polimorfismo (descripcion()), abstracción (IVA como atributo de clase).
🚀 ¿QUÉ LOGRAMOS?
Un inventario real para "Moda Hermosillo" con productos de distintos tipos, catálogo en pantalla y operaciones de venta con validación de stock.
🧠 ¿QUÉ APRENDEMOS?
Cómo combinar herencia con super(), usar @property y setters para encapsular datos, y aprovechar el polimorfismo para que el mismo bucle for funcione con cualquier tipo de producto.
inventario_tienda.py — Sistema de Inventario POO completo
# ═════════════════════════════════════════════════════════════════
# SISTEMA DE INVENTARIO — Tienda de Ropa "Moda Hermosillo"
# Unidad 2: Herencia, Encapsulamiento, Polimorfismo
# ═════════════════════════════════════════════════════════════════

class Producto:  # Define la clase Producto
    """
    CLASE BASE para todos los productos de la tienda.
    Define lo que tienen en común: nombre, precio, stock.
    """
    iva = 0.16   # Atributo de CLASE: compartido por todos los productos

    def __init__(self, nombre, precio_base, stock):  # Constructor: inicializa el objeto con los datos recibidos
        self.nombre = nombre  # Guarda nombre como atributo de instancia
        self.__precio_base = precio_base   # __ = privado (encapsulado)
        self.__stock = stock               # __ = privado

    # ── PROPIEDADES (acceso controlado a atributos privados) ──

    @property  # Decorador: permite acceder a este método como si fuera un atributo
    def precio_base(self):  # Método que precio base
        # @property permite acceder como atributo: producto.precio_base
        return self.__precio_base  # Retorna el valor calculado al que llamó este método

    @precio_base.setter  # Decorador: define el setter de una propiedad para validar o actualizar datos
    def precio_base(self, valor):  # Método que precio base
        # El setter VALIDA el dato antes de guardarlo
        if valor < 0:  # Verifica valor < 0 antes de continuar
            raise ValueError("El precio no puede ser negativo.")  # Lanza una excepción si hay un error; detiene el flujo normal
        self.__precio_base = valor  # Guarda __precio_base como atributo de instancia

    @property  # Decorador: permite acceder a este método como si fuera un atributo
    def stock(self):  # Método que stock
        return self.__stock  # Retorna el valor calculado al que llamó este método

    def precio_con_iva(self):  # Método que precio con iva
        # Calcula el precio final sumando el IVA
        return self.__precio_base * (1 + self.iva)  # Retorna el valor calculado al que llamó este método

    def vender(self, cantidad):  # Método que vender
        # Verifica stock antes de vender
        if cantidad > self.__stock:  # Verifica cantidad > self.__stock antes de continuar
            return f"❌ Stock insuficiente. Solo hay {self.__stock} unidades."  # Retorna el valor calculado al que llamó este método
        self.__stock -= cantidad  # Continúa la lógica del ejemplo paso a paso
        return f"✅ Venta de {cantidad} '{self.nombre}' registrada. Stock: {self.__stock}"  # Retorna el valor calculado al que llamó este método

    def descripcion(self):  # Método que descripcion
        # Método que CADA subclase puede reemplazar (polimorfismo)
        return f"Producto genérico: {self.nombre}"  # Retorna el valor calculado al que llamó este método

    def __str__(self):  # Define cómo se muestra el objeto al usar print()
        return (  # Retorna el valor calculado al que llamó este método
            f"📦 {self.nombre}\n"  # Continúa la lógica del ejemplo paso a paso
            f"   Precio base: ${self.__precio_base:.2f}\n"  # Continúa la lógica del ejemplo paso a paso
            f"   Precio con IVA: ${self.precio_con_iva():.2f}\n"  # Continúa la lógica del ejemplo paso a paso
            f"   Stock: {self.__stock} unidades"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores


class Ropa(Producto):  # Ropa hereda de Producto — recibe todos sus atributos y métodos
    """
    SUBCLASE de Producto. Agrega talla y color.
    Hereda TODO de Producto y le añade sus propias cosas.
    """
    def __init__(self, nombre, precio_base, stock, talla, color):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(nombre, precio_base, stock)  # Inicializa la parte de Producto
        self.talla = talla  # Guarda talla como atributo de instancia
        self.color = color  # Guarda color como atributo de instancia

    def descripcion(self):  # Método que descripcion
        # SOBREESCRITURA del método (polimorfismo): Ropa tiene su propia descripción
        return f"👔 Ropa: {self.nombre} | Talla: {self.talla} | Color: {self.color}"  # Retorna el valor calculado al que llamó este método


class Calzado(Producto):  # Calzado hereda de Producto — recibe todos sus atributos y métodos
    def __init__(self, nombre, precio_base, stock, numero, material):  # Constructor: inicializa el objeto con los datos recibidos
        super().__init__(nombre, precio_base, stock)  # Llama al constructor de la clase padre para inicializar los atributos heredados
        self.numero = numero  # Guarda numero como atributo de instancia
        self.material = material  # Guarda material como atributo de instancia

    def descripcion(self):  # Método que descripcion
        return f"👟 Calzado: {self.nombre} | No.{self.numero} | {self.material}"  # Retorna el valor calculado al que llamó este método


class Inventario:  # Define la clase Inventario
    """
    Clase que CONTIENE una lista de productos (Composición).
    El inventario tiene productos, no hereda de ellos.
    """
    def __init__(self, nombre_tienda):  # Constructor: inicializa el objeto con los datos recibidos
        self.nombre_tienda = nombre_tienda  # Guarda nombre_tienda como atributo de instancia
        self.productos = []  # Guarda productos como atributo de instancia

    def agregar(self, producto):  # Método que agregar
        self.productos.append(producto)  # Agrega un elemento al final de self.productos
        print(f"➕ '{producto.nombre}' añadido al inventario.")  # Muestra en consola el resultado

    def mostrar_catalogo(self):  # Método que mostrar catalogo
        print(f"\n{'='*50}")  # Muestra en consola el resultado
        print(f"🏪 {self.nombre_tienda} — Catálogo")  # Muestra en consola el resultado
        print(f"{'='*50}")  # Muestra en consola el resultado
        for p in self.productos:  # Itera sobre cada p en self.productos
            print(p.descripcion())  # POLIMORFISMO: cada p llama su propia descripcion()
            print(f"   Precio con IVA: ${p.precio_con_iva():.2f}")  # Muestra en consola el resultado


# ══════════════════ PROGRAMA PRINCIPAL ══════════════════
inv = Inventario("Moda Hermosillo")  # Asigna un valor inicial o calculado a inv

inv.agregar(Ropa("Playera Polo", 250.0, 30, "M", "Azul marino"))  # Completa la llamada o expresión iniciada anteriormente
inv.agregar(Ropa("Jeans Skinny", 480.0, 15, "28", "Negro"))  # Completa la llamada o expresión iniciada anteriormente
inv.agregar(Calzado("Tenis Running", 890.0, 10, 27, "Sintético"))  # Completa la llamada o expresión iniciada anteriormente

inv.mostrar_catalogo()  # Completa la llamada o expresión iniciada anteriormente

# Probamos venta:
print("\n🛒 Realizando venta...")  # Muestra en consola el resultado
print(inv.productos[0].vender(3))   # Vende 3 playeras
print(inv.productos[0].vender(50))  # Intenta vender 50 (stock insuficiente)

🔒 Tema 2.3 — Encapsulamiento Detallado

🔑 Regla de Encapsulamiento en Python
self.datopúblico: cualquiera puede leerlo y modificarlo
self._datoprotegido: convención, no modificar desde afuera
self.__datoprivado: Python lo "oculta" (name mangling), usa property para acceder
🖼️ Encapsulamiento — Los datos privados solo se tocan con métodos controlados
🏦 CuentaBancaria
🔒 PRIVADO (nadie accede directo)
• __saldo = 5000
• __historial = []
✅ PÚBLICO (interfaz controlada)
+ saldo → @property
+ depositar(monto)
+ retirar(monto)
❌ cuenta.__saldo = 999999 → Error
✅ cuenta.depositar(2000) → OK
✅ cuenta.saldo → 7000
💡 Analogía: Como un cajero automático — puedes sacar dinero con tu tarjeta, pero no puedes abrir la máquina y agarrar los billetes directamente
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
El encapsulamiento real: los atributos privados (__saldo, __historial) no se pueden modificar desde afuera — solo mediante métodos que validan la operación.
🚀 ¿QUÉ LOGRAMOS?
Una cuenta bancaria segura: los depósitos y retiros validan el monto antes de ejecutarse, y el historial se lleva automáticamente sin que nadie lo pueda borrar.
🧠 ¿QUÉ APRENDEMOS?
La diferencia entre atributos públicos, protegidos y privados, cómo usar @property para leer datos privados de forma segura, y cómo el setter valida antes de guardar.
encapsulamiento.py — Cuenta Bancaria con validación
class CuentaBancaria:  # Define la clase CuentaBancaria
    def __init__(self, titular, saldo_inicial):  # Constructor: inicializa el objeto con los datos recibidos
        self.titular = titular            # Público
        self.__saldo = saldo_inicial      # Privado — nadie lo toca directamente
        self.__historial = []             # Privado — registro de movimientos

    @property  # Decorador: permite acceder a este método como si fuera un atributo
    def saldo(self):  # Método que saldo
        # Getter: permite LEER el saldo de forma controlada
        return self.__saldo  # Retorna el valor calculado al que llamó este método

    def depositar(self, monto):  # Método que depositar
        if monto <= 0:  # Verifica monto <= 0 antes de continuar
            return "❌ Monto debe ser mayor a 0."  # Retorna el valor calculado al que llamó este método
        self.__saldo += monto  # Continúa la lógica del ejemplo paso a paso
        self.__historial.append(f"+${monto}")  # Agrega un elemento al final de self.__historial
        return f"✅ Depósito de ${monto}. Saldo: ${self.__saldo}"  # Retorna el valor calculado al que llamó este método

    def retirar(self, monto):  # Método que retirar
        if monto > self.__saldo:  # Verifica monto > self.__saldo antes de continuar
            return "❌ Fondos insuficientes."  # Retorna el valor calculado al que llamó este método
        self.__saldo -= monto  # Continúa la lógica del ejemplo paso a paso
        self.__historial.append(f"-${monto}")  # Agrega un elemento al final de self.__historial
        return f"✅ Retiro de ${monto}. Saldo: ${self.__saldo}"  # Retorna el valor calculado al que llamó este método

    def ver_historial(self):  # Método que ver historial
        return self.__historial  # Retorna el valor calculado al que llamó este método

# Prueba:
cuenta = CuentaBancaria("Juan López", 5000)  # Asigna un valor inicial o calculado a cuenta
print(cuenta.depositar(2000))  # Muestra en consola el resultado
print(cuenta.retirar(800))  # Muestra en consola el resultado
print(f"Saldo actual: ${cuenta.saldo}")  # Usamos la property
# cuenta.__saldo = 999999  ← ERROR: no se puede acceder directamente

🎭 Polimorfismo — Mismo método, diferente comportamiento

🖼️ Polimorfismo — El mismo método procesar(), tres comportamientos distintos
🔴 MetodoPago (base)
procesar(monto)
← define el contrato
💵 Efectivo: "Pago en efectivo de $350"
💳 Tarjeta: "Cargo a tarjeta ****4521"
🏦 BBVA: "Transferencia vía BBVA"
def cobrar(pago, total): # No sabe el tipo exacto
  pago.procesar(total) # Cada uno responde diferente
💡 Analogía: Como en una ferretería de Hermosillo — "cobrar" funciona igual con efectivo, tarjeta o transferencia; la caja no cambia su proceso
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
El polimorfismo: la función cobrar() no necesita saber si el pago es efectivo, tarjeta o transferencia — simplemente llama procesar() y cada objeto responde diferente.
🚀 ¿QUÉ LOGRAMOS?
Un sistema de pagos donde puedes agregar nuevas formas de pago (ej: PayPal, CoDi) sin cambiar la función cobrar() — solo creas una nueva subclase.
🧠 ¿QUÉ APRENDEMOS?
Cómo sobreescribir métodos en subclases (override), el concepto de "duck typing" de Python, y por qué el polimorfismo hace el código más flexible y extensible.
polimorfismo.py — Sistema de Pagos
class MetodoPago:  # Define la clase MetodoPago
    # Clase abstracta base para métodos de pago
    def procesar(self, monto):  # Método que procesar
        raise NotImplementedError("Cada pago debe implementar procesar()")  # Lanza una excepción si hay un error; detiene el flujo normal

class PagoEfectivo(MetodoPago):  # PagoEfectivo hereda de MetodoPago — recibe todos sus atributos y métodos
    def procesar(self, monto):  # Método que procesar
        return f"💵 Pago en efectivo de ${monto:.2f} recibido."  # Retorna el valor calculado al que llamó este método

class PagoTarjeta(MetodoPago):  # PagoTarjeta hereda de MetodoPago — recibe todos sus atributos y métodos
    def __init__(self, ultimos4):  # Constructor: inicializa el objeto con los datos recibidos
        self.ultimos4 = ultimos4  # Guarda ultimos4 como atributo de instancia
    def procesar(self, monto):  # Método que procesar
        return f"💳 Cargo de ${monto:.2f} a tarjeta ****{self.ultimos4}."  # Retorna el valor calculado al que llamó este método

class PagoTransferencia(MetodoPago):  # PagoTransferencia hereda de MetodoPago — recibe todos sus atributos y métodos
    def __init__(self, banco):  # Constructor: inicializa el objeto con los datos recibidos
        self.banco = banco  # Guarda banco como atributo de instancia
    def procesar(self, monto):  # Método que procesar
        return f"🏦 Transferencia de ${monto:.2f} vía {self.banco}."  # Retorna el valor calculado al que llamó este método

# POLIMORFISMO en acción:
# La función no necesita saber QUÉ tipo de pago es, solo llama procesar()
def cobrar(pago: MetodoPago, total: float):  # Método que cobrar
    resultado = pago.procesar(total)   # Cada objeto responde diferente
    print(resultado)  # Muestra en consola el resultado

cobrar(PagoEfectivo(), 350.00)  # Completa la llamada o expresión iniciada anteriormente
cobrar(PagoTarjeta("4521"), 1280.50)  # Completa la llamada o expresión iniciada anteriormente
cobrar(PagoTransferencia("BBVA"), 9500.00)  # Completa la llamada o expresión iniciada anteriormente

🎭 Tema 2.4 — Abstracción

🧠 ¿Qué es la Abstracción?
La abstracción consiste en definir QUÉ debe hacer una clase sin especificar CÓMO lo hace. Es como un contrato: la clase abstracta exige a sus subclases que implementen ciertos métodos. En Python se logra con el módulo abc (Abstract Base Classes).
ElementoSintaxisDescripción
Importar ABCfrom abc import ABC, abstractmethodMódulo estándar de Python para abstracción
Clase abstractaclass Animal(ABC):No se puede instanciar directamente
Método abstracto@abstractmethod
def sonido(self): ...
Las subclases DEBEN implementarlo o Python lanza error
Método concretodef respirar(self): return "inhala"Puede coexistir con métodos abstractos
Subclase concretaclass Perro(Animal):Debe implementar TODOS los métodos abstractos
Propiedad abstracta@property
@abstractmethod
def categoria(self): ...
Obliga a definir una propiedad en la subclase
🏢 Ejemplo de Negocio: Sistema de Reportes — Empresa de Logística Hermosillo

Una empresa de logística necesita generar reportes en distintos formatos (PDF, Excel, correo electrónico). Todos los reportes comparten la misma estructura base pero cada uno se genera diferente. Usaremos abstracción para definir el contrato y que cada formato lo cumpla a su manera.

🖼️ Abstracción — Clase abstracta define el contrato, subclases lo implementan
🎭 CLASE ABSTRACTA: ReporteBase (ABC)
⛔ No se puede instanciar directamente
@abstractmethod generar() → obligatorio
@abstractmethod enviar() → obligatorio
encabezado() → ya implementado (hereda)
↓ hereda y cumple el contrato
📄 ReportePDF
generar() → PDF
enviar() → ruta
📊 ReporteExcel
generar() → .xlsx
enviar() → carpeta
✉️ ReporteEmail
generar() → texto
enviar() → correo
💡 Analogía: Como el contrato laboral de "Envíos Sonora" — define lo que DEBE hacer cada empleado, pero cada uno lo hace a su manera
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
La abstracción con ABC y @abstractmethod: la clase ReporteBase define QUÉ métodos deben existir sin implementarlos — cada subclase decide el CÓMO.
🚀 ¿QUÉ LOGRAMOS?
Un sistema de reportes para la empresa "Envíos Sonora" que puede generar y enviar reportes en PDF, Excel o correo usando exactamente el mismo bucle for.
🧠 ¿QUÉ APRENDEMOS?
Importar y usar ABC y abstractmethod, la diferencia entre métodos abstractos y concretos, y por qué Python lanza TypeError al intentar instanciar una clase abstracta.
abstraccion.py — Sistema de Reportes con Clases Abstractas
# ═══════════════════════════════════════════════════════════════
# ABSTRACCIÓN — Sistema de Reportes
# Empresa de Logística "Envíos Sonora"
# Unidad 2 · Programación Orientada a Objetos
# ═══════════════════════════════════════════════════════════════

from abc import ABC, abstractmethod   # Paso 1: importar el módulo abc


# ── CLASE ABSTRACTA BASE ────────────────────────────────────────
# No se puede instanciar: ReporteBase() lanzaría TypeError
# Solo define el CONTRATO que deben cumplir las subclases
class ReporteBase(ABC):  # ReporteBase hereda de ABC — recibe todos sus atributos y métodos
    """
    Contrato base para todos los tipos de reporte.
    Cualquier clase que herede de ReporteBase DEBE implementar:
        - generar()
        - enviar()
        - nombre_formato (propiedad)
    """

    def __init__(self, titulo, datos):  # Constructor: inicializa el objeto con los datos recibidos
        self.titulo = titulo          # Común a todos los reportes
        self.datos  = datos           # Lista de registros a reportar

    # ── Métodos ABSTRACTOS: cada subclase los implementa a su manera ──

    @abstractmethod  # Decorador: obliga a las subclases a implementar este método
    def generar(self):  # Método que generar
        """Genera el contenido del reporte en el formato específico."""
        ...   # Los '...' indican que el cuerpo lo define la subclase

    @abstractmethod  # Decorador: obliga a las subclases a implementar este método
    def enviar(self, destino):  # Método que enviar
        """Envía el reporte generado al destino indicado."""
        ...  # Continúa la lógica del ejemplo paso a paso

    @property  # Decorador: permite acceder a este método como si fuera un atributo
    @abstractmethod  # Decorador: obliga a las subclases a implementar este método
    def nombre_formato(self):  # Método que nombre formato
        """Devuelve el nombre del formato (PDF, Excel, Email...)."""
        ...  # Continúa la lógica del ejemplo paso a paso

    # ── Método CONCRETO: ya tiene implementación, se hereda tal cual ──
    def encabezado(self):  # Método que encabezado
        # Este método funciona igual para TODOS los formatos
        sep = "═" * 48  # Asigna un valor inicial o calculado a sep
        return (  # Retorna el valor calculado al que llamó este método
            f"\n{sep}\n"  # Continúa la lógica del ejemplo paso a paso
            f"  📊 {self.titulo}\n"  # Continúa la lógica del ejemplo paso a paso
            f"  Formato : {self.nombre_formato}\n"  # Continúa la lógica del ejemplo paso a paso
            f"  Registros: {len(self.datos)}\n"  # Continúa la lógica del ejemplo paso a paso
            f"{sep}"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores


# ── SUBCLASE CONCRETA 1: Reporte en PDF ─────────────────────────
class ReportePDF(ReporteBase):  # ReportePDF hereda de ReporteBase — recibe todos sus atributos y métodos
    """Implementación concreta para reportes en formato PDF."""

    @property  # Decorador: permite acceder a este método como si fuera un atributo
    def nombre_formato(self):  # Método que nombre formato
        return "PDF"  # Retorna el valor calculado al que llamó este método

    def generar(self):  # Método que generar
        # Aquí iría la librería reportlab/fpdf en un proyecto real
        lineas = [f"  • {r['paquete']} → {r['destino']} [{r['estado']}]"  # Asigna un valor inicial o calculado a lineas
                  for r in self.datos]  # Itera sobre cada elemento de la colección indicada
        return self.encabezado() + "\n" + "\n".join(lineas)  # Retorna el valor calculado al que llamó este método

    def enviar(self, destino):  # Método que enviar
        return f"📤 PDF enviado a la ruta: {destino}/reporte.pdf"  # Retorna el valor calculado al que llamó este método


# ── SUBCLASE CONCRETA 2: Reporte en Excel ───────────────────────
class ReporteExcel(ReporteBase):  # ReporteExcel hereda de ReporteBase — recibe todos sus atributos y métodos
    """Implementación concreta para reportes en formato Excel."""

    @property  # Decorador: permite acceder a este método como si fuera un atributo
    def nombre_formato(self):  # Método que nombre formato
        return "Excel (.xlsx)"  # Retorna el valor calculado al que llamó este método

    def generar(self):  # Método que generar
        # En proyecto real usaría openpyxl o pandas
        encabezado_cols = "  PAQUETE          | DESTINO       | ESTADO"  # Asigna un valor inicial o calculado a encabezado_cols
        filas = [f"  {r['paquete']:<18}| {r['destino']:<14}| {r['estado']}"  # Asigna un valor inicial o calculado a filas
                 for r in self.datos]  # Itera sobre cada elemento de la colección indicada
        return self.encabezado() + f"\n{encabezado_cols}\n" + "\n".join(filas)  # Retorna el valor calculado al que llamó este método

    def enviar(self, destino):  # Método que enviar
        return f"📊 Excel guardado en: {destino}/reporte.xlsx"  # Retorna el valor calculado al que llamó este método


# ── SUBCLASE CONCRETA 3: Reporte por Correo ─────────────────────
class ReporteEmail(ReporteBase):  # ReporteEmail hereda de ReporteBase — recibe todos sus atributos y métodos
    """Implementación concreta para envío por correo electrónico."""

    @property  # Decorador: permite acceder a este método como si fuera un atributo
    def nombre_formato(self):  # Método que nombre formato
        return "Correo Electrónico"  # Retorna el valor calculado al que llamó este método

    def generar(self):  # Método que generar
        resumen = f"Total de envíos: {len(self.datos)}\n"  # Asigna un valor inicial o calculado a resumen
        pendientes = sum(1 for r in self.datos if r['estado'] == "Pendiente")  # Asigna un valor inicial o calculado a pendientes
        return self.encabezado() + f"\n  {resumen}  Pendientes: {pendientes}"  # Retorna el valor calculado al que llamó este método

    def enviar(self, destino):  # Método que enviar
        return f"✉️  Correo enviado a: {destino}"  # Retorna el valor calculado al que llamó este método


# ══════════════════ PROGRAMA PRINCIPAL ══════════════════
paquetes = [  # Asigna un valor inicial o calculado a paquetes
    {"paquete": "PKT-001", "destino": "Hermosillo", "estado": "Entregado"},  # Continúa la lógica del ejemplo paso a paso
    {"paquete": "PKT-002", "destino": "Nogales",    "estado": "En tránsito"},  # Continúa la lógica del ejemplo paso a paso
    {"paquete": "PKT-003", "destino": "Guaymas",    "estado": "Pendiente"},  # Continúa la lógica del ejemplo paso a paso
]  # Cierra la estructura abierta en las líneas anteriores

# Creamos los tres tipos de reporte
reportes = [  # Asigna un valor inicial o calculado a reportes
    ReportePDF  ("Envíos Sonora — Semana 18", paquetes),  # Continúa la lógica del ejemplo paso a paso
    ReporteExcel("Envíos Sonora — Semana 18", paquetes),  # Continúa la lógica del ejemplo paso a paso
    ReporteEmail("Envíos Sonora — Semana 18", paquetes),  # Continúa la lógica del ejemplo paso a paso
]  # Cierra la estructura abierta en las líneas anteriores

# ABSTRACCIÓN + POLIMORFISMO: el mismo código funciona con cualquier tipo
for rep in reportes:  # Itera sobre cada rep en reportes
    print(rep.generar())  # Muestra en consola el resultado
    print(rep.enviar("C:/reportes"))  # Muestra en consola el resultado
    print()  # Muestra en consola el resultado

# ── ¿Qué pasa si intentamos instanciar la clase abstracta? ──
# reporte = ReporteBase("X", [])   ← TypeError: Can't instantiate abstract class
# Python NO lo permite. ¡Eso es justamente la abstracción!
💡 Los 4 pilares en el orden correcto de aprendizaje:
• 🎭 1. Abstracción — Define QUÉ debe hacer un objeto (el contrato, la interfaz)
• 🔒 2. Encapsulamiento — Controla CÓMO se accede a los datos internos
• 🧬 3. Herencia — Reutiliza y extiende código de una clase padre en una clase hija
• 🎪 4. Polimorfismo — El mismo método se comporta diferente según el objeto

📖 Tema 2.5 — Documentación Profesional

🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
El estándar profesional de documentación de código Python: docstrings en módulos, clases y métodos con secciones de parámetros, tipos de retorno y ejemplos de uso.
🚀 ¿QUÉ LOGRAMOS?
Código auto-documentado que cualquier desarrollador puede entender sin necesidad de explicaciones adicionales, y que permite generar documentación HTML con pydoc.
🧠 ¿QUÉ APRENDEMOS?
La estructura de un docstring profesional: encabezado del módulo (autor, fecha, versión), docstring de clase (atributos, ejemplo), docstring de método (Args, Returns, Raises).
documentacion.py — Estándar de documentación UTH
"""
inventario_tienda.py
====================
Sistema de Inventario para Tienda de Ropa.
Módulo principal — Versión 1.0

Autor: Nombre del Alumno
Materia: Programación Orientada a Objetos
Docente: Bernardo Prado Díaz
Fecha: Mayo 2026
"""

class Producto:  # Define la clase Producto
    """
    Representa un producto en el inventario de la tienda.

    Atributos:
        nombre (str): Nombre descriptivo del producto.
        precio_base (float): Precio antes de impuestos.
        stock (int): Unidades disponibles.

    Ejemplo:
        >>> p = Producto("Playera", 200.0, 10)
        >>> p.precio_con_iva()
        232.0
    """

    def precio_con_iva(self) -> float:  # Método que precio con iva
        """
        Calcula el precio total incluyendo IVA del 16%.

        Returns:
            float: Precio base más 16% de IVA.
        """
        return self.precio_base * 1.16  # Retorna el valor calculado al que llamó este método

📋 Criterios de Evaluación — Unidad 2

CriterioEstratégicoAutónomoBásicoReceptivo
Clases y ObjetosImplementa correctamente con encapsulamiento, properties y validaciónClases correctas con encapsulamiento básicoClases funcionales sin encapsulamientoErrores de sintaxis o lógica
HerenciaUsa super(), herencia múltiple y MRO correctamenteHerencia simple con super() correctoHerencia básica sin super()Intenta herencia con errores
EncapsulamientoAtributos privados, properties con validación completaAtributos privados con getters/settersAtributos privados sin propertiesSin encapsulamiento
AbstracciónUsa ABC, @abstractmethod, propiedades abstractas y método concreto heredadoABC con @abstractmethod implementado en subclasesSimula abstracción con NotImplementedErrorNo implementa abstracción
PolimorfismoImplementa override, interfaces y duck typingOverride correcto en subclasesOverride básico en 1 métodoNo implementa polimorfismo
DocumentaciónDocstrings completos con parámetros, returns y ejemplosDocstrings con descripción y parámetrosDocstrings básicos descriptivosSin documentación
🎬

Videos de Apoyo — Unidad 2: Sintaxis y Pilares POO

Videos seleccionados para reforzar los conceptos de esta unidad. Míralos después de clase para afianzar lo aprendido.

Abstracción
Clases Abstractas y ABC en Python

abstractmethod, ABC, contratos entre clases con ejemplos reales

🕐 ~18 minES✓ Recomendado
Encapsulamiento
Encapsulamiento: @property, getters y setters

Atributos privados, protegidos y el decorador @property

🕐 ~22 minES✓ Recomendado
Herencia
Herencia Simple y Múltiple — super() y MRO

Cómo funciona la cadena de herencia y super() correctamente

🕐 ~26 minES✓ Recomendado
Polimorfismo
Polimorfismo y Duck Typing en Python

Override de métodos, interfaces implícitas, polimorfismo en acción

🕐 ~20 minES✓ Recomendado
🎥

Videos de Clase — Unidad 2 — 8 Sesiones

Grabaciones de las sesiones presenciales. Si faltaste o quieres repasar, estas grabaciones son tu mejor aliado. Se actualizan después de cada clase.

Sesión 1
Clase 1 — Clases, Objetos y Constructor

__init__, self, atributos, primer objeto en Python

S1📅 Por confirmar🕐 Próximamente
Sesión 2
Clase 2 — Pilar 1: Abstracción con ABC

Clases abstractas, @abstractmethod, contrato entre clases

S2📅 Por confirmar🕐 Próximamente
Sesión 3
Clase 3 — Pilar 2: Encapsulamiento y @property

Atributos privados, getters, setters, validación

S3📅 Por confirmar🕐 Próximamente
Sesión 4
Clase 4 — Pilar 3: Herencia Simple y super()

Herencia, super(), reutilización de código

S4📅 Por confirmar🕐 Próximamente
Sesión 5
Clase 5 — Herencia Múltiple y MRO

Resolución de orden de método, diamond problem

S5📅 Por confirmar🕐 Próximamente
Sesión 6
Clase 6 — Pilar 4: Polimorfismo y Duck Typing

Override, mismo método, comportamientos distintos

S6📅 Por confirmar🕐 Próximamente
Sesión 7
Clase 7 — Documentación Profesional con Docstrings

pydoc, docstrings de módulos, clases y métodos

S7📅 Por confirmar🕐 Próximamente
Sesión 8
Clase 8 — Repaso y Ejercicio Integrador U2

Proyecto integrador con los 4 pilares aplicados

S8📅 Por confirmar🕐 Próximamente
💡 ¿Cómo aprovecharlos? Pausa el video cada vez que el profesor escribe código, escríbelo tú mismo y luego compara. Los errores que cometes y resuelves son los que más aprendes.
💎 Ir a la Práctica Guiada — Unidad 2 →

🎨 Unidad 3 — FLET + MySQL: Interfaces Gráficas y Bases de Datos

Aprenderás a crear aplicaciones con interfaz gráfica usando FLET, conectar Python con MySQL, aplicar los principios SOLID y documentar tu proyecto profesionalmente.

⏱ 14 horas📊 Exámen y Prácticas en clase - Calificación🛠 FLET + MySQL + WampServer + DBeaver

📋 Temas de la Unidad

🎨

3.1 FLET — GUI Moderna

Instalación, componentes, eventos, diseño de interfaces.

🐬

3.2 MySQL con WampServer

Instalación, creación de BD, tablas, consultas SQL.

🔌

3.3 Python + MySQL OOP

Conexión, CRUD completo en clases, patrón DAO.

🏗️

3.4 Principios SOLID

5 principios para código limpio y mantenible.

📄

3.5 Documentación

Manual de usuario y guía de instalación profesional.

🗺️ Stack Tecnológico — Unidad 3: Lo que vamos a conectar
🐍
Python + POO
Clases, DAO
Lógica del negocio
+
🎨
FLET
Interfaz gráfica
Eventos, Controles
+
🐬
MySQL
WampServer
DBeaver
+
🏗️
SOLID
Principios de
diseño limpio
=
🏪
Sistema POS
Tienda Hermosillo
Completo y funcional

🔧 Instalación de WampServer 3.3.7

⚠️ Antes de instalar WampServer Asegúrate de tener instalado Microsoft Visual C++ Redistributable (x64). Si no lo tienes, WampServer no iniciará. Descárgalo desde la página oficial de Microsoft.
Descargar WampServer 3.3.7

Ve a wampserver.com → "Download" → selecciona la versión 64-bit para Windows 10/11. El archivo pesa aprox. 300MB.

Instalar WampServer

Ejecuta el instalador como Administrador (clic derecho → "Ejecutar como administrador"). Acepta la carpeta de destino predeterminada: C:\wamp64\. Completa el asistente.

Iniciar WampServer

Busca WampServer en el menú de inicio y ábrelo. En la barra de tareas (esquina inferior derecha) aparecerá un icono W. Espera a que cambie de rojo a naranja y finalmente a verde.

Verificar que funciona

Abre tu navegador y ve a http://localhost. Deberías ver la página de bienvenida de WampServer con links a phpMyAdmin y las versiones instaladas.

Configurar contraseña de MySQL

Haz clic en el icono W (barra de tareas) → MySQL → MySQL console. Se abrirá un CMD. Escribe:

MySQL Console
ALTER USER 'root'@'localhost' IDENTIFIED BY 'tu_password';
FLUSH PRIVILEGES;

Reemplaza tu_password por tu contraseña. Anótala bien.

🦦 Instalación de DBeaver 25.1.0

Descargar DBeaver

Ve a dbeaver.io/download → selecciona Community Edition → Windows installer (.exe).

Instalar DBeaver

Ejecuta el instalador. Acepta todos los valores por defecto. Se instalará en C:\Program Files\DBeaver\.

Conectar a MySQL local

Abre DBeaver → clic en "New Database Connection" (ícono de enchufe) → selecciona MySQL → configura:

  • Server Host: localhost
  • Port: 3306
  • Username: root
  • Password: (la que configuraste)

Clic en "Test Connection" → si aparece verde, ¡éxito!

Crear base de datos del proyecto

En el panel izquierdo, clic derecho en la conexión → "Create New Database" → nombre: pos_tienda → charset: utf8mb4 → OK.

🎨 Instalación y Primeros Pasos con FLET

🤔 ¿Qué es FLET? FLET es una librería Python que permite crear aplicaciones de escritorio y web con una interfaz visual moderna, usando el mismo código. Es mucho más fácil y moderno que Tkinter. Creado por el equipo de Flutter/Google.
Instalar FLET

Abre PyCharm → Terminal (menú View → Tool Windows → Terminal) y escribe:

Terminal PyCharm
pip install flet

Espera a que termine. Verás mensajes de descarga. Al final aparece Successfully installed flet-X.X.X.

Instalar mysql-connector-python
Terminal PyCharm
pip install mysql-connector-python
Tu primera app FLET — "Hola Mundo"

Crea un archivo hola_flet.py y copia este código:

🖼️ Arquitectura FLET — Cómo fluye una aplicación
👆
EVENTO
on_click
on_change
⚙️
FUNCIÓN
def guardar(e):
lógica...
🖥️
PÁGINA
page.add()
page.update()
ft.Page
├── ft.Column
├── ft.Text("¡Hola!")
└── ft.ElevatedButton("Click")
💡 ft.app(target=main) inicia todo — "page" es la ventana que administras
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
La estructura mínima de una aplicación FLET: la función main(page) recibe la ventana como parámetro, y ft.app() es el motor que la hace funcionar.
🚀 ¿QUÉ LOGRAMOS?
Una ventana de escritorio real con título, color de fondo, texto y un botón que responde al clic agregando mensajes dinámicamente en pantalla.
🧠 ¿QUÉ APRENDEMOS?
La estructura ft.app(target=main), los componentes ft.Text, ft.ElevatedButton y ft.Column, el parámetro on_click para manejar eventos, y page.add().
hola_flet.py — Tu primera aplicación con FLET
import flet as ft   # Importamos FLET con el alias "ft"

def main(page: ft.Page):  # Método que main
    """
    'page' es la VENTANA de la aplicación.
    Todo lo que pongas aquí aparecerá en pantalla.
    """
    page.title = "Mi Primera App FLET"    # Título de la ventana
    page.bgcolor = ft.colors.BLUE_GREY_50  # Color de fondo
    page.window_width = 500              # Ancho de la ventana en pixeles
    page.window_height = 400             # Alto de la ventana en pixeles

    # ft.Text crea un texto visible
    titulo = ft.Text(  # Asigna un valor inicial o calculado a titulo
        value="¡Hola, UTH! 🎓",           # El texto a mostrar
        size=32,                          # Tamaño de fuente
        color=ft.colors.BLUE_900,         # Color del texto
        weight=ft.FontWeight.BOLD         # Negrita
    )  # Cierra la estructura abierta en las líneas anteriores

    # ft.ElevatedButton crea un botón
    boton = ft.ElevatedButton(  # Asigna un valor inicial o calculado a boton
        text="Presióname 👋",  # Asigna un valor inicial o calculado a text
        on_click=lambda e: page.add(   # on_click = qué hacer al presionar
            ft.Text("¡Botón presionado! 🎉", color=ft.colors.GREEN_700)  # Completa la llamada o expresión iniciada anteriormente
        )  # Cierra la estructura abierta en las líneas anteriores
    )  # Cierra la estructura abierta en las líneas anteriores

    # page.add() agrega componentes a la ventana
    page.add(  # Inicia una llamada o estructura en varias líneas para mejorar la lectura
        ft.Column(                        # Column organiza elementos verticalmente
            controls=[titulo, boton],     # Lista de componentes
            horizontal_alignment=ft.CrossAxisAlignment.CENTER,  # Asigna un valor inicial o calculado a horizontal_alignment
            spacing=20                   # Espacio entre elementos
        )  # Cierra la estructura abierta en las líneas anteriores
    )  # Cierra la estructura abierta en las líneas anteriores

# ft.app() inicia la aplicación y llama a la función main
ft.app(target=main)  # Completa la llamada o expresión iniciada anteriormente
▶️ Para ejecutar: Haz clic derecho en el archivo en PyCharm → "Run hola_flet". Se abrirá una ventana de escritorio con tu aplicación.

🧩 Componentes Principales de FLET

ComponenteUsoEjemplo
ft.TextTexto en pantallaft.Text("Hola", size=20)
ft.TextFieldCampo de entradaft.TextField(label="Nombre")
ft.ElevatedButtonBotón principalft.ElevatedButton("OK", on_click=f)
ft.TextButtonBotón textoft.TextButton("Cancelar")
ft.DropdownLista desplegableft.Dropdown(options=[ft.dropdown.Option("A")])
ft.DataTableTabla de datosft.DataTable(columns=[...], rows=[...])
ft.ColumnColumna verticalft.Column([comp1, comp2])
ft.RowFila horizontalft.Row([comp1, comp2])
ft.ContainerContenedor con estiloft.Container(content=..., bgcolor=...)
ft.AlertDialogCuadro de diálogoft.AlertDialog(title=..., content=...)
ft.SnackBarMensaje temporalft.SnackBar(content=ft.Text("OK"))
ft.AppBarBarra de títuloft.AppBar(title=ft.Text("App"))

🐬 Tema 3.2 — Base de Datos MySQL para el Proyecto

🏪 Ejemplo de Negocio: Sistema de Punto de Venta (POS) — Tienda de Abarrotes

Crearemos un sistema POS para una tienda de abarrotes en Hermosillo. Tendrá gestión de productos, clientes y ventas con interfaz gráfica FLET y base de datos MySQL.

🖼️ Diagrama ER — Relaciones entre tablas del POS
🗂️ categorias
🔑 id (PK)
nombre
descripcion
1↔N
📦 productos
🔑 id (PK)
nombre, precio, stock
🔗 id_categoria (FK)
N↔N
🧾 detalle_venta
🔑 id (PK)
🔗 id_venta, id_producto
cantidad, precio_unit
N↔1
🛒 ventas
🔑 id (PK)
🔗 id_cliente (FK)
fecha, total
💡 PK = Llave Primaria (identificador único) | FK = Llave Foránea (referencia a otra tabla)
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
Cómo diseñar una base de datos relacional para un sistema POS: tablas normalizadas con llaves primarias, llaves foráneas y relaciones entre entidades del negocio.
🚀 ¿QUÉ LOGRAMOS?
La base de datos completa del sistema POS para la tienda de abarrotes con 5 tablas relacionadas, datos de prueba incluidos, lista para conectar con Python.
🧠 ¿QUÉ APRENDEMOS?
Sintaxis SQL: CREATE TABLE, tipos de datos (INT, VARCHAR, DECIMAL), restricciones (NOT NULL, AUTO_INCREMENT), y relaciones con FOREIGN KEY.
crear_bd.sql — Script SQL para crear la base de datos
-- Ejecuta este script en DBeaver o phpMyAdmin
-- Selecciona la BD 'pos_tienda' antes de ejecutar

USE pos_tienda;

-- Tabla de Categorías
CREATE TABLE categorias (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    nombre     VARCHAR(100) NOT NULL,
    descripcion TEXT
);

-- Tabla de Productos
CREATE TABLE productos (
    id           INT AUTO_INCREMENT PRIMARY KEY,
    nombre       VARCHAR(150) NOT NULL,
    precio       DECIMAL(10,2) NOT NULL,
    stock        INT DEFAULT 0,
    id_categoria INT,
    codigo_barra VARCHAR(50),
    FOREIGN KEY (id_categoria) REFERENCES categorias(id)
);

-- Tabla de Clientes
CREATE TABLE clientes (
    id       INT AUTO_INCREMENT PRIMARY KEY,
    nombre   VARCHAR(150) NOT NULL,
    telefono VARCHAR(20),
    email    VARCHAR(100)
);

-- Tabla de Ventas (encabezado)
CREATE TABLE ventas (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    id_cliente INT,
    fecha      DATETIME DEFAULT NOW(),
    total      DECIMAL(10,2),
    FOREIGN KEY (id_cliente) REFERENCES clientes(id)
);

-- Tabla de Detalle de Venta
CREATE TABLE detalle_venta (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    id_venta    INT,
    id_producto INT,
    cantidad    INT,
    precio_unit DECIMAL(10,2),
    FOREIGN KEY (id_venta) REFERENCES ventas(id),
    FOREIGN KEY (id_producto) REFERENCES productos(id)
);

-- Datos de prueba
INSERT INTO categorias (nombre) VALUES
    ('Abarrotes'), ('Bebidas'), ('Lácteos');

INSERT INTO productos (nombre, precio, stock, id_categoria) VALUES
    ('Arroz 1kg', 22.50, 100, 1),
    ('Refresco 600ml', 18.00, 60, 2),
    ('Leche 1L', 28.00, 40, 3);

🔌 Tema 3.3 — Conexión Python + MySQL con Clases (Patrón DAO)

💡 ¿Qué es el patrón DAO? Data Access Object. Es una clase dedicada SOLO a hablar con la base de datos. Así separamos la lógica del negocio del acceso a datos — uno de los principios SOLID.
🖼️ Arquitectura Python ↔ MySQL con Patrón Singleton
🐍
Python
ProductoDAO
VentaDAO
mysql-connector
Clase Conexion
🐬
MySQL
WampServer
localhost:3306
SQL queries
resultados dict
🗄️
BD: pos_tienda
productos
clientes
ventas
Singleton: Conexion() siempre devuelve la misma conexión
→ No se abren múltiples conexiones a la BD
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
Cómo aplicar el patrón Singleton a una clase de conexión MySQL: garantizar que toda la aplicación comparta una sola conexión, evitando sobrecarga en el servidor.
🚀 ¿QUÉ LOGRAMOS?
Una clase Conexion reutilizable que centraliza el acceso a MySQL, con métodos ejecutar() para modificaciones y consultar() para lecturas que devuelven diccionarios.
🧠 ¿QUÉ APRENDEMOS?
Usar mysql.connector.connect(), el parámetro dictionary=True para resultados como dicts, la importancia de commit() para guardar cambios, y cómo combinar Singleton con BD.
database.py — Clase de conexión a MySQL
import mysql.connector   # Importamos el conector de MySQL

class Conexion:  # Define la clase Conexion
    """
    Clase que maneja la conexión a MySQL.
    Usamos el patrón Singleton para que solo haya UNA conexión.
    """
    _instancia = None  # Asigna un valor inicial o calculado a _instancia

    def __new__(cls):  # Método que new
        if cls._instancia is None:  # Verifica cls._instancia is None antes de continuar
            cls._instancia = super().__new__(cls)  # Asigna un valor inicial o calculado a cls._instancia
            cls._instancia._conectar()  # Completa la llamada o expresión iniciada anteriormente
        return cls._instancia  # Retorna el valor calculado al que llamó este método

    def _conectar(self):  # Método que conectar
        # Establece la conexión con MySQL
        self.conn = mysql.connector.connect(  # Guarda conn como atributo de instancia
            host="localhost",      # El servidor (WampServer)
            user="root",           # Usuario MySQL
            password="tu_password",# Tu contraseña configurada
            database="pos_tienda"  # Nombre de la BD
        )  # Cierra la estructura abierta en las líneas anteriores
        self.cursor = self.conn.cursor(dictionary=True)  # Guarda cursor como atributo de instancia
        # dictionary=True → los resultados vienen como dicts {col: valor}

    def ejecutar(self, sql, params=None):  # Método que ejecutar
        # Ejecuta una instrucción SQL (INSERT, UPDATE, DELETE)
        self.cursor.execute(sql, params)  # Completa la llamada o expresión iniciada anteriormente
        self.conn.commit()         # Guarda los cambios en la BD

    def consultar(self, sql, params=None):  # Método que consultar
        # Ejecuta un SELECT y retorna los resultados
        self.cursor.execute(sql, params)  # Completa la llamada o expresión iniciada anteriormente
        return self.cursor.fetchall()  # Lista de diccionarios

    def cerrar(self):  # Método que cerrar
        self.cursor.close()  # Completa la llamada o expresión iniciada anteriormente
        self.conn.close()  # Completa la llamada o expresión iniciada anteriormente
        Conexion._instancia = None   # Limpia el singleton
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
El patrón DAO (Data Access Object): una clase cuya única responsabilidad es hablar con la base de datos — CRUD completo (Create, Read, Update, Delete) para productos.
🚀 ¿QUÉ LOGRAMOS?
Métodos listos para usar: obtener_todos(), insertar(), actualizar(), eliminar() y buscar_por_nombre(), con protección contra SQL Injection.
🧠 ¿QUÉ APRENDEMOS?
Separar la lógica de negocio del acceso a datos (Principio S de SOLID), usar %s como marcadores seguros en consultas SQL, y el patrón JOIN para obtener datos de múltiples tablas.
producto_dao.py — DAO para CRUD de Productos
from database import Conexion  # Importa Conexion del módulo database

class ProductoDAO:  # Define la clase ProductoDAO
    """
    DAO (Data Access Object) para la tabla 'productos'.
    TODA la lógica de base de datos de productos va aquí.
    Principio de Responsabilidad Única (SOLID-S):
    esta clase solo se encarga de acceso a datos.
    """

    def __init__(self):  # Constructor: inicializa el objeto con los datos recibidos
        self.db = Conexion()   # Obtenemos la conexión (singleton)

    def obtener_todos(self):  # Método que obtener todos
        # SELECT: obtiene todos los productos
        sql = "SELECT p.id, p.nombre, p.precio, p.stock, c.nombre AS categoria FROM productos p LEFT JOIN categorias c ON p.id_categoria = c.id"  # Asigna un valor inicial o calculado a sql
        return self.db.consultar(sql)  # Retorna el valor calculado al que llamó este método

    def insertar(self, nombre, precio, stock, id_categoria):  # Método que insertar
        # INSERT: agrega un nuevo producto
        # Usamos %s como marcadores de posición (evita SQL Injection)
        sql = "INSERT INTO productos (nombre, precio, stock, id_categoria) VALUES (%s, %s, %s, %s)"  # Asigna un valor inicial o calculado a sql
        self.db.ejecutar(sql, (nombre, precio, stock, id_categoria))  # Completa la llamada o expresión iniciada anteriormente

    def actualizar(self, id_prod, nombre, precio, stock):  # Método que actualizar
        # UPDATE: modifica un producto existente
        sql = "UPDATE productos SET nombre=%s, precio=%s, stock=%s WHERE id=%s"  # Asigna un valor inicial o calculado a sql
        self.db.ejecutar(sql, (nombre, precio, stock, id_prod))  # Completa la llamada o expresión iniciada anteriormente

    def eliminar(self, id_prod):  # Método que eliminar
        # DELETE: elimina un producto por ID
        sql = "DELETE FROM productos WHERE id = %s"  # Asigna un valor inicial o calculado a sql
        self.db.ejecutar(sql, (id_prod,))   # Nota la coma: es una tupla

    def buscar_por_nombre(self, texto):  # Método que buscar por nombre
        sql = "SELECT * FROM productos WHERE nombre LIKE %s"  # Asigna un valor inicial o calculado a sql
        return self.db.consultar(sql, (f"%{texto}%",))  # Retorna el valor calculado al que llamó este método
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
La integración completa de FLET + MySQL + POO: una interfaz gráfica que se conecta a la base de datos y permite gestionar productos con formularios y tabla de datos.
🚀 ¿QUÉ LOGRAMOS?
Una app POS real con ventana de 900×650 px, tabla que carga productos de MySQL, formulario de alta/edición, y botones de Guardar/Eliminar que persisten datos en la BD.
🧠 ¿QUÉ APRENDEMOS?
Usar ft.DataTable con filas dinámicas, capturar eventos de selección de fila para prellenar formularios, y el flujo INSERT vs UPDATE condicionado al estado de selección.
pos_app.py — Aplicación POS completa con FLET + MySQL
import flet as ft  # Importa el módulo flet para usar sus funciones y clases
from producto_dao import ProductoDAO  # Importa ProductoDAO del módulo producto_dao

def main(page: ft.Page):  # Método que main
    page.title = "🏪 POS Tienda Hermosillo"  # Asigna un valor inicial o calculado a page.title
    page.bgcolor = ft.colors.GREY_50  # Asigna un valor inicial o calculado a page.bgcolor
    page.window_width = 900  # Asigna un valor inicial o calculado a page.window_width
    page.window_height = 650  # Asigna un valor inicial o calculado a page.window_height
    page.window_center()  # Completa la llamada o expresión iniciada anteriormente

    dao = ProductoDAO()   # Creamos el DAO para acceder a la BD

    # ── Campos del formulario ──
    txt_nombre  = ft.TextField(label="Nombre del producto", expand=True)  # Asigna un valor inicial o calculado a txt_nombre
    txt_precio  = ft.TextField(label="Precio ($)", width=150)  # Asigna un valor inicial o calculado a txt_precio
    txt_stock   = ft.TextField(label="Stock", width=100)  # Asigna un valor inicial o calculado a txt_stock
    id_seleccionado = [None]   # Lista para guardar el ID seleccionado

    # ── Tabla de datos ──
    tabla = ft.DataTable(  # Asigna un valor inicial o calculado a tabla
        columns=[  # Asigna un valor inicial o calculado a columns
            ft.DataColumn(ft.Text("ID")),  # Continúa la lógica del ejemplo paso a paso
            ft.DataColumn(ft.Text("Nombre")),  # Continúa la lógica del ejemplo paso a paso
            ft.DataColumn(ft.Text("Precio")),  # Continúa la lógica del ejemplo paso a paso
            ft.DataColumn(ft.Text("Stock")),  # Continúa la lógica del ejemplo paso a paso
            ft.DataColumn(ft.Text("Categoría")),  # Continúa la lógica del ejemplo paso a paso
        ],  # Continúa la lógica del ejemplo paso a paso
        rows=[],   # Se llenará con datos de BD
        border=ft.border.all(1, ft.colors.GREY_300),  # Asigna un valor inicial o calculado a border
    )  # Cierra la estructura abierta en las líneas anteriores

    def cargar_productos(e=None):  # Método que cargar productos
        # Lee productos de MySQL y llena la tabla
        tabla.rows.clear()  # Completa la llamada o expresión iniciada anteriormente
        productos = dao.obtener_todos()  # Asigna un valor inicial o calculado a productos
        for p in productos:  # Itera sobre cada p en productos
            tabla.rows.append(ft.DataRow(  # Agrega un elemento al final de tabla.rows
                cells=[  # Asigna un valor inicial o calculado a cells
                    ft.DataCell(ft.Text(str(p["id"]))),  # Continúa la lógica del ejemplo paso a paso
                    ft.DataCell(ft.Text(p["nombre"])),  # Continúa la lógica del ejemplo paso a paso
                    ft.DataCell(ft.Text(f"${p['precio']:.2f}")),  # Continúa la lógica del ejemplo paso a paso
                    ft.DataCell(ft.Text(str(p["stock"]))),  # Continúa la lógica del ejemplo paso a paso
                    ft.DataCell(ft.Text(p["categoria"] or "—")),  # Continúa la lógica del ejemplo paso a paso
                ],  # Continúa la lógica del ejemplo paso a paso
                on_select_changed=lambda e, prod=p: seleccionar(prod)  # Asigna un valor inicial o calculado a on_select_changed
            ))  # Completa la llamada o expresión iniciada anteriormente
        page.update()   # Refresca la interfaz

    def seleccionar(prod):  # Método que seleccionar
        # Rellena el formulario con los datos del producto seleccionado
        txt_nombre.value  = prod["nombre"]  # Asigna un valor inicial o calculado a txt_nombre.value
        txt_precio.value  = str(prod["precio"])  # Asigna un valor inicial o calculado a txt_precio.value
        txt_stock.value   = str(prod["stock"])  # Asigna un valor inicial o calculado a txt_stock.value
        id_seleccionado[0] = prod["id"]  # Asigna un valor inicial o calculado a id_seleccionado[0]
        page.update()  # Completa la llamada o expresión iniciada anteriormente

    def guardar(e):  # Método que guardar
        if id_seleccionado[0]:  # Verifica id_seleccionado[0] antes de continuar
            # Si hay ID seleccionado → UPDATE
            dao.actualizar(id_seleccionado[0], txt_nombre.value,  # Continúa la lógica del ejemplo paso a paso
                           float(txt_precio.value), int(txt_stock.value))  # Completa la llamada o expresión iniciada anteriormente
        else:  # Si la condición anterior no se cumplió
            # Si no → INSERT
            dao.insertar(txt_nombre.value, float(txt_precio.value),  # Continúa la lógica del ejemplo paso a paso
                         int(txt_stock.value), 1)  # Completa la llamada o expresión iniciada anteriormente
        limpiar(None)  # Completa la llamada o expresión iniciada anteriormente
        cargar_productos()  # Completa la llamada o expresión iniciada anteriormente

    def eliminar(e):  # Método que eliminar
        if id_seleccionado[0]:  # Verifica id_seleccionado[0] antes de continuar
            dao.eliminar(id_seleccionado[0])  # Completa la llamada o expresión iniciada anteriormente
            limpiar(None)  # Completa la llamada o expresión iniciada anteriormente
            cargar_productos()  # Completa la llamada o expresión iniciada anteriormente

    def limpiar(e):  # Método que limpiar
        txt_nombre.value = txt_precio.value = txt_stock.value = ""  # Asigna un valor inicial o calculado a txt_nombre.value
        id_seleccionado[0] = None  # Asigna un valor inicial o calculado a id_seleccionado[0]
        page.update()  # Completa la llamada o expresión iniciada anteriormente

    # ── Botones ──
    btns = ft.Row([  # Asigna un valor inicial o calculado a btns
        ft.ElevatedButton("💾 Guardar",  on_click=guardar,  # Continúa la lógica del ejemplo paso a paso
                          bgcolor=ft.colors.BLUE_700, color=ft.colors.WHITE),  # Asigna un valor inicial o calculado a bgcolor
        ft.ElevatedButton("🗑 Eliminar", on_click=eliminar,  # Continúa la lógica del ejemplo paso a paso
                          bgcolor=ft.colors.RED_700,  color=ft.colors.WHITE),  # Asigna un valor inicial o calculado a bgcolor
        ft.ElevatedButton("🔄 Refrescar",on_click=cargar_productos),  # Continúa la lógica del ejemplo paso a paso
        ft.TextButton("✖ Limpiar", on_click=limpiar),  # Continúa la lógica del ejemplo paso a paso
    ])  # Completa la llamada o expresión iniciada anteriormente

    # ── AppBar (barra superior) ──
    page.appbar = ft.AppBar(  # Asigna un valor inicial o calculado a page.appbar
        title=ft.Text("Sistema POS — Tienda Hermosillo", color=ft.colors.WHITE),  # Asigna un valor inicial o calculado a title
        bgcolor=ft.colors.BLUE_900  # Asigna un valor inicial o calculado a bgcolor
    )  # Cierra la estructura abierta en las líneas anteriores

    # ── Layout principal ──
    page.add(  # Inicia una llamada o estructura en varias líneas para mejorar la lectura
        ft.Column([  # Continúa la lógica del ejemplo paso a paso
            ft.Text("📦 Gestión de Productos", size=18, weight=ft.FontWeight.BOLD),  # Continúa la lógica del ejemplo paso a paso
            ft.Row([txt_nombre, txt_precio, txt_stock]),  # Continúa la lógica del ejemplo paso a paso
            btns,  # Continúa la lógica del ejemplo paso a paso
            ft.Divider(),  # Continúa la lógica del ejemplo paso a paso
            ft.Text("Haz clic en una fila para seleccionar", size=12, color=ft.colors.GREY_600),  # Continúa la lógica del ejemplo paso a paso
            tabla,  # Continúa la lógica del ejemplo paso a paso
        ], scroll=ft.ScrollMode.AUTO)  # Completa la llamada o expresión iniciada anteriormente
    )  # Cierra la estructura abierta en las líneas anteriores

    cargar_productos()   # Carga datos al iniciar la app

ft.app(target=main)  # Completa la llamada o expresión iniciada anteriormente

🏗️ Tema 3.4 — Principios SOLID

S

S — Single Responsibility

Una clase = una responsabilidad. ProductoDAO solo accede a BD. ProductoService solo tiene lógica del negocio. No mezcles ambas.

O

O — Open/Closed

Abierto para extender, cerrado para modificar. Si necesitas un nuevo tipo de descuento, agrega una subclase, no modifiques la clase existente.

L

L — Liskov Substitution

Una subclase debe poder reemplazar a su padre. Si tienes Animal con método hablar(), todos sus hijos deben tenerlo funcionando.

I

I — Interface Segregation

Mejor muchas interfaces pequeñas que una grande. No obligues a una clase a implementar métodos que no usa.

D

D — Dependency Inversion

Depende de abstracciones, no de implementaciones. Tu app no debe depender de MySQL específicamente, sino de una interfaz genérica de BD.

📋 Criterios de Evaluación — Unidad 3

CriterioEstratégicoAutónomoBásicoReceptivo
GUI con FLETInterfaz profesional, validación completa, UX cuidadaInterfaz funcional con validaciones básicasInterfaz básica sin validacionesNo funciona o usa otra librería
MySQL OOPDAO completo con CRUD, manejo de errores, patrón SingletonCRUD funcional con clases bien definidasConexión y consultas básicasConexión sin OOP
Principios SOLIDAplica los 5 principios visiblemente en el códigoAplica S y O claramenteMenciona SOLID en comentariosNo aplica SOLID
DocumentaciónManual de usuario + guía de instalación profesionalManual de usuario completoREADME básicoSin documentación
🎬

Videos de Apoyo — Unidad 3: FLET + MySQL

Videos seleccionados para reforzar los conceptos de esta unidad. Míralos después de clase para afianzar lo aprendido.

FLET
FLET — Apps de Escritorio con Python

Primeros pasos: controles, eventos, páginas y navegación

🕐 ~30 minEN+SUB✓ Recomendado
MySQL
MySQL con Python — Conector y CRUD Completo

Instalar mysql-connector, conectar, INSERT, UPDATE, DELETE

🕐 ~35 minES✓ Recomendado
SOLID
Principios SOLID — Ejemplos en Python

SRP, OCP, LSP, ISP, DIP aplicados a código orientado a objetos

🕐 ~28 minES✓ Recomendado
WampServer
Instalar WampServer y configurar MySQL local

Guía paso a paso para tener MySQL en Windows con WampServer

🕐 ~12 minES✓ Recomendado
🎥

Videos de Clase — Unidad 3 — 7 Sesiones

Grabaciones de las sesiones presenciales. Si faltaste o quieres repasar, estas grabaciones son tu mejor aliado. Se actualizan después de cada clase.

DSM3-1
Sesión 1
Clase 1 — Instalación FLET y Primera App
DSM3-1

pip install flet, estructura de proyecto, primer ft.app()

S1📅 Confirmado23 de Junio 2026
DSM3-2
Sesión 1
Clase 1 — Instalación FLET y Primera App
DSM3-2

pip install flet, estructura de proyecto, primer ft.app()

S1📅 Confirmado23 de Junio 2026
Sesión 2
Clase 2 — Controles FLET: Botones, Inputs, Listas
DSM3-1

ElevatedButton, TextField, ListView, Row, Column

S2📅 Por confirmar🕐 Próximamente
Sesión 3
Clase 3 — Instalación MySQL y WampServer

Configuración de MySQL local, DBeaver, conexión inicial

S3📅 Por confirmar🕐 Próximamente
Sesión 4
Clase 4 — CRUD MySQL con Python OOP

Clase Conexion, DAOs, INSERT/SELECT/UPDATE/DELETE

S4📅 Por confirmar🕐 Próximamente
Sesión 5
Clase 5 — Conectar FLET con MySQL

DAO + interfaz gráfica: CRUD completo con FLET

S5📅 Por confirmar🕐 Próximamente
Sesión 6
Clase 6 — Principios SOLID en el Proyecto

Refactorizar aplicando SRP, OCP y LSP

S6📅 Por confirmar🕐 Próximamente
Sesión 7
Clase 7 — Documentación y Cierre U3

Manual de usuario, guía de instalación, entrega final

S7📅 Por confirmar🕐 Próximamente
💡 ¿Cómo aprovecharlos? Pausa el video cada vez que el profesor escribe código, escríbelo tú mismo y luego compara. Los errores que cometes y resuelves son los que más aprendes.
⚙️ Ir a la Práctica Guiada — Unidad 3 → 📋 Requisitos y Fechas de Entrega — Examen U3 →

🚀 Unidad 4 — Entornos de Desarrollo, Git y Proyecto Final

Dominarás PyCharm como IDE profesional, aprenderás Git para control de versiones, manejarás excepciones correctamente, harás pruebas unitarias y entregarás tu Proyecto Final POO completo.

⏱ 14 horas📊 Exposición de Conocimiento ante Maestros - Calificación🛠 PyCharm + Git + MySQL

📋 Temas de la Unidad

💡

4.1 PyCharm Avanzado

Atajos, depurador, refactorización, virtual environments.

🔀

4.2 Git y Control de Versiones

init, add, commit, branches, push, GitHub.

🛡️

4.3 Manejo de Excepciones

try/except/finally, excepciones personalizadas.

🧪

4.4 Pruebas Unitarias

unittest, assert, casos de prueba.

🏆

4.5 Proyecto Final

Sistema completo: GUI + CRUD + reportes + excepciones + documentación.

🗺️ Flujo de Trabajo Profesional — Unidad 4
💡
PyCharm Pro
Debug, atajos
Refactoring
🔀
Git + GitHub
Commits, ramas
Historial
🛡️
Excepciones
try/except
Errores propios
🧪
Pruebas
unittest
TDD
🏆
Proyecto Final
Sistema completo
Documentado

🔀 Guía de Instalación y Uso de Git

🤔 ¿Qué es Git? Git es un sistema que guarda el historial de cambios de tu código. Es como un "Ctrl+Z" infinito con comentarios. Todas las empresas de software lo usan. GitHub es la plataforma web que almacena tus repositorios Git.
Instalar Git

Ve a git-scm.com/download/win → descarga el instalador → ejecuta → acepta todas las opciones por defecto (son las recomendadas).

Configurar tu identidad

Abre Git Bash (se instala con Git) y configura tu nombre y email:

Git Bash
git config --global user.name "Tu Nombre"
git config --global user.email "tu@email.com"
Crear cuenta en GitHub

Ve a github.com → "Sign up" → usa tu email institucional UTH.

Iniciar repositorio en tu proyecto
Terminal (carpeta de tu proyecto)
git init                    # Inicia el repositorio
git add .                   # Agrega TODOS los archivos
git commit -m "Primer commit del proyecto POO"   # Guarda el estado

📋 Comandos Git Esenciales

Comando¿Qué hace?Ejemplo
git initCrea un repositorio nuevogit init mi-proyecto
git statusMuestra qué archivos cambiarongit status
git addPrepara archivos para commitgit add archivo.py o git add .
git commitGuarda una versión con mensajegit commit -m "Agrega módulo de ventas"
git logHistorial de commitsgit log --oneline
git branchLista o crea ramasgit branch nueva-feature
git checkoutCambia de ramagit checkout nueva-feature
git mergeUne ramasgit merge nueva-feature
git pushSube cambios a GitHubgit push origin main
git pullDescarga cambios de GitHubgit pull origin main
git cloneClona repositorio remotogit clone https://github.com/user/repo.git

💡 Tema 4.1 — PyCharm: Atajos y Funciones Profesionales

AcciónAtajo Windows¿Para qué?
Ejecutar programaShift+F10Corre el archivo actual
DepuradorShift+F9Ejecuta en modo debug (con breakpoints)
Agregar breakpointF9 (clic en margen)Pausa la ejecución en esa línea
Buscar en proyectoCtrl+Shift+FBusca texto en todos los archivos
Ir a definiciónCtrl+BSalta a donde está definida la función/clase
Renombrar símboloShift+F6Renombra en todos los archivos a la vez
AutocompletarCtrl+SpaceMuestra sugerencias de código
Comentar líneaCtrl+/Comenta/descomenta la línea actual
Formatear códigoCtrl+Alt+LAplica el estilo PEP 8 automáticamente
Terminal integradaAlt+F12Abre terminal dentro de PyCharm
Buscar claseCtrl+NBusca una clase por nombre
Ver estructuraAlt+7Panel con métodos y atributos de la clase

Virtual Environment (Entorno Virtual)

💡 ¿Para qué sirve un venv? Un entorno virtual aísla las librerías de tu proyecto. Así dos proyectos pueden usar versiones diferentes de la misma librería sin conflictos. ¡Siempre usa uno!
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
Por qué los entornos virtuales son esenciales: aislar las dependencias de cada proyecto para que no interfieran entre sí, como si cada proyecto tuviera su propia "cajita" de librerías.
🚀 ¿QUÉ LOGRAMOS?
Un entorno virtual aislado con FLET y mysql-connector instalados, y un archivo requirements.txt que permite reproducir el entorno exacto en cualquier otra computadora.
🧠 ¿QUÉ APRENDEMOS?
Los 5 pasos del flujo de trabajo profesional: crear venv → activar → instalar librerías → programar → pip freeze para documentar dependencias. Práctica usada en toda la industria.
Terminal — Crear y usar entorno virtual
# 1. Crear el entorno virtual (ejecuta una sola vez)
python -m venv venv

# 2. Activarlo (cada vez que abras el proyecto)
venv\Scripts\activate          # Windows

# Verás el prefijo (venv) en el terminal, significa que está activo

# 3. Instalar librerías dentro del venv
pip install flet mysql-connector-python

# 4. Guardar lista de dependencias
pip freeze > requirements.txt

# 5. Para instalar en otro equipo:
pip install -r requirements.txt

🛡️ Tema 4.3 — Manejo de Excepciones

⚠️ ¿Qué es una excepción? Es un error que ocurre en tiempo de ejecución (cuando el programa ya está corriendo). Si no la manejas, el programa se detiene con un mensaje de error feo. Con try/except puedes capturarla y responder de forma elegante.
🖼️ Flujo de Manejo de Excepciones
try:
Código que
puede fallar
except ValueError: texto en lugar de número
except ZeroDivisionError: división entre 0
except Exception as e: cualquier otro error
else: solo si no hubo error
finally: SIEMPRE se ejecuta
💡 Analogía: Como el protocolo de emergencias de un hospital en Hermosillo — hay un procedimiento para cada tipo de problema, y la limpieza (finally) se hace siempre
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
Cómo capturar errores en tiempo de ejecución con try/except/else/finally y cómo crear excepciones personalizadas que comunican errores específicos del negocio.
🚀 ¿QUÉ LOGRAMOS?
Un programa que nunca colapsa inesperadamente: responde con mensajes claros al usuario, registra el error, y continúa o cierra de forma ordenada sin mostrar trazas técnicas.
🧠 ¿QUÉ APRENDEMOS?
La jerarquía de excepciones de Python (ValueError, ZeroDivisionError heredan de Exception), cómo crear StockInsuficienteError personalizada y manejar errores de conexión MySQL.
excepciones.py — Manejo completo de excepciones
# ── Estructura básica de try/except ──
try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
    # Código que PUEDE fallar va aquí
    numero = int(input("Ingresa un número: "))  # Asigna un valor inicial o calculado a numero
    resultado = 100 / numero  # Asigna un valor inicial o calculado a resultado
    print(f"Resultado: {resultado}")  # Muestra en consola el resultado

except ValueError:  # Captura el error ValueError si ocurre en el try
    # Se ejecuta si el usuario escribe texto en lugar de número
    print("❌ Error: debes ingresar un número entero.")  # Muestra en consola el resultado

except ZeroDivisionError:  # Captura el error ZeroDivisionError si ocurre en el try
    # Se ejecuta si el usuario escribe 0
    print("❌ Error: no se puede dividir entre cero.")  # Muestra en consola el resultado

except Exception as e:  # Captura el error Exception as e si ocurre en el try
    # Captura CUALQUIER otro error inesperado
    print(f"❌ Error inesperado: {e}")  # Muestra en consola el resultado

else:  # Si la condición anterior no se cumplió
    # Se ejecuta SOLO si NO hubo error
    print("✅ Operación exitosa.")  # Muestra en consola el resultado

finally:  # Se ejecuta SIEMPRE, haya o no error (para limpieza de recursos)
    # Se ejecuta SIEMPRE, haya o no error (útil para cerrar BD, archivos)
    print("ℹ️ Fin de la operación.")  # Muestra en consola el resultado


# ── Excepciones personalizadas ──
class StockInsuficienteError(Exception):  # StockInsuficienteError hereda de Exception — recibe todos sus atributos y métodos
    """Error propio de nuestra aplicación."""
    def __init__(self, producto, cantidad_pedida, stock_actual):  # Constructor: inicializa el objeto con los datos recibidos
        self.producto = producto  # Guarda producto como atributo de instancia
        mensaje = (  # Asigna un valor inicial o calculado a mensaje
            f"Stock insuficiente para '{producto}'. "  # Continúa la lógica del ejemplo paso a paso
            f"Pedido: {cantidad_pedida}, Disponible: {stock_actual}"  # Continúa la lógica del ejemplo paso a paso
        )  # Cierra la estructura abierta en las líneas anteriores
        super().__init__(mensaje)  # Llama al constructor de la clase padre para inicializar los atributos heredados


class ProductoService:  # Define la clase ProductoService
    def vender(self, producto, cantidad, stock):  # Método que vender
        if cantidad > stock:  # Verifica cantidad > stock antes de continuar
            raise StockInsuficienteError(producto, cantidad, stock)  # Lanza una excepción si hay un error; detiene el flujo normal
        return stock - cantidad  # Retorna el valor calculado al que llamó este método


# Uso con manejo de la excepción propia:
svc = ProductoService()  # Asigna un valor inicial o calculado a svc
try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
    nuevo_stock = svc.vender("Arroz 1kg", 50, 10)  # Asigna un valor inicial o calculado a nuevo_stock
except StockInsuficienteError as e:  # Captura el error StockInsuficienteError as e si ocurre en el try
    print(f"❌ {e}")   # Muestra el mensaje personalizado


# ── Excepciones en conexión MySQL ──
import mysql.connector  # Importa el módulo mysql.connector para usar sus funciones y clases

def conectar_bd():  # Método que conectar bd
    try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
        conn = mysql.connector.connect(  # Asigna un valor inicial o calculado a conn
            host="localhost", user="root",  # Asigna un valor inicial o calculado a host
            password="pass", database="pos_tienda"  # Asigna un valor inicial o calculado a password
        )  # Cierra la estructura abierta en las líneas anteriores
        return conn  # Retorna el valor calculado al que llamó este método
    except mysql.connector.Error as e:  # Captura el error mysql.connector.Error as e si ocurre en el try
        print(f"❌ No se pudo conectar a MySQL: {e}")  # Muestra en consola el resultado
        return None  # Retorna el valor calculado al que llamó este método

🧪 Tema 4.4 — Pruebas Unitarias con unittest

💡 ¿Para qué sirven las pruebas? Una prueba unitaria verifica que cada función/método haga lo que se supone. Así cuando cambias código, las pruebas te avisan si algo dejó de funcionar. Es como un sistema de alarma para tu código.
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
El framework unittest de Python: cómo estructurar una suite de pruebas con setUp(), múltiples métodos test_*() y assertions que verifican el comportamiento esperado.
🚀 ¿QUÉ LOGRAMOS?
5 pruebas automatizadas que verifican: cálculo de IVA, venta exitosa, stock insuficiente, validación de precio negativo y herencia de Ropa. Se ejecutan con un solo comando.
🧠 ¿QUÉ APRENDEMOS?
assertEqual, assertIn, assertAlmostEqual, assertRaises e assertIsInstance — los 5 assertions más comunes. Y el ciclo: escribir prueba → ejecutar → corregir código.
test_producto.py — Pruebas unitarias completas
import unittest  # Importa el módulo unittest para usar sus funciones y clases
from inventario_tienda import Producto, Ropa   # Importamos las clases a probar

class TestProducto(unittest.TestCase):  # TestProducto hereda de unittest.TestCase — recibe todos sus atributos y métodos
    """
    Clase de pruebas. Hereda de unittest.TestCase.
    Cada método que empiece con 'test_' es una prueba.
    """

    def setUp(self):  # Se ejecuta automáticamente antes de cada prueba para preparar el entorno
        # setUp() se ejecuta ANTES de cada prueba
        # Aquí creamos objetos que necesitaremos en las pruebas
        self.prod = Producto("Arroz", 20.0, 10)  # Guarda prod como atributo de instancia

    def test_precio_con_iva(self):  # Prueba unitaria: verifica que precio con iva funcione correctamente
        # Verifica que el cálculo de IVA sea correcto
        resultado = self.prod.precio_con_iva()  # Asigna un valor inicial o calculado a resultado
        self.assertAlmostEqual(resultado, 23.2, places=2)  # Verifica que ambos valores sean casi iguales (útil con decimales)

    def test_vender_exitoso(self):  # Prueba unitaria: verifica que vender exitoso funcione correctamente
        # Verifica que vender 3 unidades funcione
        resultado = self.prod.vender(3)  # Asigna un valor inicial o calculado a resultado
        self.assertIn("✅", resultado)         # Debe contener ✅
        self.assertEqual(self.prod.stock, 7)  # Stock debe quedar en 7

    def test_vender_stock_insuficiente(self):  # Prueba unitaria: verifica que vender stock insuficiente funcione correctamente
        # Verifica que vender más del stock muestre error
        resultado = self.prod.vender(100)  # Asigna un valor inicial o calculado a resultado
        self.assertIn("❌", resultado)  # Verifica que el elemento esperado esté dentro de la colección

    def test_precio_negativo_lanza_error(self):  # Prueba unitaria: verifica que precio negativo lanza error funcione correctamente
        # Verifica que precio negativo lance ValueError
        with self.assertRaises(ValueError):  # Abre un contexto controlado para usar y cerrar recursos automáticamente
            self.prod.precio_base = -100   # Debe lanzar excepción

    def test_herencia_ropa(self):  # Prueba unitaria: verifica que herencia ropa funcione correctamente
        # Verifica que Ropa herede correctamente
        ropa = Ropa("Playera", 250.0, 5, "M", "Blanco")  # Asigna un valor inicial o calculado a ropa
        self.assertIsInstance(ropa, Producto)  # Ropa ES un Producto
        self.assertEqual(ropa.talla, "M")  # Verifica que ambos valores sean iguales; falla el test si son diferentes

    def tearDown(self):  # Se ejecuta automáticamente después de cada prueba para limpiar
        # tearDown() se ejecuta DESPUÉS de cada prueba (limpieza)
        pass  # Cuerpo vacío por ahora; aquí irá la lógica en las subclases


# Ejecutar pruebas:
if __name__ == "__main__":  # Se ejecuta solo cuando este archivo se corre directamente (no cuando se importa)
    unittest.main(verbosity=2)   # verbosity=2 muestra detalle de cada prueba

🏆 Tema 4.5 — Proyecto Final POO

🏢 Ejemplo de Negocio: Sistema de Administración para Empresa Local de Hermosillo

El proyecto final integra TODO lo del curso: clases POO, FLET, MySQL, SOLID, excepciones y pruebas. Elige un negocio local (restaurant, farmacia, ferretería, etc.) y crea un sistema completo de administración.

📋 Requisitos del Proyecto Final
✅ Mínimo 5 clases con POO (herencia, encapsulamiento, polimorfismo)
✅ Interfaz gráfica con FLET (pantallas de login, menú y módulos)
✅ Base de datos MySQL con mínimo 4 tablas relacionadas
✅ CRUD completo para al menos 2 entidades
✅ Manejo de excepciones en toda la app
✅ Mínimo 10 pruebas unitarias
✅ Documentación: diagrama de clases UML + manual de usuario
✅ Repositorio Git con historial de commits

Estructura recomendada del proyecto

🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
La estructura profesional de un proyecto Python POO: cómo organizar el código en carpetas (models/, dao/, ui/, services/, tests/) siguiendo el principio de responsabilidad única.
🚀 ¿QUÉ LOGRAMOS?
Una plantilla de proyecto lista para usar en tu proyecto final: cada archivo tiene su lugar específico, lo que facilita encontrar código, trabajar en equipo y agregar nuevas funciones.
🧠 ¿QUÉ APRENDEMOS?
El patrón de arquitectura en capas: UI → Services → DAO → BD. Cada capa tiene una responsabilidad y solo se comunica con la capa adyacente — base del diseño de software profesional.
estructura_proyecto.txt — Organización de archivos
mi_sistema_poo/
│
├── main.py              ← Punto de entrada de la aplicación
│
├── database/
│   ├── __init__.py
│   ├── conexion.py      ← Clase Singleton de conexión MySQL
│   └── script.sql       ← Script para crear la BD
│
├── models/              ← Clases del negocio (POO)
│   ├── __init__.py
│   ├── producto.py
│   ├── cliente.py
│   └── venta.py
│
├── dao/                 ← Clases DAO (acceso a BD)
│   ├── __init__.py
│   ├── producto_dao.py
│   ├── cliente_dao.py
│   └── venta_dao.py
│
├── ui/                  ← Vistas FLET
│   ├── __init__.py
│   ├── login_view.py
│   ├── menu_view.py
│   ├── productos_view.py
│   └── ventas_view.py
│
├── services/            ← Lógica del negocio (SOLID-S)
│   ├── __init__.py
│   └── venta_service.py
│
├── tests/               ← Pruebas unitarias
│   ├── __init__.py
│   ├── test_producto.py
│   └── test_venta.py
│
├── docs/                ← Documentación
│   ├── manual_usuario.pdf
│   ├── guia_instalacion.pdf
│   └── diagrama_clases.drawio
│
├── requirements.txt     ← pip freeze > requirements.txt
└── README.md            ← Descripción del proyecto
🎯 Objetivo del Ejercicio
📌 ¿QUÉ DEMOSTRAMOS?
El punto de entrada completo del proyecto final: integración de FLET, POO, MySQL, manejo de excepciones y navegación entre pantallas (Login → Menú) en un solo archivo main.py.
🚀 ¿QUÉ LOGRAMOS?
Una aplicación robusta que verifica la conexión a MySQL al arrancar, muestra la pantalla de login, y navega al menú principal — con manejo elegante de errores de conexión.
🧠 ¿QUÉ APRENDEMOS?
Cómo estructurar el main.py de una app real: encabezado de documentación, importaciones, función main(page) con navegación, y try/except para errores de inicio.
main.py — Punto de entrada de la aplicación final
"""
Sistema de Administración — Proyecto Final POO
Universidad Tecnológica de Hermosillo
Materia: Programación Orientada a Objetos
Docente: Bernardo Prado Díaz
Alumno: [Tu Nombre]
Fecha: Mayo–Agosto 2026
"""

import flet as ft  # Importa el módulo flet para usar sus funciones y clases
from ui.login_view import LoginView  # Importa LoginView del módulo ui.login_view
from ui.menu_view import MenuView  # Importa MenuView del módulo ui.menu_view
from database.conexion import Conexion  # Importa Conexion del módulo database.conexion

def main(page: ft.Page):  # Método que main
    page.title = "Sistema de Administración UTH"  # Asigna un valor inicial o calculado a page.title
    page.bgcolor = ft.colors.BLUE_GREY_50  # Asigna un valor inicial o calculado a page.bgcolor
    page.window_width = 1200  # Asigna un valor inicial o calculado a page.window_width
    page.window_height = 750  # Asigna un valor inicial o calculado a page.window_height
    page.window_center()  # Completa la llamada o expresión iniciada anteriormente

    def ir_a_menu(usuario):  # Método que ir a menu
        # Función para navegar del login al menú principal
        page.clean()  # Completa la llamada o expresión iniciada anteriormente
        page.add(MenuView(page, usuario))  # Completa la llamada o expresión iniciada anteriormente
        page.update()  # Completa la llamada o expresión iniciada anteriormente

    try:  # Intenta ejecutar el bloque; si hay error, lo captura el except
        db = Conexion()    # Verificamos la conexión a BD al iniciar
        page.add(LoginView(page, ir_a_menu))  # Completa la llamada o expresión iniciada anteriormente
    except Exception as e:  # Captura el error Exception as e si ocurre en el try
        # Si no hay conexión, mostramos error en vez de colapsar
        page.add(ft.Column([  # Continúa la lógica del ejemplo paso a paso
            ft.Icon(ft.icons.ERROR_OUTLINE, size=60, color=ft.colors.RED),  # Continúa la lógica del ejemplo paso a paso
            ft.Text("No se pudo conectar a la base de datos", size=20),  # Continúa la lógica del ejemplo paso a paso
            ft.Text(str(e), size=13, color=ft.colors.GREY_600),  # Continúa la lógica del ejemplo paso a paso
            ft.ElevatedButton("Reintentar", on_click=lambda _: page.clean()),  # Continúa la lógica del ejemplo paso a paso
        ], horizontal_alignment=ft.CrossAxisAlignment.CENTER))  # Completa la llamada o expresión iniciada anteriormente

    page.update()  # Completa la llamada o expresión iniciada anteriormente

ft.app(target=main)  # Completa la llamada o expresión iniciada anteriormente

📄 Plantilla de README.md Profesional

README.md — Plantilla para tu proyecto final
# 🏢 Sistema de Administración [Nombre del Negocio]

## Descripción
Sistema de gestión desarrollado como proyecto final de la asignatura
**Programación Orientada a Objetos** en la Universidad Tecnológica de Hermosillo.

**Alumno:** [Tu Nombre]
**Docente:** Bernardo Prado Díaz
**Periodo:** Mayo–Agosto 2026
**Carrera:** TIID

## Tecnologías
- Python 3.12+
- FLET (GUI)
- MySQL / WampServer 3.3.7
- DBeaver 25.1.0
- Git

## Características
- ✅ CRUD completo de productos y clientes
- ✅ Módulo de ventas con reportes
- ✅ Autenticación de usuarios
- ✅ Manejo de excepciones personalizado
- ✅ Pruebas unitarias

## Instalación
1. Clonar: `git clone [URL]`
2. Activar venv: `venv\Scripts\activate`
3. Instalar: `pip install -r requirements.txt`
4. Ejecutar SQL: abrir `database/script.sql` en DBeaver
5. Configurar contraseña en `database/conexion.py`
6. Ejecutar: `python main.py`

## Estructura del Proyecto
[ver documentación técnica en /docs]

📋 Criterios de Evaluación — Unidad 4 y Proyecto Final

CriterioEstratégicoAutónomoBásicoReceptivo
GitRepositorio con ramas, commits descriptivos, README profesionalCommits regulares con mensajes descriptivosRepositorio con commits inicialesEntrega sin control de versiones
ExcepcionesExcepciones propias, try/except en toda la app, loggingManeja excepciones MySQL y de usuariotry/except básico en operaciones claveSin manejo de excepciones
Pruebas Unitarias+15 pruebas con setUp, tearDown y casos límite10+ pruebas funcionales5+ pruebas básicasMenos de 3 pruebas
Proyecto FinalSistema completo, profesional, documentado, con pruebas y GitSistema funcional con la mayoría de requisitosSistema parcial con funcionalidad básicaProyecto incompleto o no funcional
DocumentaciónUML + manual usuario + guía instalación + comentarios de códigoUML y manual de usuarioREADME y comentarios básicosSin documentación
🎬

Videos de Apoyo — Unidad 4: Git, PyCharm y Proyecto Final

Videos seleccionados para reforzar los conceptos de esta unidad. Míralos después de clase para afianzar lo aprendido.

Git
Git y GitHub desde Cero — Guía Completa

init, add, commit, push, pull, ramas y flujo profesional

🕐 ~40 minES✓ Recomendado
PyCharm
PyCharm — Configuración y Productividad

Atajos, depurador, refactorización y plugins esenciales

🕐 ~22 minES✓ Recomendado
unittest
Pruebas Unitarias en Python con unittest

TestCase, assertEqual, setUp, tearDown — testing profesional

🕐 ~25 minES✓ Recomendado
Excepciones
Manejo de Excepciones — try/except/finally

Excepciones propias, jerarquía de errores, logging

🕐 ~20 minES✓ Recomendado
🎥

Videos de Clase — Unidad 4 — 8 Sesiones

Grabaciones de las sesiones presenciales. Si faltaste o quieres repasar, estas grabaciones son tu mejor aliado. Se actualizan después de cada clase.

Sesión 1
Clase 1 — Git: Instalación y Primeros Commits

git init, git add, git commit, .gitignore, primer repositorio

S1📅 Por confirmar🕐 Próximamente
Sesión 2
Clase 2 — GitHub: Ramas y Pull Requests

git branch, merge, conflictos, GitHub Desktop

S2📅 Por confirmar🕐 Próximamente
Sesión 3
Clase 3 — PyCharm Profesional: Debugger y Refactor

Breakpoints, inspeccionar variables, refactorizar código

S3📅 Por confirmar🕐 Próximamente
Sesión 4
Clase 4 — Manejo de Excepciones Propias

try/except/finally, crear excepciones personalizadas

S4📅 Por confirmar🕐 Próximamente
Sesión 5
Clase 5 — Pruebas Unitarias con unittest

TestCase, assertEqual, setUp/tearDown, estructura de pruebas

S5📅 Por confirmar🕐 Próximamente
Sesión 6
Clase 6 — Proyecto Final: Planificación y Arquitectura

Selección del sistema, UML, estructura de carpetas

S6📅 Por confirmar🕐 Próximamente
Sesión 7
Clase 7 — Proyecto Final: Desarrollo Parte 1

Modelos, clases, base de datos — primeras pantallas FLET

S7📅 Por confirmar🕐 Próximamente
Sesión 8
Clase 8 — Proyecto Final: Entrega y Presentación

Demo del sistema, documentación final, rúbrica de evaluación

S8📅 Por confirmar🕐 Próximamente
💡 ¿Cómo aprovecharlos? Pausa el video cada vez que el profesor escribe código, escríbelo tú mismo y luego compara. Los errores que cometes y resuelves son los que más aprendes.

📎 Apoyo, Requisitos y Herramientas

Documentos oficiales de la asignatura: planeación didáctica, plan académico, calendarios de grupos y escolar, y el reglamento del curso. Consúltalos antes de cada actividad.

🏫 UTH – TIID📅 2026-2👨‍🏫 Bernardo Prado Díaz
📐
Herramienta interactiva

📐 Repositorio de Diagramas Entidad-Relación

Sube la imagen o el PDF de tu diagrama E-R: escribe tu nombre completo (apellidos primero), elige tu grupo y súbelo. Una vez subido queda fijo — nadie puede editarlo ni borrarlo.

📤 Subida de archivos 🔒 Sin edición ni borrado
Planeacion Didactica

📘 Planeación Didáctica POO 2026-2

Documento oficial que define objetivos, contenidos, estrategias de enseñanza, recursos y criterios de evaluación de la asignatura para el periodo Mayo–Agosto 2026.

📄 Documento oficial🎓 4 Unidades
Contenido: Datos de identificación, propósito, competencias por unidad, actividades de aprendizaje, recursos didácticos y bibliografía.
Plan Academico

🎓 Plan Académico POO

Plan académico oficial de la asignatura Programación Orientada a Objetos para la carrera TIID. Define horas, créditos, prerrequisitos y competencias a desarrollar.

⚡ 105 horas totales💎 6.56 créditos
Distribución: 18 horas teoría + 42 horas práctica. 3° cuatrimestre de la carrera TIID.
Calendario DSM3-1

📅 Calendario POO — Grupo DSM3-1

Calendario de actividades, prácticas, entregas y evaluaciones del grupo DSM3-1. Consulta fechas de entrega y actividades para Mayo–Agosto 2026.

👥 Grupo DSM3-1📆 Mayo–Ago 2026
Importante: Revisa este calendario semanalmente para no perder fechas de entrega de prácticas o evaluaciones.
Calendario DSM3-2

📅 Calendario POO — Grupo DSM3-2

Calendario de actividades, prácticas, entregas y evaluaciones del grupo DSM3-2. Consulta fechas de entrega y actividades para Mayo–Agosto 2026.

👥 Grupo DSM3-2📆 Mayo–Ago 2026
Importante: Revisa este calendario semanalmente para no perder fechas de entrega de prácticas o evaluaciones.
Calendario Escolar UTH

🏫 Calendario Escolar 2025–2026 UTH

Calendario oficial de la Universidad Tecnológica de Hermosillo para el ciclo escolar 2025–2026. Incluye periodos de clases, asuetos, exámenes y eventos institucionales.

🏛️ Oficial UTH📆 2025–2026
Incluye: Inicio y fin de cuatrimestre, vacaciones, días inhábiles y fechas de exámenes finales de todos los cuatrimestres.
📝
Examen U1 · Opción 1

📝 Examen U1 — Opción 1

Ejercicios 1–6: lógica condicional con if / elif / else y atributos bool. Incluye escenario, diagrama de clases, diagrama de casos de uso y salida esperada.

📊 6 ejercicios 🎯 100 pts 🐍 Python 3
Temas: Semáforo, Calificaciones, Descuentos, Sensor, Membresías, Triángulos.
📝
Examen U1 · OP2

📝 Examen U1 OP2 — Evaluación Alternativa

Evaluación alternativa de la Unidad 1. Instrumento de evaluación con ejercicios prácticos de Programación Orientada a Objetos.

🎯 Unidad 1 🐍 Python 3
🏛️
Examen U2 · OP2

🏛️ Examen U2 OP2 — Pilares de la POO

Evaluación alternativa de la Unidad 2. 6 ejercicios resueltos y comentados línea por línea, cubriendo Abstracción, Encapsulamiento, Herencia y Polimorfismo.

🎯 Unidad 2 🐍 Python 3 📊 85 pts
Reglas de la Asignatura

📜 Reglas de la Asignatura

Normas y acuerdos del curso de Programación Orientada a Objetos. Léelas con atención: definen dinámica, entregas, asistencia y conducta esperada del alumno.

📋 Principales reglas: ✅ Entrega puntual — sin excepciones salvo causa justificada
✅ Código propio — plagio = calificación 0
✅ Mínimo 80% de asistencia para tener derecho a calificación
✅ Computadora solo para actividades de la clase
✅ Respeto al docente y compañeros en todo momento
✅ Herramientas instaladas desde la 1ª sesión práctica
✅ Participación activa suma al desempeño general
📬

Atención y Soporte

Profr. Bernardo Prado Díaz — POO 2026-2

✉️ Correo de contacto

tenor_prado@yahoo.com.mx

⏱️ Tiempo de respuesta

Máximo 24 horas, aunque generalmente es mucho menor.

🔄 Sin respuesta en 24 hrs

Reenvía tu correo. A veces los mensajes se desvían a SPAM. Revisa también tu bandeja de no deseados.

💡
Consejo para evitar que tu correo vaya al SPAM Los filtros de correo de Yahoo y Hotmail suelen marcar como basura los mensajes sin asunto claro. Al escribir, usa este formato en el campo Asunto:
Duda  —  Programación Orientada a Objetos  —  [Tu Nombre Completo]  —  [Grupo]
🛡️ Ir a la Práctica Guiada — Unidad 4 →

📅 Calendario Final — Cierre del Cuatrimestre 2026-2

Fechas oficiales desde el examen de la Unidad 2 hasta el examen extraordinario. Revisa este calendario con frecuencia: no hay prórrogas salvo aviso expreso del docente.

🏫 UTH – TIID📅 Junio – Agosto 2026👨‍🏫 Bernardo Prado Díaz
Examen Calificación / Inicio de unidad Límite de entrega Recuperación / Preparación Extraordinario
🗓️ Junio
Lunes 22 de Junio
Examen Oportunidad 1 de la Unidad 2.
Martes 23 de Junio
Calificación de exámenes e inicio de las prácticas de la Unidad 3. Presentación del modo de evaluación.
Lunes 29 de Junio
Examen Oportunidad 2 de la Unidad 2.
Martes 30 de Junio
Calificación de exámenes e inicio de la Unidad 4. Presentación del modo de evaluación.
🗓️ Julio
Lunes 06 de Julio
Límite de entrega de la evaluación de la Unidad 3.
Martes 14 de Julio
Límite de entrega de la evaluación de la Unidad 4.
🗓️ Agosto
Lunes 10 de Agosto
Recuperación de la Unidad 1 u otra unidad pendiente.
Martes 11 de Agosto
Recuperación de la Unidad 2 u otra unidad pendiente.
Lunes 17 y Martes 18 de Agosto
Preparación para el examen extraordinario.
Martes 25 de Agosto
Examen Extraordinario, de 7:00 a 10:00 am.
Importante: Los límites de entrega y fechas de recuperación no tienen prórroga salvo causa justificada autorizada por el docente. Cualquier cambio se anunciará con anticipación.
Cargando...
🎬
Video no disponible aún El profesor publicará esta grabación después de la sesión.
Revisa esta sección más tarde o consulta el grupo de clase.
📄 ▶ Usa los controles del video para pausar y retroceder ▮▮ ESC para cerrar