IF0100 - Programación OO II

Unidad 1: Programación Orientada a Objetos

Clase 1: Clases y Objetos

Martes, 17 de febrero de 2026 Semana 3 - Martes (120 minutos) 60 min Teoría 60 min Práctica

Próxima Entrega: E1 - Dominio POO (26/02/2026)

Objetivos de Aprendizaje

Al finalizar esta clase, serás capaz de:

  • Definir clases en Python usando class
  • Crear objetos (instancias) de una clase
  • Usar __init__ para inicializar objetos
  • Comprender el propósito de self
  • Crear métodos para definir comportamientos
  • Implementar el modelo Usuario para TaskFlow
Video Recomendado

Introducción a la Programación Orientada a Objetos en Python:

Ver "POO en Python - Introducción" 16:46 min | Sergio A. Castaño Giraldo

¿Qué es una Clase? (10 min)

La POO (Programación Orientada a Objetos) es un paradigma basado en objetos. Una clase es un molde o plantilla para crear objetos. Define atributos (datos) y métodos (comportamientos).

Analogía: Una clase es como un plano de una casa, mientras que un objeto es una casa construida a partir de ese plano.

Sintaxis Básica

class NombreClase:
    """Descripción de la clase."""
    
    # Atributo de clase (compartido por todos los objetos)
    atributo_clase = "valor"
    
    def __init__(self, parametro1, parametro2):
        """Constructor: inicializa el objeto."""
        # Atributos de instancia (únicos para cada objeto)
        self.atributo1 = parametro1
        self.atributo2 = parametro2
    
    def metodo(self):
        """Método de instancia."""
        return f"Atributo 1: {self.atributo1}"

# Crear objetos (instancias)
objeto1 = NombreClase("valor1", "valor2")
objeto2 = NombreClase("otro1", "otro2")

print(objeto1.atributo1)  # "valor1"
print(objeto2.atributo1)  # "otro1"

Ejemplo: Clase Perro

class Perro:
    """Representa un perro con nombre y edad."""
    
    especie = "Canis familiaris"  # Atributo de clase
    
    def __init__(self, nombre, edad):
        self.nombre = nombre  # Atributo de instancia
        self.edad = edad      # Atributo de instancia
    
    def ladrar(self):
        return f"{self.nombre} dice: ¡Guau!"
    
    def es_cachorro(self):
        return self.edad < 2

# Crear perros
mi_perro = Perro("Fido", 3)
otro_perro = Perro("Rex", 1)

print(mi_perro.ladrar())       # "Fido dice: ¡Guau!"
print(mi_perro.es_cachorro())  # False
print(otro_perro.es_cachorro()) # True

Constructor __init__ (15 min)

El método __init__ se ejecuta automáticamente cuando se crea un objeto. Sirve para inicializar los atributos.

Ejemplo: Clase Persona

class Persona:
    def __init__(self, nombre, edad, email=None):
        """Constructor: inicializa una persona."""
        self.nombre = nombre
        self.edad = edad
        self.email = email
        self.activo = True  # Valor por defecto
    
    def presentarse(self):
        return f"Hola, soy {self.nombre} y tengo {self.edad} años."

# Crear personas
juan = Persona("Juan", 25, "juan@email.com")
ana = Persona("Ana", 30)

print(juan.presentarse())
print(f"Email de Juan: {juan.email}")
print(f"Email de Ana: {ana.email}")  # None

Valores por Defecto

class Producto:
    def __init__(self, nombre, precio, cantidad=0):
        self.nombre = nombre
        self.precio = precio
        self.cantidad = cantidad  # Si no se pasa, es 0

# Uso
p1 = Producto("Laptop", 1000, 5)  # cantidad = 5
p2 = Producto("Mouse", 25)         # cantidad = 0 (por defecto)

print(f"{p1.nombre}: {p1.cantidad} unidades")
print(f"{p2.nombre}: {p2.cantidad} unidades")

