🎓 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.
Unidad 1 – Modelado
Conceptos POO, diagramas UML, patrones de diseño, Draw.io, PyCharm.
Unidad 2 – Sintaxis POO
Python orientado a objetos completo: clases, herencia, polimorfismo, encapsulamiento.
Unidad 3 – FLET + MySQL
Interfaces gráficas con FLET, conexión a MySQL, principios SOLID, clean code.
Unidad 4 – IDE, Git y Proyecto
PyCharm avanzado, Git, manejo de excepciones, pruebas unitarias y proyecto final.
🛠️ 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
📐 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.
📋 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.
🔧 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:
python --versionDeberí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
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
| Concepto | Sintaxis Python | Descripción |
|---|---|---|
| Definir clase | class NombreClase: | Crea un molde (plantilla) de objetos |
| Constructor | def __init__(self, param): | Se ejecuta al crear un objeto. self = el propio objeto |
| Atributo | self.nombre = nombre | Guarda un dato dentro del objeto |
| Método | def mi_metodo(self): | Función que pertenece a la clase |
| Crear objeto | obj = NombreClase(args) | Instancia un objeto de la clase |
| Usar objeto | obj.metodo() | Llama a un método del objeto |
| Herencia | class Hijo(Padre): | Hijo hereda todo de Padre |
| Llamar padre | super().__init__() | Llama al constructor del padre |
| Atrib. privado | self.__dato = x | Solo accesible dentro de la clase |
| Atrib. protegido | self._dato = x | Convención: no tocar desde afuera |
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.
__init__, crear objetos, usar self, escribir métodos, llamar métodos de un objeto desde otro, y usar __str__ para imprimir.# ═══════════════════════════════════════════════════════
# 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
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
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".
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).
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:
- edad: int
- correo: str
+ __str__(): str
visibilidad nombre: tipoCrear las clases Estudiante y Profesor
Arrastra 2 Class shapes más al lienzo. Colócalos debajo de Persona. Edítalos así:
- promedio: float
- semestre: int
+ esta_aprobado(): bool
- especialidad: str
- salario: float
+ 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:
endArrow → cámbialo a block (triángulo hueco).- edad: int
- promedio: float
- salario: float
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.
actor o umlActorEs la figura de palito con nombre abajo.
ellipse o useCaseÓvalo con el nombre de la acción.
rectangleRectángulo grande que rodea los óvalos.
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:
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:
lifeline en la búsqueda. Rectángulo arriba + línea punteada abajo.Colocar las 3 lifelines (de izquierda a derecha)
Busca lifeline en el panel. Arrastra 3 lifelines horizontalmente. Nómbralos de izquierda a derecha:
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:
iniciarSesion(usuario, pass)
verificarCredenciales(usuario, pass)
return True / False
return "Bienvenido, Ana"
Resultado final del diagrama de secuencia
diagrama-clases.png, diagrama-casos-uso.png, diagrama-secuencia.png.🧩 Tema 1.3 — Patrones de Diseño
Patrón Singleton — "Solo existe uno"
Útil cuando solo debe existir UN objeto de una clase. Ejemplo: la configuración del sistema.
ConfiguracionHotel(), Python siempre devuelve el mismo objeto único en memoria.cfg1 is cfg2 devuelve True — ambas variables apuntan al mismo objeto.__new__ en Python (se llama antes de __init__), y cuándo usar el Singleton: configuraciones, conexiones a BD, logs del sistema.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"
Habitacion(...).@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.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
| Criterio | Estratégico | Autónomo | Básico | Receptivo |
|---|---|---|---|---|
| Orden/Organización | Código impecable, comentado, estructura profesional | Bien organizado con comentarios clave | Organizado pero sin comentarios | Desorganizado o incompleto |
| Diagramas UML | Diagramas completos, correctos y bien etiquetados | Diagramas correctos con algunos detalles faltantes | Diagramas básicos incompletos | Intento de diagrama incorrecto |
| Conceptos POO | Domina todos los pilares y los aplica correctamente | Aplica correctamente la mayoría de conceptos | Aplica algunos conceptos básicos | Confunde los conceptos |
| Patrones Diseño | Implementa y explica Singleton, Factory, Observer | Implementa correctamente 2 patrones | Implementa 1 patrón con errores menores | No 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.
¿Qué es la POO? — Explicación Visual
Introducción a clases, objetos y los 4 pilares con animaciones didácticas
Diagramas UML — Clases y Relaciones paso a paso
Cómo dibujar diagramas de clases en Draw.io desde cero
Patrones de Diseño — Singleton y Factory
Implementación práctica de los patrones más usados en la industria
Python desde Cero — Instalación y Primer Programa
Instala Python y PyCharm, ejecuta tu primer script paso a paso
Los 4 Pilares de la POO — Explicación Completa
Abstracción, Encapsulamiento, Herencia y Polimorfismo explicados con ejemplos visuales y código Python
Ejecutar Python en Visual Studio Code — Explicación Completa
Aprende a configurar y ejecutar Python en Visual Studio Code paso a paso
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.
Clase 1 — Introducción a POO y Python
DSM3-1Conceptos básicos: clases, objetos, atributos, métodos
Clase 1 — Introducción a POO y Python
DSM3-2Conceptos básicos: clases, objetos, atributos, métodos
Clase 2 — Introducción a POO y Python
DSM3-1Conceptos básicos: clases, objetos, atributos, métodos
Clase 2 — Introducción a POO y Python
DSM3-2Conceptos básicos: clases, objetos, atributos, métodos
Clase 3 — Introducción a POO y Python
DSM3-1Conceptos básicos: clases, objetos, atributos, métodos
Clase 3 — Introducción a POO y Python
DSM3-2Conceptos básicos: clases, objetos, atributos, métodos
Clase 4 — Introducción a POO y Python
DSM3-1Conceptos básicos: clases, objetos, atributos, métodos
Clase 4 — Introducción a POO y Python
DSM3-2Conceptos básicos: clases, objetos, atributos, métodos
Clase 5 — Introducción a POO y Python
DSM3-1Conceptos básicos: clases, objetos, atributos, métodos, diagramas y patrones de diseño
Clase 5 — Introducción a POO y Python
DSM3-2Conceptos básicos: clases, objetos, atributos, métodos, diagramas y patrones de diseño
📖 Material de Apoyo — Guía Didáctica POO
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.
↗ Nueva pestaña
🎯 Ejercicio Integrador — Guía Completa para Examen Unidad 1
📊 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 | ||
📋 Contexto del Problema
El BancoUTH necesita digitalizar su sistema de cuentas. Ofrece dos tipos que comparten comportamiento base pero difieren en reglas de negocio:
- Genera intereses mensuales (tasa 3 %)
- Máximo 5 retiros por mes
- No permite saldo negativo
- Interés = saldo × tasa
- 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
📦 Diagrama de Clases UML
Estructura completa del sistema — léelo antes de escribir código
Solo desde dentro de la clase.
__saldo, __dni. Python usa name mangling.Clase y subclases.
_historial, _modificar_saldo().Accesible desde cualquier parte del programa.
Triángulo hueco → "es un tipo de". Apunta al padre.
Línea punteada → "tiene/usa". Banco tiene Clientes.
No puede instanciarse. Obliga a implementar los métodos marcados.
👤 Diagrama de Casos de Uso
¿Quién hace qué? — La visión del usuario del sistema
«include» = un caso de uso invoca automáticamente a otro (Retirar siempre verifica disponibilidad).
🔄 Diagrama de Secuencia — "Realizar un Retiro"
Cómo se comunican los objetos en tiempo de ejecución — se lee de arriba a abajo
🐍 Construyendo el Código — Paso a Paso, Línea por Línea
Transaccion → Cuenta (ABC) → CuentaAhorro → CuentaCorriente → Cliente → Banco → mainDe adentro hacia afuera: primero las clases sin dependencias, luego las que las usan.
🔧 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.
# ═══════════════════════════════════════════════════════
# 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
TypeError al crear el objeto.💾 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 ⑤.
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 $ ─┘
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).
🔷 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.
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.
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
Python lo renombra internamente a
_Cuenta__saldo. Desde fuera de la clase, cuenta.__saldo lanza AttributeError. Solo get_saldo() lo expone.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.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.
💰 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.
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
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.
💳 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.
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
👤 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.
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
🏦 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.
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
▶️ 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.
# ══════════════════════ 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
✅ Checklist del Examen — Verifica que tu código lo tenga todo
class Cuenta(ABC):✓- Al menos 2 métodos con
@abstractmethod✓ - No puedes crear
Cuenta()directo ✓
CuentaAhorro(Cuenta)yCuentaCorriente(Cuenta)✓super().__init__(...)en ambas ✓- Subclases con atributos y métodos propios ✓
calcular_interes()en ambas subclases ✓- Resultado diferente según el tipo ✓
- Bucle
for cuenta in cuentas:que llama al mismo método ✓
__saldoprivado enCuenta✓get_saldo()como único acceso externo ✓_modificar_saldo()protegido para subclases ✓
- Diagrama de clases con visibilidades (+/–/#) ✓
- Diagrama de casos de uso con actores ✓
- Diagrama de secuencia con mensajes numerados ✓
from abc import ABC, abstractmethod✓super().__init__()en cada subclase ✓if __name__ == "__main__":al final ✓
🧩 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.
🐍 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.
📋 Temas de la Unidad
🏛️ LOS 4 PILARES DE LA POO — Orden de Aprendizaje
💡 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
| Elemento | Sintaxis | Descripción |
|---|---|---|
| Clase simple | class Producto: | Clase sin herencia |
| Clase con herencia | class Ropa(Producto): | Hereda de Producto |
| Constructor | def __init__(self, n, p): | Se llama al crear el objeto |
| Atributo de clase | iva = 0.16 (fuera de __init__) | Compartido por todos los objetos |
| Atributo de instancia | self.nombre = n | Único para cada objeto |
| Método regular | def 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.setter | Valida al asignar valor |
| Repr | def __repr__(self): | Representación técnica del objeto |
| Comparación | def __eq__(self, otro): | Define cuándo dos objetos son iguales |
| Longitud | def __len__(self): | Define len(objeto) |
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.
__precio_base), polimorfismo (descripcion()), abstracción (IVA como atributo de clase).super(), usar @property y setters para encapsular datos, y aprovechar el polimorfismo para que el mismo bucle for funcione con cualquier tipo de producto.# ═════════════════════════════════════════════════════════════════
# 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
•
self.dato — público: cualquiera puede leerlo y modificarlo•
self._dato — protegido: convención, no modificar desde afuera•
self.__dato — privado: Python lo "oculta" (name mangling), usa property para acceder__saldo, __historial) no se pueden modificar desde afuera — solo mediante métodos que validan la operación.@property para leer datos privados de forma segura, y cómo el setter valida antes de guardar.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
procesar(), tres comportamientos distintospago.procesar(total) # Cada uno responde diferente
cobrar() no necesita saber si el pago es efectivo, tarjeta o transferencia — simplemente llama procesar() y cada objeto responde diferente.cobrar() — solo creas una nueva subclase.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
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).
| Elemento | Sintaxis | Descripción |
|---|---|---|
| Importar ABC | from abc import ABC, abstractmethod | Módulo estándar de Python para abstracción |
| Clase abstracta | class Animal(ABC): | No se puede instanciar directamente |
| Método abstracto | @abstractmethoddef sonido(self): ... | Las subclases DEBEN implementarlo o Python lanza error |
| Método concreto | def respirar(self): return "inhala" | Puede coexistir con métodos abstractos |
| Subclase concreta | class Perro(Animal): | Debe implementar TODOS los métodos abstractos |
| Propiedad abstracta | @property@abstractmethoddef categoria(self): ... | Obliga a definir una propiedad en la subclase |
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.
ABC y @abstractmethod: la clase ReporteBase define QUÉ métodos deben existir sin implementarlos — cada subclase decide el CÓMO.for.ABC y abstractmethod, la diferencia entre métodos abstractos y concretos, y por qué Python lanza TypeError al intentar instanciar una clase abstracta.# ═══════════════════════════════════════════════════════════════
# 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!
• 🎭 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
pydoc."""
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
| Criterio | Estratégico | Autónomo | Básico | Receptivo |
|---|---|---|---|---|
| Clases y Objetos | Implementa correctamente con encapsulamiento, properties y validación | Clases correctas con encapsulamiento básico | Clases funcionales sin encapsulamiento | Errores de sintaxis o lógica |
| Herencia | Usa super(), herencia múltiple y MRO correctamente | Herencia simple con super() correcto | Herencia básica sin super() | Intenta herencia con errores |
| Encapsulamiento | Atributos privados, properties con validación completa | Atributos privados con getters/setters | Atributos privados sin properties | Sin encapsulamiento |
| Abstracción | Usa ABC, @abstractmethod, propiedades abstractas y método concreto heredado | ABC con @abstractmethod implementado en subclases | Simula abstracción con NotImplementedError | No implementa abstracción |
| Polimorfismo | Implementa override, interfaces y duck typing | Override correcto en subclases | Override básico en 1 método | No implementa polimorfismo |
| Documentación | Docstrings completos con parámetros, returns y ejemplos | Docstrings con descripción y parámetros | Docstrings básicos descriptivos | Sin 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.
Clases Abstractas y ABC en Python
abstractmethod, ABC, contratos entre clases con ejemplos reales
Encapsulamiento: @property, getters y setters
Atributos privados, protegidos y el decorador @property
Herencia Simple y Múltiple — super() y MRO
Cómo funciona la cadena de herencia y super() correctamente
Polimorfismo y Duck Typing en Python
Override de métodos, interfaces implícitas, polimorfismo en acción
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.
Clase 1 — Clases, Objetos y Constructor
__init__, self, atributos, primer objeto en Python
Clase 2 — Pilar 1: Abstracción con ABC
Clases abstractas, @abstractmethod, contrato entre clases
Clase 3 — Pilar 2: Encapsulamiento y @property
Atributos privados, getters, setters, validación
Clase 4 — Pilar 3: Herencia Simple y super()
Herencia, super(), reutilización de código
Clase 5 — Herencia Múltiple y MRO
Resolución de orden de método, diamond problem
Clase 6 — Pilar 4: Polimorfismo y Duck Typing
Override, mismo método, comportamientos distintos
Clase 7 — Documentación Profesional con Docstrings
pydoc, docstrings de módulos, clases y métodos
Clase 8 — Repaso y Ejercicio Integrador U2
Proyecto integrador con los 4 pilares aplicados
🎨 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.
📋 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.
Lógica del negocio
Eventos, Controles
DBeaver
diseño limpio
Completo y funcional
🔧 Instalación de WampServer 3.3.7
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:
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
Instalar FLET
Abre PyCharm → Terminal (menú View → Tool Windows → Terminal) y escribe:
pip install fletEspera a que termine. Verás mensajes de descarga. Al final aparece Successfully installed flet-X.X.X.
Instalar mysql-connector-python
pip install mysql-connector-pythonTu primera app FLET — "Hola Mundo"
Crea un archivo hola_flet.py y copia este código:
on_change
lógica...
page.update()
main(page) recibe la ventana como parámetro, y ft.app() es el motor que la hace funcionar.ft.app(target=main), los componentes ft.Text, ft.ElevatedButton y ft.Column, el parámetro on_click para manejar eventos, y page.add().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
🧩 Componentes Principales de FLET
| Componente | Uso | Ejemplo |
|---|---|---|
ft.Text | Texto en pantalla | ft.Text("Hola", size=20) |
ft.TextField | Campo de entrada | ft.TextField(label="Nombre") |
ft.ElevatedButton | Botón principal | ft.ElevatedButton("OK", on_click=f) |
ft.TextButton | Botón texto | ft.TextButton("Cancelar") |
ft.Dropdown | Lista desplegable | ft.Dropdown(options=[ft.dropdown.Option("A")]) |
ft.DataTable | Tabla de datos | ft.DataTable(columns=[...], rows=[...]) |
ft.Column | Columna vertical | ft.Column([comp1, comp2]) |
ft.Row | Fila horizontal | ft.Row([comp1, comp2]) |
ft.Container | Contenedor con estilo | ft.Container(content=..., bgcolor=...) |
ft.AlertDialog | Cuadro de diálogo | ft.AlertDialog(title=..., content=...) |
ft.SnackBar | Mensaje temporal | ft.SnackBar(content=ft.Text("OK")) |
ft.AppBar | Barra de título | ft.AppBar(title=ft.Text("App")) |
🐬 Tema 3.2 — Base de Datos MySQL para el Proyecto
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.
CREATE TABLE, tipos de datos (INT, VARCHAR, DECIMAL), restricciones (NOT NULL, AUTO_INCREMENT), y relaciones con FOREIGN KEY.-- 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)
VentaDAO
localhost:3306
clientes
ventas
→ No se abren múltiples conexiones a la BD
Conexion reutilizable que centraliza el acceso a MySQL, con métodos ejecutar() para modificaciones y consultar() para lecturas que devuelven diccionarios.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.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
obtener_todos(), insertar(), actualizar(), eliminar() y buscar_por_nombre(), con protección contra SQL Injection.%s como marcadores seguros en consultas SQL, y el patrón JOIN para obtener datos de múltiples tablas.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
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.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 — Single Responsibility
Una clase = una responsabilidad. ProductoDAO solo accede a BD. ProductoService solo tiene lógica del negocio. No mezcles ambas.
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 — Liskov Substitution
Una subclase debe poder reemplazar a su padre. Si tienes Animal con método hablar(), todos sus hijos deben tenerlo funcionando.
I — Interface Segregation
Mejor muchas interfaces pequeñas que una grande. No obligues a una clase a implementar métodos que no usa.
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
| Criterio | Estratégico | Autónomo | Básico | Receptivo |
|---|---|---|---|---|
| GUI con FLET | Interfaz profesional, validación completa, UX cuidada | Interfaz funcional con validaciones básicas | Interfaz básica sin validaciones | No funciona o usa otra librería |
| MySQL OOP | DAO completo con CRUD, manejo de errores, patrón Singleton | CRUD funcional con clases bien definidas | Conexión y consultas básicas | Conexión sin OOP |
| Principios SOLID | Aplica los 5 principios visiblemente en el código | Aplica S y O claramente | Menciona SOLID en comentarios | No aplica SOLID |
| Documentación | Manual de usuario + guía de instalación profesional | Manual de usuario completo | README básico | Sin 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 — Apps de Escritorio con Python
Primeros pasos: controles, eventos, páginas y navegación
MySQL con Python — Conector y CRUD Completo
Instalar mysql-connector, conectar, INSERT, UPDATE, DELETE
Principios SOLID — Ejemplos en Python
SRP, OCP, LSP, ISP, DIP aplicados a código orientado a objetos
Instalar WampServer y configurar MySQL local
Guía paso a paso para tener MySQL en Windows con WampServer
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.
Clase 1 — Instalación FLET y Primera App
DSM3-1pip install flet, estructura de proyecto, primer ft.app()
Clase 1 — Instalación FLET y Primera App
DSM3-2pip install flet, estructura de proyecto, primer ft.app()
Clase 2 — Controles FLET: Botones, Inputs, Listas
DSM3-1ElevatedButton, TextField, ListView, Row, Column
Clase 3 — Instalación MySQL y WampServer
Configuración de MySQL local, DBeaver, conexión inicial
Clase 4 — CRUD MySQL con Python OOP
Clase Conexion, DAOs, INSERT/SELECT/UPDATE/DELETE
Clase 5 — Conectar FLET con MySQL
DAO + interfaz gráfica: CRUD completo con FLET
Clase 6 — Principios SOLID en el Proyecto
Refactorizar aplicando SRP, OCP y LSP
Clase 7 — Documentación y Cierre U3
Manual de usuario, guía de instalación, entrega final
🚀 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.
📋 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.
Refactoring
Historial
Errores propios
TDD
Documentado
🔀 Guía de Instalación y Uso de 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 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
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 init | Crea un repositorio nuevo | git init mi-proyecto |
git status | Muestra qué archivos cambiaron | git status |
git add | Prepara archivos para commit | git add archivo.py o git add . |
git commit | Guarda una versión con mensaje | git commit -m "Agrega módulo de ventas" |
git log | Historial de commits | git log --oneline |
git branch | Lista o crea ramas | git branch nueva-feature |
git checkout | Cambia de rama | git checkout nueva-feature |
git merge | Une ramas | git merge nueva-feature |
git push | Sube cambios a GitHub | git push origin main |
git pull | Descarga cambios de GitHub | git pull origin main |
git clone | Clona repositorio remoto | git clone https://github.com/user/repo.git |
💡 Tema 4.1 — PyCharm: Atajos y Funciones Profesionales
| Acción | Atajo Windows | ¿Para qué? |
|---|---|---|
| Ejecutar programa | Shift+F10 | Corre el archivo actual |
| Depurador | Shift+F9 | Ejecuta en modo debug (con breakpoints) |
| Agregar breakpoint | F9 (clic en margen) | Pausa la ejecución en esa línea |
| Buscar en proyecto | Ctrl+Shift+F | Busca texto en todos los archivos |
| Ir a definición | Ctrl+B | Salta a donde está definida la función/clase |
| Renombrar símbolo | Shift+F6 | Renombra en todos los archivos a la vez |
| Autocompletar | Ctrl+Space | Muestra sugerencias de código |
| Comentar línea | Ctrl+/ | Comenta/descomenta la línea actual |
| Formatear código | Ctrl+Alt+L | Aplica el estilo PEP 8 automáticamente |
| Terminal integrada | Alt+F12 | Abre terminal dentro de PyCharm |
| Buscar clase | Ctrl+N | Busca una clase por nombre |
| Ver estructura | Alt+7 | Panel con métodos y atributos de la clase |
Virtual Environment (Entorno Virtual)
requirements.txt que permite reproducir el entorno exacto en cualquier otra computadora.pip freeze para documentar dependencias. Práctica usada en toda la industria.# 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
try/except puedes capturarla y responder de forma elegante.puede fallar
try/except/else/finally y cómo crear excepciones personalizadas que comunican errores específicos del negocio.ValueError, ZeroDivisionError heredan de Exception), cómo crear StockInsuficienteError personalizada y manejar errores de conexión MySQL.# ── 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
unittest de Python: cómo estructurar una suite de pruebas con setUp(), múltiples métodos test_*() y assertions que verifican el comportamiento esperado.assertEqual, assertIn, assertAlmostEqual, assertRaises e assertIsInstance — los 5 assertions más comunes. Y el ciclo: escribir prueba → ejecutar → corregir código.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
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.
✅ 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
models/, dao/, ui/, services/, tests/) siguiendo el principio de responsabilidad única.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
main.py.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."""
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
# 🏢 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
| Criterio | Estratégico | Autónomo | Básico | Receptivo |
|---|---|---|---|---|
| Git | Repositorio con ramas, commits descriptivos, README profesional | Commits regulares con mensajes descriptivos | Repositorio con commits iniciales | Entrega sin control de versiones |
| Excepciones | Excepciones propias, try/except en toda la app, logging | Maneja excepciones MySQL y de usuario | try/except básico en operaciones clave | Sin manejo de excepciones |
| Pruebas Unitarias | +15 pruebas con setUp, tearDown y casos límite | 10+ pruebas funcionales | 5+ pruebas básicas | Menos de 3 pruebas |
| Proyecto Final | Sistema completo, profesional, documentado, con pruebas y Git | Sistema funcional con la mayoría de requisitos | Sistema parcial con funcionalidad básica | Proyecto incompleto o no funcional |
| Documentación | UML + manual usuario + guía instalación + comentarios de código | UML y manual de usuario | README y comentarios básicos | Sin 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 y GitHub desde Cero — Guía Completa
init, add, commit, push, pull, ramas y flujo profesional
PyCharm — Configuración y Productividad
Atajos, depurador, refactorización y plugins esenciales
Pruebas Unitarias en Python con unittest
TestCase, assertEqual, setUp, tearDown — testing profesional
Manejo de Excepciones — try/except/finally
Excepciones propias, jerarquía de errores, logging
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.
Clase 1 — Git: Instalación y Primeros Commits
git init, git add, git commit, .gitignore, primer repositorio
Clase 2 — GitHub: Ramas y Pull Requests
git branch, merge, conflictos, GitHub Desktop
Clase 3 — PyCharm Profesional: Debugger y Refactor
Breakpoints, inspeccionar variables, refactorizar código
Clase 4 — Manejo de Excepciones Propias
try/except/finally, crear excepciones personalizadas
Clase 5 — Pruebas Unitarias con unittest
TestCase, assertEqual, setUp/tearDown, estructura de pruebas
Clase 6 — Proyecto Final: Planificación y Arquitectura
Selección del sistema, UML, estructura de carpetas
Clase 7 — Proyecto Final: Desarrollo Parte 1
Modelos, clases, base de datos — primeras pantallas FLET
Clase 8 — Proyecto Final: Entrega y Presentación
Demo del sistema, documentación final, rúbrica de evaluación
📎 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.
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.
Duda — Programación Orientada a Objetos — [Tu Nombre Completo] — [Grupo]
📅 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.