Programación Orientada a Objetos

Ing. Bernardo Prado · UTH 2026-2 · DSM3

💎 Unidad 2 🐍 Python 3 💻 VS Code

💎 Práctica 2 — Los 4 Pilares de la POO

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.

⏱ 120 minutos aprox. 🎯 Nivel: Intermedio 📁 Archivo: institucion_educativa.py 🛠 Herramienta: VS Code

📋 Sección 0 — Antes de Empezar

⚠️ Necesitas haber completado la Práctica de Unidad 1 Esta práctica retoma los conceptos de clase, objeto y constructor. Si no completaste la Unidad 1, regresa y hazla primero.
💡 ¿Qué aprenderás? Los 4 Pilares son el corazón de la POO. Toda la industria del software usa estos principios. Al terminar esta práctica tendrás un sistema de institución educativa completo con múltiples clases relacionadas.

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.

🏛️ Sección 1 — Los 4 Pilares: Visión General

🔒 Pilar 1: Encapsulamiento

¿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.

🧬 Pilar 2: Herencia

¿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.

🎭 Pilar 3: Abstracción

¿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.

🦎 Pilar 4: Polimorfismo

¿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.

🏢 Sección 2 — El Escenario del Proyecto

🏫 Sistema de Gestión de una Institución Educativa

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.

  • Una Persona es abstracta — no existe una "persona genérica" sin rol.
  • Un Estudiante tiene matrícula, carrera, y promedio (encapsulado).
  • Un Profesor tiene número de empleado, especialidad, y salario (encapsulado).
  • Un EstudianteBecario hereda de Estudiante y agrega monto de beca.
  • Todos pueden presentarse(), pero cada uno lo hace de forma diferente (polimorfismo).

Diagrama UML del sistema

«abstracta» Persona
Atributos protegidos

_nombre : str

_edad : int

_id_persona : str

Métodos

__init__(nombre, edad, id)

nombre → property

edad → property + setter

presentarse() → abstracto

__str__() → str

Estudiante (hereda Persona)
Atributos privados

__matricula : str

__carrera : str

__promedio : float

Métodos + Properties

promedio → property + setter

matricula → property

presentarse() → override

esta_aprobado() → bool

EstudianteBecario (hereda Estudiante)
Atributo propio

__monto_beca : float

Métodos

presentarse() → override

monto_beca → property + setter

Profesor (hereda Persona)
Atributos privados

__num_empleado : str

__especialidad : str

__salario : float

Métodos

salario → property + setter

presentarse() → override

dar_aumento(pct) → None

🎭 Sección 3 — Pilar 1: Abstracción con Clases Abstractas (ABC)

Empezamos creando la clase base abstracta Persona. Para hacer clases abstractas en Python necesitamos el módulo abc.

1
Importar 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.

institucion_educativa.py — Importaciones
# =====================================================
# 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
FragmentoExplicación
from abc import ABC, abstractmethodImporta dos cosas del módulo abc: la clase ABC (Abstract Base Class) y el decorador abstractmethod. Solo importamos lo que necesitamos.
ABCAl heredar de esta clase, nuestra clase se convierte en abstracta — Python no permitirá crear objetos directamente de ella.
abstractmethodEs un decorador (va con @ encima del método) que marca un método como "abstracto": las subclases DEBEN sobreescribirlo.
2
Crear la clase abstracta Persona

Esta 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.

institucion_educativa.py — Clase Persona (Abstracta)

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 claveExplicació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ámetrosLas 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.
@propertyDecorador 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.setterDefine el setter de la propiedad edad. Permite hacer persona.edad = 25 y ejecutar código de validación antes de guardar el valor.
@abstractmethodMarca el método como abstracto. Cualquier clase que herede de Persona DEBE implementar presentarse(), o Python lanzará un error al intentar crear objetos.
passEn 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".
🧠 ¿Por qué NO poner setter en 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.