El parámetro self (10 min)

self representa la instancia actual del objeto. Permite acceder a sus atributos y métodos.

Importante: Aunque self es el primer parámetro de los métodos, NO lo pasas al llamar el método. Python lo hace automáticamente.
class CuentaBancaria:
    def __init__(self, titular, saldo_inicial=0):
        self.titular = titular
        self.saldo = saldo_inicial
    
    def depositar(self, cantidad):
        """self permite modificar los atributos del objeto."""
        if cantidad > 0:
            self.saldo += cantidad
            return f"Depósito exitoso. Nuevo saldo: ${self.saldo}"
        return "Cantidad inválida"
    
    def retirar(self, cantidad):
        """self permite leer y modificar atributos."""
        if cantidad <= self.saldo:
            self.saldo -= cantidad
            return f"Retiro exitoso. Nuevo saldo: ${self.saldo}"
        return "Saldo insuficiente"
    
    def consultar_saldo(self):
        """self permite leer atributos."""
        return f"Saldo de {self.titular}: ${self.saldo}"

# Uso - ¡No pasamos self!
cuenta = CuentaBancaria("Juan", 1000)
print(cuenta.consultar_saldo())  # Saldo de Juan: $1000
print(cuenta.depositar(500))     # Depósito exitoso...
print(cuenta.retirar(200))       # Retiro exitoso...
print(cuenta.consultar_saldo())  # Saldo de Juan: $1300

Métodos Especiales (10 min)

Python tiene métodos especiales (dunder methods) que comienzan y terminan con __.

Método Cuándo se usa Ejemplo
__init__ Crear objeto obj = Clase()
__str__ Imprimir objeto print(obj)
__repr__ Representación técnica repr(obj)
__eq__ Comparar objetos obj1 == obj2
class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        """Representación amigable para usuarios."""
        return f"({self.x}, {self.y})"
    
    def __repr__(self):
        """Representación técnica para debugging."""
        return f"Punto(x={self.x}, y={self.y})"
    
    def __eq__(self, otro):
        """Comparar dos puntos."""
        return self.x == otro.x and self.y == otro.y

p1 = Punto(3, 4)
p2 = Punto(3, 4)
p3 = Punto(1, 2)

print(p1)           # (3, 4) - usa __str__
print(repr(p1))     # Punto(x=3, y=4) - usa __repr__
print(p1 == p2)     # True - usa __eq__
print(p1 == p3)     # False
Ejercicio Rápido 1: Clase Rectángulo (5 min)

Crea una clase Rectángulo que tenga:

  1. Atributos: base y altura
  2. Método area() que retorne base × altura
  3. Método perimetro() que retorne 2×(base+altura)
  4. Método __str__ que muestre "Rectángulo de base=X y altura=Y"

Crea 2 rectángulos diferentes y compara sus áreas.

Ejercicio: Clase Usuario (25 min)

Vamos a crear el modelo Usuario para el proyecto TaskFlow.

Requisitos

  • Atributos: username, email, nombre_completo (opcional)
  • Métodos: presentarse(), es_valido(), activar(), desactivar()
  • Validar que username tenga al menos 3 caracteres
  • Validar que email contenga "@"
# usuario.py
class Usuario:
    """Representa un usuario en el sistema TaskFlow."""
    
    def __init__(self, username, email, nombre_completo=None):
        """Inicializa un usuario."""
        self.username = username
        self.email = email
        self.nombre_completo = nombre_completo
        self.activo = True
    
    def __str__(self):
        return f"@{self.username}"
    
    def __repr__(self):
        return f"Usuario('{self.username}', '{self.email}')"
    
    def presentarse(self):
        """Retorna una presentación del usuario."""
        if self.nombre_completo:
            return f"Hola, soy {self.nombre_completo} (@{self.username})"
        return f"Hola, soy @{self.username}"
    
    def es_valido(self):
        """Valida los datos del usuario."""
        if len(self.username) < 3:
            return False, "Username debe tener al menos 3 caracteres"
        
        if "@" not in self.email:
            return False, "Email debe contener @"
        
        return True, "Válido"
    
    def activar(self):
        """Activa la cuenta del usuario."""
        self.activo = True
    
    def desactivar(self):
        """Desactiva la cuenta del usuario."""
        self.activo = False
    
    def esta_activo(self):
        """Retorna True si el usuario está activo."""
        return self.activo


