Ing. Bernardo Prado · UTH 2026-2 · DSM3
Aprenderás los 4 conceptos fundamentales de la Programación Orientada a Objetos: Encapsulamiento, Herencia, Abstracción y Polimorfismo. Cada uno se explicará desde cero con ejemplos prácticos.
Crea una nueva carpeta Unidad2 dentro de tu carpeta de trabajo POO_UTH. Abre esa carpeta en VS Code. Crea el archivo institucion_educativa.py.
¿Qué es? Ocultar los datos internos de un objeto y protegerlos de acceso no autorizado. Solo se puede acceder a ellos a través de métodos controlados (getters y setters).
Analogía: Un cajero automático — puedes depositar y retirar dinero, pero no puedes meter la mano dentro de la máquina y agarrar el dinero directamente.
¿Qué es? Una clase puede "heredar" atributos y métodos de otra clase, evitando repetir código. La clase que hereda se llama subclase o clase hija; la clase de la que hereda es la superclase o clase padre.
Analogía: Tú heredas rasgos de tus padres (color de ojos, apellido), pero también tienes tus propias características únicas.
¿Qué es? Definir una plantilla (clase abstracta) que obliga a sus subclases a implementar ciertos métodos. La clase abstracta en sí NO puede instanciarse — es solo un contrato.
Analogía: El contrato de renta de un departamento define reglas que DEBEN seguirse, pero el contrato por sí solo no es un departamento.
¿Qué es? Distintos objetos de distintas clases pueden responder al mismo método de formas diferentes. "Poly" = muchos, "morphos" = formas.
Analogía: Cuando dices "habla" a una persona, a un perro y a un loro, cada uno responde diferente: la persona habla, el perro ladra, el loro repite. Mismo mensaje, respuestas distintas.
La UTH necesita un sistema para gestionar a las personas que forman parte de la institución. Hay dos tipos: estudiantes y profesores. Ambos son "personas" y comparten datos básicos (nombre, edad, ID), pero cada uno tiene características propias.
presentarse(), pero cada uno lo hace de forma diferente (polimorfismo)._nombre : str
_edad : int
_id_persona : str
__init__(nombre, edad, id)
nombre → property
edad → property + setter
presentarse() → abstracto
__str__() → str
__matricula : str
__carrera : str
__promedio : float
promedio → property + setter
matricula → property
presentarse() → override
esta_aprobado() → bool
__monto_beca : float
presentarse() → override
monto_beca → property + setter
__num_empleado : str
__especialidad : str
__salario : float
salario → property + setter
presentarse() → override
dar_aumento(pct) → None
Empezamos creando la clase base abstracta Persona. Para hacer clases abstractas en Python necesitamos el módulo abc.
Al principio del archivo escribe lo siguiente. abc es un módulo estándar de Python (ya viene incluido, no necesitas instalarlo). Proporciona las herramientas para crear clases abstractas.
# =====================================================
# Práctica Unidad 2 - Los 4 Pilares de la POO
# Asignatura: POO | UTH 2026-2
# Sistema de Gestión de Institución Educativa
# =====================================================
from abc import ABC, abstractmethod
# 'ABC' es la clase base de la que heredan nuestras clases abstractas
# 'abstractmethod' es un decorador que marca métodos que DEBEN implementarse
| Fragmento | Explicación |
|---|---|
from abc import ABC, abstractmethod | Importa dos cosas del módulo abc: la clase ABC (Abstract Base Class) y el decorador abstractmethod. Solo importamos lo que necesitamos. |
ABC | Al heredar de esta clase, nuestra clase se convierte en abstracta — Python no permitirá crear objetos directamente de ella. |
abstractmethod | Es un decorador (va con @ encima del método) que marca un método como "abstracto": las subclases DEBEN sobreescribirlo. |
PersonaEsta clase es abstracta porque hereda de ABC. Define la estructura común de todas las personas de la institución. El método presentarse() es abstracto — cada subclase lo implementará diferente.
class Persona(ABC):
"""
Clase ABSTRACTA que representa a cualquier persona de la institución.
No se puede instanciar directamente — es solo una plantilla.
"""
def __init__(self, nombre: str, edad: int, id_persona: str):
"""Constructor de Persona."""
# Atributos PROTEGIDOS (un guión bajo = convención "no acceder directo")
self._nombre = nombre
self._edad = edad
self._id_persona = id_persona
# ── PROPERTIES (Encapsulamiento) ──
@property
def nombre(self):
"""Getter: devuelve el nombre (solo lectura)."""
return self._nombre
@property
def edad(self):
"""Getter: devuelve la edad."""
return self._edad
@edad.setter
def edad(self, nueva_edad: int):
"""Setter: valida y actualiza la edad."""
if 0 < nueva_edad < 120:
self._edad = nueva_edad
else:
print(f"❌ Edad inválida: {nueva_edad}")
@property
def id_persona(self):
"""Getter: devuelve el ID (solo lectura, sin setter)."""
return self._id_persona
# ── MÉTODO ABSTRACTO ──
@abstractmethod
def presentarse(self):
"""
Método ABSTRACTO: cada subclase DEBE implementar este método.
Si no lo implementa, Python lanza TypeError al instanciar.
"""
pass
def __str__(self):
return f"Persona(ID:{self._id_persona}, Nombre:{self._nombre}, Edad:{self._edad})"
| Concepto clave | Explicación detallada |
|---|---|
class Persona(ABC): | Heredar de ABC hace que esta clase sea abstracta. Python no dejará crear objetos Persona directamente. |
nombre: str en los parámetros | Las anotaciones de tipo (type hints) son opcionales pero excelente práctica. Le dicen al lector qué tipo de dato espera cada parámetro. |
self._nombre (un guión bajo) | Convención de atributo protegido. El guión bajo dice: "este atributo es interno, no deberías acceder a él desde fuera de la clase". Python NO lo impide, pero es un aviso. |
@property | Decorador que convierte un método en una propiedad. Permite acceder con sintaxis persona.nombre sin paréntesis, como si fuera un atributo, pero pasando por el método getter. |
@edad.setter | Define el setter de la propiedad edad. Permite hacer persona.edad = 25 y ejecutar código de validación antes de guardar el valor. |
@abstractmethod | Marca el método como abstracto. Cualquier clase que herede de Persona DEBE implementar presentarse(), o Python lanzará un error al intentar crear objetos. |
pass | En el método abstracto, pass es un marcador de posición vacío. Le dice a Python "este método no hace nada aquí — es solo una declaración". |
id_persona?
Definir solo el getter (sin setter) hace que el atributo sea de solo lectura. Si alguien intenta hacer persona.id_persona = "otro_id", Python lanzará un AttributeError. Eso es protección real.
EstudianteLa clase Estudiante hereda de Persona y aplica encapsulamiento fuerte con atributos privados (doble guión bajo).
_atributo (1 guión bajo) = Protegido: convención de "no toques esto desde fuera", pero Python no lo impide.
__atributo (2 guiones bajos) = Privado: Python aplica name mangling. El atributo se renombra internamente a _ClaseNombre__atributo, haciendo casi imposible el acceso externo accidental.
Estudiante que hereda de Persona
class Estudiante(Persona):
"""Clase que representa a un estudiante. Hereda de Persona."""
MINIMO_APROBATORIO = 7.0 # Atributo de clase
def __init__(self, nombre: str, edad: int, id_persona: str,
matricula: str, carrera: str, promedio: float = 0.0):
"""Constructor: llama al constructor padre y agrega atributos propios."""
# super() llama al constructor de la clase padre (Persona)
# Es obligatorio llamarlo para que los atributos del padre se inicialicen
super().__init__(nombre, edad, id_persona)
# Atributos PRIVADOS (doble guión bajo → name mangling)
self.__matricula = matricula
self.__carrera = carrera
self.__promedio = promedio
# ── PROPERTIES (Encapsulamiento) ──
@property
def matricula(self):
return self.__matricula
@property
def carrera(self):
return self.__carrera
@property
def promedio(self):
return self.__promedio
@promedio.setter
def promedio(self, nuevo: float):
"""Setter con validación: solo acepta valores entre 0.0 y 10.0."""
if 0.0 <= nuevo <= 10.0:
self.__promedio = nuevo
else:
print(f"❌ Promedio inválido: {nuevo}. Debe estar entre 0.0 y 10.0")
# ── MÉTODOS PROPIOS ──
def esta_aprobado(self) -> bool:
"""Retorna True si el promedio es suficiente para aprobar."""
return self.__promedio >= Estudiante.MINIMO_APROBATORIO
# ── MÉTODO ABSTRACTO IMPLEMENTADO (override) ──
def presentarse(self):
"""Implementación del método abstracto de Persona."""
estado = "✅ Aprobado" if self.esta_aprobado() else "❌ Reprobado"
print(f"🎓 Soy {self._nombre}, estudiante de {self.__carrera}.")
print(f" Matrícula: {self.__matricula} | Promedio: {self.__promedio:.1f} | {estado}")
def __str__(self):
return f"Estudiante({self._nombre}, {self.__matricula}, {self.__carrera}, {self.__promedio:.1f})"
| Fragmento clave | Explicación |
|---|---|
class Estudiante(Persona): | Entre paréntesis va la clase padre. Estudiante hereda TODOS los atributos y métodos de Persona. |
super().__init__(nombre, edad, id_persona) | super() retorna una referencia a la clase padre. Llamamos a su __init__ para que inicialice _nombre, _edad y _id_persona. Si no llamamos super().__init__, esos atributos no existirán. |
self.__matricula (doble guión) | Atributo privado. Python lo renombra internamente a _Estudiante__matricula. Intentar acceder a objeto.__matricula desde fuera de la clase dará AttributeError. |
def presentarse(self): | Esta es la implementación concreta del método abstracto de Persona. Al sobreescribir el método, Estudiante deja de ser abstracta y puede instanciarse. |
-> bool | Anotación de tipo de retorno. Le dice al lector que este método retorna un bool (True/False). |
EstudianteBecarioEstudianteBecario hereda de Estudiante, que ya hereda de Persona. Esto es herencia de 3 niveles.
EstudianteBecario
class EstudianteBecario(Estudiante):
"""
Estudiante con beca económica.
Hereda de Estudiante (que a su vez hereda de Persona).
Herencia de 3 niveles: Persona → Estudiante → EstudianteBecario
"""
def __init__(self, nombre, edad, id_persona,
matricula, carrera, promedio, monto_beca: float):
# Llama al constructor de Estudiante (que a su vez llama al de Persona)
super().__init__(nombre, edad, id_persona, matricula, carrera, promedio)
# Atributo propio de EstudianteBecario (privado)
self.__monto_beca = monto_beca
@property
def monto_beca(self):
return self.__monto_beca
@monto_beca.setter
def monto_beca(self, nuevo_monto: float):
if nuevo_monto >= 0:
self.__monto_beca = nuevo_monto
else:
print("❌ El monto de beca no puede ser negativo.")
def presentarse(self):
"""Override de presentarse — incluye información de la beca."""
# Llamamos al presentarse de Estudiante y añadimos la beca
super().presentarse()
print(f" 💰 Beca mensual: ${self.__monto_beca:,.2f}")
def __str__(self):
return f"EstudianteBecario({self.nombre}, Beca: ${self.__monto_beca:,.2f})"
| Fragmento | Explicación |
|---|---|
class EstudianteBecario(Estudiante): | Hereda de Estudiante. Como Estudiante ya hereda de Persona, EstudianteBecario hereda de ambas automáticamente. |
super().presentarse() | Llama al método presentarse de Estudiante (la clase padre). Así reutilizamos código ya escrito y solo añadimos la parte de la beca. |
${self.__monto_beca:,.2f} | Formato de número con separador de miles (,) y 2 decimales (.2f). Así 5000.5 se muestra como $5,000.50. |
Profesor (otra hija de Persona)Profesor
class Profesor(Persona):
"""Clase que representa a un profesor. También hereda de Persona."""
def __init__(self, nombre, edad, id_persona,
num_empleado: str, especialidad: str, salario: float):
super().__init__(nombre, edad, id_persona)
self.__num_empleado = num_empleado
self.__especialidad = especialidad
self.__salario = salario
@property
def num_empleado(self):
return self.__num_empleado
@property
def especialidad(self):
return self.__especialidad
@property
def salario(self):
return self.__salario
@salario.setter
def salario(self, nuevo_salario: float):
if nuevo_salario > 0:
self.__salario = nuevo_salario
else:
print("❌ El salario debe ser mayor a 0.")
def dar_aumento(self, porcentaje: float):
"""Aumenta el salario en el porcentaje indicado."""
if 0 < porcentaje <= 100:
aumento = self.__salario * (porcentaje / 100)
self.__salario += aumento
print(f"✅ Aumento del {porcentaje}% aplicado. Nuevo salario: ${self.__salario:,.2f}")
else:
print("❌ El porcentaje debe estar entre 1 y 100.")
# ── Implementación del método abstracto ──
def presentarse(self):
print(f"👨🏫 Soy el Prof. {self._nombre}, especialidad: {self.__especialidad}.")
print(f" Empleado #{self.__num_empleado} | Salario: ${self.__salario:,.2f}")
def __str__(self):
return f"Profesor({self._nombre}, {self.__especialidad}, ${self.__salario:,.2f})"
El polimorfismo lo veremos en el programa principal. La clave es que una función puede trabajar con cualquier objeto que sea una Persona, sin saber si es un Estudiante o un Profesor.
hacer_presentacion()Esta función recibe cualquier objeto de tipo Persona (o subclase). No le importa si es Estudiante, EstudianteBecario o Profesor — solo llama a presentarse() y cada objeto lo resuelve a su manera.
def hacer_presentacion(persona: Persona):
"""
Función polimórfica: acepta cualquier Persona (o subclase).
Llama a presentarse() → cada clase lo implementa diferente.
"""
print("-" * 50)
persona.presentarse() # polimorfismo en acción
print("-" * 50)
if __name__ == "__main__":
print("\n🏫 SISTEMA UTH — GESTIÓN DE PERSONAS")
print("=" * 55)
# ── 1. HERENCIA y ABSTRACCIÓN: crear objetos de subclases ──
# Nota: NO se puede hacer: persona = Persona("Ana", 20, "P001")
# Porque Persona es abstracta. Probar esto te dará TypeError.
est1 = Estudiante("Ana López", 20, "P001",
"DSM2024001", "DSM", 8.5)
est2 = Estudiante("Carlos Mendoza", 22, "P002",
"DSM2024002", "DSM", 5.9)
becario = EstudianteBecario("María García", 19, "P003",
"DSM2024003", "DSM", 9.1, 3500.0)
prof1 = Profesor("Ing. Bernardo Prado", 38, "E001",
"UTH-2001", "Programación Orientada a Objetos", 25000.0)
# ── 2. POLIMORFISMO: misma función, comportamientos distintos ──
print("\n🎤 PRESENTACIONES (Polimorfismo):")
personas = [est1, est2, becario, prof1]
for p in personas:
hacer_presentacion(p) # misma llamada, resultado diferente
# ── 3. ENCAPSULAMIENTO: acceso controlado por properties ──
print("\n🔒 DEMOSTRACIÓN DE ENCAPSULAMIENTO:")
# Acceso correcto: usando el getter (property)
print(f"Promedio de Ana (getter): {est1.promedio}")
# Modificación correcta: usando el setter (con validación)
print("Cambiando promedio de Carlos a 7.2...")
est2.promedio = 7.2 # llama automáticamente al @promedio.setter
print(f"Nuevo promedio: {est2.promedio}")
# Intento de valor inválido — el setter lo rechaza
print("Intentando poner promedio = 15 (inválido)...")
est2.promedio = 15 # el setter impide esto
# Aumento de salario al profesor
print("\n💰 DANDO AUMENTO AL PROFESOR:")
prof1.dar_aumento(10) # 10% de aumento
# ── 4. HERENCIA MULTINIVEL: EstudianteBecario tiene TODO ──
print("\n🎓 DATOS DEL ESTUDIANTE BECARIO (herencia multinivel):")
print(f"Nombre (de Persona): {becario.nombre}")
print(f"Matrícula (de Estudiante): {becario.matricula}")
print(f"Beca (propio): ${becario.monto_beca:,.2f}")
print(f"¿Aprobada? (de Estudiante): {becario.esta_aprobado()}")
# ── isinstance(): verificar tipo de objeto ──
print("\n🔍 VERIFICACIÓN DE TIPOS (isinstance):")
print(f"¿becario es Persona? {isinstance(becario, Persona)}")
print(f"¿becario es Estudiante? {isinstance(becario, Estudiante)}")
print(f"¿becario es EstudianteBecario? {isinstance(becario, EstudianteBecario)}")
print(f"¿becario es Profesor? {isinstance(becario, Profesor)}")
| Concepto | Dónde se ve en el código |
|---|---|
| 🎭 Abstracción | No se puede crear Persona(...) directamente. presentarse() es abstracto. |
| 🔒 Encapsulamiento | est2.promedio = 7.2 usa el setter. est2.__promedio desde fuera daría error. |
| 🧬 Herencia | becario.nombre usa atributo de Persona; becario.matricula usa atributo de Estudiante. |
| 🦎 Polimorfismo | El bucle for p in personas llama presentarse() en cada objeto diferente. |
isinstance(obj, Clase) | Verifica si un objeto es instancia de una clase o de alguna de sus clases padre. |
Guarda el archivo (Ctrl+S) y ejecuta:
python institucion_educativa.py
ABC y abstractmethod al inicio del archivoPersona hereda de ABC y tiene el método abstracto presentarse()Persona NO puede instanciarse (da TypeError si lo intentas)Estudiante tiene atributos privados con doble guión bajo __Estudiante tiene properties con getter y setter para promedioEstudianteBecario llama a super().__init__() correctamenteEstudianteBecario.presentarse() llama super().presentarse()Profesor implementa su propio presentarse()hacer_presentacion() demuestra polimorfismo