🔒🧬 Sección 4 — Pilares 2 y 3: Encapsulamiento + Herencia en Estudiante

La clase Estudiante hereda de Persona y aplica encapsulamiento fuerte con atributos privados (doble guión bajo).

💡 Diferencia: Protegido vs Privado
_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.
3
Crear la clase Estudiante que hereda de Persona
institucion_educativa.py — Clase Estudiante

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 claveExplicació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.
-> boolAnotación de tipo de retorno. Le dice al lector que este método retorna un bool (True/False).

🧬🧬 Sección 5 — Herencia Multinivel: EstudianteBecario

EstudianteBecario hereda de Estudiante, que ya hereda de Persona. Esto es herencia de 3 niveles.

4
Crear la clase EstudianteBecario
institucion_educativa.py — 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})"
FragmentoExplicació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.

👨‍🏫 Sección 6 — Clase Profesor (otra hija de Persona)

5
Crear la clase Profesor
institucion_educativa.py — Clase 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})"

🦎 Sección 7 — Pilar 4: Polimorfismo en Acción

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.

6
Función polimórfica: 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.

institucion_educativa.py — Función polimórfica

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)
7
Programa principal — Crear objetos y demostrar los 4 pilares
institucion_educativa.py — Programa principal

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)}")
ConceptoDónde se ve en el código
🎭 AbstracciónNo se puede crear Persona(...) directamente. presentarse() es abstracto.
🔒 Encapsulamientoest2.promedio = 7.2 usa el setter. est2.__promedio desde fuera daría error.
🧬 Herenciabecario.nombre usa atributo de Persona; becario.matricula usa atributo de Estudiante.
🦎 PolimorfismoEl 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.

▶️ Sección 8 — Ejecutar y Resultado Esperado

Guarda el archivo (Ctrl+S) y ejecuta:

Terminal
python institucion_educativa.py
🖥️ Salida esperada: 🏫 SISTEMA UTH — GESTIÓN DE PERSONAS ======================================================= 🎤 PRESENTACIONES (Polimorfismo): -------------------------------------------------- 🎓 Soy Ana López, estudiante de DSM. Matrícula: DSM2024001 | Promedio: 8.5 | ✅ Aprobado -------------------------------------------------- -------------------------------------------------- 🎓 Soy Carlos Mendoza, estudiante de DSM. Matrícula: DSM2024002 | Promedio: 5.9 | ❌ Reprobado -------------------------------------------------- -------------------------------------------------- 🎓 Soy María García, estudiante de DSM. Matrícula: DSM2024003 | Promedio: 9.1 | ✅ Aprobado 💰 Beca mensual: $3,500.00 -------------------------------------------------- -------------------------------------------------- 👨‍🏫 Soy el Prof. Ing. Bernardo Prado, especialidad: Programación Orientada a Objetos. Empleado #UTH-2001 | Salario: $25,000.00 -------------------------------------------------- 🔒 DEMOSTRACIÓN DE ENCAPSULAMIENTO: Promedio de Ana (getter): 8.5 Cambiando promedio de Carlos a 7.2... Nuevo promedio: 7.2 Intentando poner promedio = 15 (inválido)... ❌ Promedio inválido: 15. Debe estar entre 0.0 y 10.0 💰 DANDO AUMENTO AL PROFESOR: ✅ Aumento del 10% aplicado. Nuevo salario: $27,500.00 🎓 DATOS DEL ESTUDIANTE BECARIO: Nombre (de Persona): María García Matrícula (de Estudiante): DSM2024003 Beca (propio): $3,500.00 ¿Aprobada? (de Estudiante): True 🔍 VERIFICACIÓN DE TIPOS: ¿becario es Persona? True ¿becario es Estudiante? True ¿becario es EstudianteBecario? True ¿becario es Profesor? False
🎉 ¡Felicidades! Has implementado los 4 pilares de la POO en un sistema real.

✅ Lista de Verificación Final

🏠 Volver al Inicio del Curso