# === PRUEBAS ===
if __name__ == "__main__":
    # Crear usuarios
    usuario1 = Usuario("juan123", "juan@example.com", "Juan Pérez")
    usuario2 = Usuario("ana", "ana@example.com")
    usuario3 = Usuario("ab", "email-invalido")  # Inválido
    
    # Probar presentación
    print(usuario1.presentarse())
    print(usuario2.presentarse())
    
    # Probar validación
    for u in [usuario1, usuario2, usuario3]:
        valido, mensaje = u.es_valido()
        estado = "✅" if valido else "❌"
        print(f"{estado} {u}: {mensaje}")
    
    # Probar activación
    usuario1.desactivar()
    print(f"\n{usuario1} activo: {usuario1.esta_activo()}")
    usuario1.activar()
    print(f"{usuario1} activo: {usuario1.esta_activo()}")
    
    # Mostrar representación
    print(f"\nRepresentación: {repr(usuario1)}")
Conexión con TaskFlow: Esta clase Usuario es la base del sistema. En las siguientes clases agregaremos: encapsulamiento (propiedades), herencia (tipos de usuarios), y persistencia (guardar en base de datos).

Retos Adicionales

  1. Agrega atributo fecha_registro que se establezca automáticamente
  2. Agrega método cambiar_email(nuevo_email) con validación
  3. Agrega método to_dict() que retorne un diccionario con los datos

Laboratorio 2: Sistema de Cuentas Bancarias (25 min)

Crea un sistema de cuentas bancarias con múltiples cuentas y transferencias entre ellas.

Requisitos

  • Clase CuentaBancaria con número de cuenta único, titular y saldo
  • Métodos: depositar(), retirar(), transferir(), consultar_saldo()
  • Historial de transacciones (lista de diccionarios)
  • Validaciones: saldo insuficiente, montos negativos
  • Interfaz de menú para gestionar múltiples cuentas
# sistema_bancario.py
import random
import datetime

