Objetivos de Aprendizaje
Al finalizar esta clase, serás capaz de:
- Entender qué son las clases abstractas
- Crear clases abstractas con
ABCy@abstractmethod - Definir interfaces para estandarizar comportamiento
- Implementar el patrón Repository para TaskFlow
Clases Abstractas (15 min)
Una clase abstracta es una clase que no puede instanciarse directamente. Sirve como "molde" para otras clases.
Analogía: No puedes crear un "Animal" genérico, pero sí un "Perro" o un "Gato". Animal sería abstracta.
¿Por qué usar clases abstractas?
- Forzar a las clases hijas a implementar ciertos métodos
- Definir una interfaz común
- Evitar instanciar clases incompletas
# Sin clases abstractas - PROBLEMA
class Animal:
def hacer_sonido(self):
# ¡No implementado!
pass
class Perro(Animal):
def hacer_sonido(self):
return "Guau"
class Gato(Animal):
# Olvidé implementar hacer_sonido!
pass
# Funciona pero puede causar errores
gato = Gato()
print(gato.hacer_sonido()) # None - ¿Error silencioso?
ABC y @abstractmethod (25 min)
Python usa el módulo abc (Abstract Base Classes) para crear clases abstractas.
from abc import ABC, abstractmethod
class Animal(ABC): # Hereda de ABC
"""Clase abstracta - no se puede instanciar."""
def __init__(self, nombre):
self.nombre = nombre
@abstractmethod
def hacer_sonido(self):
"""Método abstracto - DEBE implementarse en la hija."""
pass
@abstractmethod
def moverse(self):
"""Otro método abstracto."""
pass
def dormir(self):
"""Método normal - opcional implementar en hija."""
return f"{self.nombre} está durmiendo"
class Perro(Animal):
def hacer_sonido(self):
return "¡Guau!"
def moverse(self):
return f"{self.nombre} está corriendo en 4 patas"
class Gato(Animal):
def hacer_sonido(self):
return "¡Miau!"
def moverse(self):
return f"{self.nombre} está saltando silenciosamente"
# Uso
# animal = Animal("Genérico") # ❌ Error: No se puede instanciar clase abstracta
perro = Perro("Fido")
gato = Gato("Michi")
print(perro.hacer_sonido()) # "¡Guau!"
print(gato.hacer_sonido()) # "¡Miau!"
print(perro.dormir()) # "Fido está durmiendo" (heredado)
# Polimorfismo
animales = [perro, gato]
for a in animales:
print(f"{a.nombre}: {a.hacer_sonido()}")
Regla: Una clase hija DEBE implementar todos los métodos abstractos del padre, o también será abstracta y no podrá instanciarse.
Interfaces y Protocol (10 min)
Una interfaz define qué debe hacer una clase, no cómo. En Python moderno usamos Protocol.
from typing import Protocol
class Guardable(Protocol):
"""Interfaz para objetos que se pueden guardar."""
def guardar(self) -> None:
"""Guardar el objeto."""
...
def cargar(self, id: int) -> None:
"""Cargar el objeto por ID."""
...
# Cualquier clase que implemente guardar() y cargar()
# automáticamente cumple con la interfaz Guardable
class Usuario:
def __init__(self, username):
self.username = username
def guardar(self):
print(f"Guardando usuario {self.username}")
def cargar(self, id):
print(f"Cargando usuario con ID {id}")
class Proyecto:
def __init__(self, nombre):
self.nombre = nombre
def guardar(self):
print(f"Guardando proyecto {self.nombre}")
def cargar(self, id):
print(f"Cargando proyecto con ID {id}")
# Función que acepta cualquier objeto "Guardable"
def procesar_guardado(objeto: Guardable):
objeto.guardar()
# Uso
u = Usuario("juan")
p = Proyecto("Website")
procesar_guardado(u) # ✅ Funciona
procesar_guardado(p) # ✅ Funciona
Ejercicio: Repository Pattern (20 min)
Implementa el patrón Repository para TaskFlow usando clases abstractas.
CRUD: Sigla para Create, Read, Update, Delete (Crear, Leer, Actualizar, Eliminar), que representa las cuatro operaciones básicas de la gestión de datos.
# repository.py
from abc import ABC, abstractmethod
from typing import List, Optional
class Usuario:
"""Modelo simple de usuario."""
def __init__(self, id: int, username: str, email: str):
self.id = id
self.username = username
self.email = email
def __repr__(self):
return f"Usuario({self.id}, {self.username})"
class Repository(ABC):
"""
Interfaz abstracta para repositorios.
Define las operaciones CRUD que debe implementar cualquier repositorio.
"""
@abstractmethod
def crear(self, usuario: Usuario) -> Usuario:
"""Crea un nuevo usuario."""
pass
@abstractmethod
def obtener_por_id(self, id: int) -> Optional[Usuario]:
"""Obtiene un usuario por ID."""
pass
@abstractmethod
def obtener_todos(self) -> List[Usuario]:
"""Obtiene todos los usuarios."""
pass
@abstractmethod
def actualizar(self, usuario: Usuario) -> Usuario:
"""Actualiza un usuario existente."""
pass
@abstractmethod
def eliminar(self, id: int) -> bool:
"""Elimina un usuario por ID."""
pass
class RepositorioEnMemoria(Repository):
"""
Implementación del repositorio en memoria (para pruebas).
"""
def __init__(self):
self._usuarios = {}
self._contador_id = 1
def crear(self, usuario: Usuario) -> Usuario:
usuario.id = self._contador_id
self._usuarios[usuario.id] = usuario
self._contador_id += 1
return usuario
def obtener_por_id(self, id: int) -> Optional[Usuario]:
return self._usuarios.get(id)
def obtener_todos(self) -> List[Usuario]:
return list(self._usuarios.values())
def actualizar(self, usuario: Usuario) -> Usuario:
if usuario.id not in self._usuarios:
raise ValueError(f"Usuario {usuario.id} no encontrado")
self._usuarios[usuario.id] = usuario
return usuario
def eliminar(self, id: int) -> bool:
if id in self._usuarios:
del self._usuarios[id]
return True
return False
class RepositorioArchivo(Repository):
"""
Implementación del repositorio en archivo (simulado).
"""
def __init__(self, archivo: str):
self._archivo = archivo
self._usuarios = {}
self._contador_id = 1
self._cargar_desde_archivo()
def _cargar_desde_archivo(self):
"""Simula cargar desde archivo."""
# En un caso real: leer JSON/CSV
pass
def _guardar_en_archivo(self):
"""Simula guardar en archivo."""
# En un caso real: escribir JSON/CSV
pass
def crear(self, usuario: Usuario) -> Usuario:
usuario.id = self._contador_id
self._usuarios[usuario.id] = usuario
self._contador_id += 1
self._guardar_en_archivo()
return usuario
def obtener_por_id(self, id: int) -> Optional[Usuario]:
return self._usuarios.get(id)
def obtener_todos(self) -> List[Usuario]:
return list(self._usuarios.values())
def actualizar(self, usuario: Usuario) -> Usuario:
if usuario.id not in self._usuarios:
raise ValueError(f"Usuario {usuario.id} no encontrado")
self._usuarios[usuario.id] = usuario
self._guardar_en_archivo()
return usuario
def eliminar(self, id: int) -> bool:
if id in self._usuarios:
del self._usuarios[id]
self._guardar_en_archivo()
return True
return False
# === PRUEBAS ===
if __name__ == "__main__":
# No podemos instanciar Repository directamente
# repo = Repository() # ❌ Error
# Pero sí las implementaciones concretas
print("=== Repositorio en Memoria ===")
repo_memoria = RepositorioEnMemoria()
# Crear usuarios
u1 = repo_memoria.crear(Usuario(None, "juan", "juan@email.com"))
u2 = repo_memoria.crear(Usuario(None, "ana", "ana@email.com"))
print(f"Creados: {u1}, {u2}")
# Obtener todos
todos = repo_memoria.obtener_todos()
print(f"Total usuarios: {len(todos)}")
# Obtener por ID
encontrado = repo_memoria.obtener_por_id(1)
print(f"Encontrado: {encontrado}")
# Actualizar
u1.email = "juan.nuevo@email.com"
repo_memoria.actualizar(u1)
print(f"Actualizado: {repo_memoria.obtener_por_id(1)}")
# Eliminar
repo_memoria.eliminar(2)
print(f"Después de eliminar: {len(repo_memoria.obtener_todos())} usuarios")
print("\n=== Polimorfismo con Repository ===")
def probar_repositorio(repo: Repository):
"""Funciona con cualquier implementación de Repository."""
u = repo.crear(Usuario(None, "test", "test@email.com"))
print(f"Creado en {type(repo).__name__}: {u}")
return u
# Funciona con ambas implementaciones
probar_repositorio(RepositorioEnMemoria())
probar_repositorio(RepositorioArchivo("usuarios.txt"))
Conexión con TaskFlow: El patrón Repository separa la lógica de negocio del acceso a datos. Podemos cambiar de "en memoria" a "base de datos" sin modificar el código que usa el repositorio.
Resumen y Preparación E1
Unidad 1: POO en Python
- Clases y Objetos: __init__, self, atributos, métodos
- Encapsulamiento: _, __, @property, getters/setters
- Herencia: class Hija(Padre), super(), sobreescritura
- Polimorfismo: Mismo método, distinto comportamiento
- Clases Abstractas: ABC, @abstractmethod, interfaces
Tips para el Examen E1 (15%)
- Practica crear clases completas con __init__, atributos y métodos
- Domina @property para encapsulamiento
- Sabe crear jerarquías con herencia y super()
- Entiende cuándo usar clases abstractas
- Lee atentamente los enunciados antes de escribir código
¡Éxito en el examen! Recuerda: la POO es el fundamento de todo el curso. Domina estos conceptos y el resto será más fácil.