class CuentaBancaria:
    """Representa una cuenta bancaria."""
    
    def __init__(self, titular, saldo_inicial=0):
        self.numero = f"10{random.randint(10000000, 99999999)}"
        self.titular = titular
        self.saldo = saldo_inicial
        self.transacciones = []
        
        # Registrar saldo inicial
        if saldo_inicial > 0:
            self._registrar_transaccion("APERTURA", saldo_inicial, "Depósito inicial")
    
    def _registrar_transaccion(self, tipo, monto, descripcion):
        """Registra una transacción en el historial."""
        transaccion = {
            "fecha": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "tipo": tipo,
            "monto": monto,
            "descripcion": descripcion,
            "saldo_resultante": self.saldo
        }
        self.transacciones.append(transaccion)
    
    def depositar(self, monto, descripcion="Depósito"):
        """Deposita dinero en la cuenta."""
        if monto <= 0:
            return False, "El monto debe ser positivo"
        
        self.saldo += monto
        self._registrar_transaccion("DEPÓSITO", monto, descripcion)
        return True, f"Depósito exitoso. Nuevo saldo: ${self.saldo:.2f}"
    
    def retirar(self, monto, descripcion="Retiro"):
        """Retira dinero de la cuenta."""
        if monto <= 0:
            return False, "El monto debe ser positivo"
        
        if monto > self.saldo:
            return False, "Saldo insuficiente"
        
        self.saldo -= monto
        self._registrar_transaccion("RETIRO", monto, descripcion)
        return True, f"Retiro exitoso. Nuevo saldo: ${self.saldo:.2f}"
    
    def transferir(self, monto, cuenta_destino, descripcion="Transferencia"):
        """Transfiere dinero a otra cuenta."""
        if monto <= 0:
            return False, "El monto debe ser positivo"
        
        if monto > self.saldo:
            return False, "Saldo insuficiente para transferencia"
        
        # Retirar de esta cuenta
        self.saldo -= monto
        self._registrar_transaccion(
            "TRANSFERENCIA SALIDA", 
            monto, 
            f"{descripcion} a cuenta {cuenta_destino.numero}"
        )
        
        # Depositar en cuenta destino
        cuenta_destino.saldo += monto
        cuenta_destino._registrar_transaccion(
            "TRANSFERENCIA ENTRADA",
            monto,
            f"{descripcion} desde cuenta {self.numero}"
        )
        
        return True, f"Transferencia exitosa. Nuevo saldo: ${self.saldo:.2f}"
    
    def consultar_saldo(self):
        """Retorna el saldo actual."""
        return self.saldo
    
    def obtener_historial(self):
        """Muestra el historial de transacciones."""
        return self.transacciones
    
    def __str__(self):
        return f"Cuenta {self.numero} - {self.titular}: ${self.saldo:.2f}"
    
    def __repr__(self):
        return f"CuentaBancaria(numero='{self.numero}', titular='{self.titular}', saldo={self.saldo})"


# Sistema de gestión de cuentas
class SistemaBancario:
    """Gestiona múltiples cuentas bancarias."""
    
    def __init__(self):
        self.cuentas = {}
    
    def crear_cuenta(self, titular, saldo_inicial=0):
        """Crea una nueva cuenta."""
        cuenta = CuentaBancaria(titular, saldo_inicial)
        self.cuentas[cuenta.numero] = cuenta
        print(f"✅ Cuenta creada: {cuenta.numero}")
        return cuenta
    
    def obtener_cuenta(self, numero):
        """Busca una cuenta por número."""
        return self.cuentas.get(numero)
    
    def listar_cuentas(self):
        """Lista todas las cuentas."""
        if not self.cuentas:
            print("📭 No hay cuentas registradas")
            return
        
        print("\n📊 CUENTAS REGISTRADAS:")
        print("=" * 60)
        for cuenta in self.cuentas.values():
            print(f"  • {cuenta}")
        print("=" * 60)


# Programa principal
sistema = SistemaBancario()

while True:
    print("\n" + "="*50)
    print("🏦 SISTEMA BANCARIO")
    print("="*50)
    print("1. Crear cuenta")
    print("2. Listar cuentas")
    print("3. Depositar")
    print("4. Retirar")
    print("5. Transferir")
    print("6. Consultar saldo")
    print("7. Ver historial")
    print("8. Salir")
    
    opcion = input("\nSelecciona una opción: ")
    
    if opcion == "1":
        titular = input("Nombre del titular: ").strip()
        try:
            saldo = float(input("Saldo inicial: $"))
            sistema.crear_cuenta(titular, saldo)
        except ValueError:
            print("❌ Saldo inválido")
    
    elif opcion == "2":
        sistema.listar_cuentas()
    
    elif opcion == "3":
        num = input("Número de cuenta: ").strip()
        cuenta = sistema.obtener_cuenta(num)
        if cuenta:
            try:
                monto = float(input("Monto a depositar: $"))
                exito, mensaje = cuenta.depositar(monto)
                print(f"{'✅' if exito else '❌'} {mensaje}")
            except ValueError:
                print("❌ Monto inválido")
        else:
            print("❌ Cuenta no encontrada")
    
    elif opcion == "4":
        num = input("Número de cuenta: ").strip()
        cuenta = sistema.obtener_cuenta(num)
        if cuenta:
            try:
                monto = float(input("Monto a retirar: $"))
                exito, mensaje = cuenta.retirar(monto)
                print(f"{'✅' if exito else '❌'} {mensaje}")
            except ValueError:
                print("❌ Monto inválido")
        else:
            print("❌ Cuenta no encontrada")
    
    elif opcion == "5":
        num_origen = input("Cuenta origen: ").strip()
        num_destino = input("Cuenta destino: ").strip()
        
        cuenta_origen = sistema.obtener_cuenta(num_origen)
        cuenta_destino = sistema.obtener_cuenta(num_destino)
        
        if cuenta_origen and cuenta_destino:
            try:
                monto = float(input("Monto a transferir: $"))
                exito, mensaje = cuenta_origen.transferir(monto, cuenta_destino)
                print(f"{'✅' if exito else '❌'} {mensaje}")
            except ValueError:
                print("❌ Monto inválido")
        else:
            print("❌ Una o ambas cuentas no existen")
    
    elif opcion == "6":
        num = input("Número de cuenta: ").strip()
        cuenta = sistema.obtener_cuenta(num)
        if cuenta:
            print(f"\n💰 Saldo actual: ${cuenta.consultar_saldo():.2f}")
        else:
            print("❌ Cuenta no encontrada")
    
    elif opcion == "7":
        num = input("Número de cuenta: ").strip()
        cuenta = sistema.obtener_cuenta(num)
        if cuenta:
            historial = cuenta.obtener_historial()
            if historial:
                print(f"\n📜 HISTORIAL DE {cuenta.titular}:")
                print("=" * 80)
                print(f"{'Fecha':<20} {'Tipo':<20} {'Monto':<12} {'Descripción'}")
                print("=" * 80)
                for t in historial:
                    print(f"{t['fecha']:<20} {t['tipo']:<20} ${t['monto']:<11.2f} {t['descripcion']}")
                print("=" * 80)
            else:
                print("📭 No hay transacciones")
        else:
            print("❌ Cuenta no encontrada")
    
    elif opcion == "8":
        print("👋 ¡Hasta luego!")
        break
    
    else:
        print("❌ Opción no válida")
Conexión con TaskFlow: Este sistema usa clases, validaciones, historial de operaciones y múltiples objetos relacionados, conceptos fundamentales para el modelo de dominio de TaskFlow.

Retos Adicionales

  1. Interés: Agrega método para calcular y aplicar interés mensual
  2. Límites: Establece límites diarios de retiro por cuenta
  3. Estadísticas: Calcula total de dinero en el sistema bancario
  4. Reportes: Genera reporte de cuentas con saldo negativo

Git: Ramas (Branches) y Merge (20 min)

Las ramas te permiten desarrollar funcionalidades aisladas sin afectar el código principal.

¿Qué son las Ramas?

Analogía: Imagina que estás escribiendo un libro. La rama main es la versión publicada. Cuando quieres agregar un capítulo nuevo, creas una copia (rama), trabajas en ella, y cuando está listo la fusionas (merge) con el libro principal.

Comandos de Ramas

# Ver en qué rama estás
git branch

# Crear nueva rama
git branch nombre-rama

# Cambiar a una rama
git checkout nombre-rama

# Crear y cambiar en un solo paso
git checkout -b nombre-rama

# Ver todas las ramas (locales y remotas)
git branch -a

Flujo de Trabajo con Ramas

# 1. Estás en main y todo funciona
git checkout main

# 2. Creas rama para nueva funcionalidad
git checkout -b feature/clase-usuario

# 3. Trabajas en tu código (creas archivos, modificas, etc.)
# ... código código código ...

# 4. Guardas cambios en la rama
git add .
git commit -m "feat: agregada clase Usuario con validaciones"

# 5. Vuelves a main
git checkout main

# 6. Fusionas la rama
git merge feature/clase-usuario

# 7. Opcional: eliminar rama ya fusionada
git branch -d feature/clase-usuario
Ejercicio Práctico: Tu Primera Rama (15 min)

Vamos a crear una nueva funcionalidad usando ramas:

  1. En tu repo local: git status (deberías estar en main)
  2. Crea rama: git checkout -b feature/clase-cuenta-bancaria
  3. Crea el archivo clase-03/cuenta_bancaria.py con la clase CuentaBancaria
  4. Prueba que funcione
  5. Agrega a Git: git add .
  6. Commit: git commit -m "feat: agregada clase CuentaBancaria"
  7. Vuelve a main: git checkout main
  8. Fusiona: git merge feature/clase-cuenta-bancaria
  9. Sube a GitHub: git push
  10. Verifica en GitHub que aparecen los archivos

Resolución de Conflictos Básica

A veces al hacer merge aparecen conflictos cuando dos personas modificaron las mismas líneas:

# Intentas hacer merge
$ git merge otra-rama

# Aparece conflicto
Auto-merging archivo.py
CONFLICT (content): Merge conflict in archivo.py
Automatic merge failed; fix conflicts and then commit the result.

# Abres el archivo y verás marcadores:
<<<<<<< HEAD
print("Mi versión")
=======
print("Otra versión")
>>>>>>> otra-rama

# Editas y eliges qué quedarse
print("Versión final combinada")

# Guardas y completas el merge
git add archivo.py
git commit -m "Merge: resuelto conflicto en archivo.py"
Tip: Los conflictos son normales en trabajo colaborativo. Siempre revisa cuidadosamente qué código conservar.

Git Ignore para Python

Actualiza tu .gitignore para proyectos Python más profesionales:

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Entornos virtuales
venv/
ENV/
env/
.venv

# IDEs
.vscode/
.idea/
*.swp
*.swo

# Archivos de sistema
.DS_Store
Thumbs.db

# Variables de entorno
.env
.env.local
Ejercicio: Flujo Completo (10 min)

Practica el flujo profesional completo:

  1. Crea una rama feature/mejora-readme
  2. Mejora tu README.md con emojis y mejor formato
  3. Commit: git commit -m "docs: mejorado README con formato profesional"
  4. Merge a main
  5. Push a GitHub
  6. Verifica que el README se ve bien en GitHub
Convención de Nombres de Ramas:
  • feature/nueva-funcionalidad - Nueva característica
  • fix/correccion-bug - Corrección de error
  • docs/actualizacion-readme - Documentación
  • refactor/mejorar-codigo - Refactorización

Ejercicios Adicionales (10 min)

Ejercicio 2: Clase Libro

Crea una clase Libro con:

  • Atributos: título, autor, ISBN, año, disponible (bool)
  • Métodos: prestar(), devolver(), info()
  • __str__ que muestre "Título por Autor (Año)"
Ejercicio 3: Clase Estudiante

Crea una clase Estudiante con:

  • Lista de calificaciones
  • Métodos: agregar_nota(), promedio(), aprobo()
  • Método para mostrar todas las notas

Resumen

Python - POO

  • Clase: Plantilla que define atributos y métodos
  • Objeto: Instancia creada a partir de una clase
  • __init__: Constructor que inicializa el objeto
  • self: Referencia al objeto actual (no se pasa al llamar)
  • Atributos: Variables que almacenan datos del objeto
  • Métodos: Funciones que definen comportamientos del objeto

Git - Ramas

Comando Función
git branch Ver ramas existentes
git checkout -b nombre Crear y cambiar a rama
git checkout main Cambiar a main
git merge rama Fusionar rama a actual
git branch -d rama Eliminar rama fusionada
Para el examen E1: Practica crear clases con __init__, atributos y métodos. Será parte de la evaluación.
← Anterior: Estructuras de Datos
Clase 3 de 25
Siguiente: Encapsulamiento →