En este capítulo, nos sumergimos en un proyecto práctico que consolida los fundamentos de la programación orientada a objetos (OOP) en Python. Construiremos un gestor de tareas completo, una aplicación que permite crear, gestionar y priorizar tareas de manera eficiente. Usaremos clases como Tarea y TaskManager, incorporaremos herencia para diferenciar tipos de tareas (por ejemplo, tareas urgentes o recurrentes), implementaremos properties para un control seguro de los atributos, y definiremos métodos dunder como __repr__ para una representación clara de los objetos. Este proyecto no solo refuerza conceptos clave de OOP, sino que te equipa con habilidades para diseñar sistemas modulares y escalables, similares a herramientas cotidianas como listas de tareas en apps móviles.
Construyendo la Clase Base: Tarea
Comencemos por el núcleo de nuestro gestor: la clase Tarea. Imagina que una tarea es como una nota adhesiva en tu escritorio – tiene un título, una descripción, una fecha límite y un estado (pendiente o completada). En OOP, encapsulamos estos elementos en una clase para que sean reutilizables y fáciles de manejar.
La clase Tarea servirá como base para otros tipos de tareas más especializadas, gracias a la herencia. Antes de codificar, explico cada parte: usaremos un inicializador (__init__) para configurar los atributos, properties para acceder y modificarlos de forma controlada (evitando valores inválidos, como una fecha límite en el pasado), y el dunder __repr__ para que, al imprimir un objeto, veamos una representación legible en lugar de algo críptico como <Tarea object at 0x123>.
Aquí está el código inicial para Tarea. Guarda esto en un archivo llamado gestor_tareas.py.
import datetime # Para manejar fechas de manera precisa
class Tarea:
def __init__(self, titulo, descripcion, fecha_limite=None):
"""Inicializa una tarea con título, descripción y fecha límite opcional."""
self._titulo = titulo # Atributo privado para encapsulación
self._descripcion = descripcion
self._fecha_limite = fecha_limite if fecha_limite else datetime.date.today() + datetime.timedelta(days=7)
self._completada = False # Por defecto, la tarea está pendiente
@property
def titulo(self):
"""Property para obtener el título de la tarea."""
return self._titulo
@titulo.setter
def titulo(self, nuevo_titulo):
"""Setter para validar y actualizar el título (debe ser no vacío)."""
if not nuevo_titulo:
raise ValueError("El título no puede estar vacío.")
self._titulo = nuevo_titulo
@property
def descripcion(self):
"""Property para obtener la descripción."""
return self._descripcion
@descripcion.setter
def descripcion(self, nueva_descripcion):
"""Setter para actualizar la descripción."""
self._descripcion = nueva_descripcion
@property
def fecha_limite(self):
"""Property para obtener la fecha límite."""
return self._fecha_limite
@fecha_limite.setter
def fecha_limite(self, nueva_fecha):
"""Setter para validar que la fecha límite no sea en el pasado."""
if nueva_fecha < datetime.date.today():
raise ValueError("La fecha límite no puede ser en el pasado.")
self._fecha_limite = nueva_fecha
@property
def completada(self):
"""Property para verificar si la tarea está completada."""
return self._completada
def marcar_completada(self):
"""Método para marcar la tarea como completada."""
self._completada = True
def __repr__(self):
"""Representación legible del objeto Tarea."""
estado = "Completada" if self._completada else "Pendiente"
return f"Tarea('{self._titulo}', '{self._descripcion}', límite: {self._fecha_limite}, estado: {estado})"
PythonPara ejecutar esto, crea una instancia en el mismo archivo o en un script separado. Por ejemplo, agrega al final de gestor_tareas.py:
if __name__ == "__main__":
tarea = Tarea("Comprar leche", "Ir al supermercado")
print(tarea) # Salida: Tarea('Comprar leche', 'Ir al supermercado', límite: [fecha futura], estado: Pendiente)
PythonEjecútalo con python gestor_tareas.py. Observa cómo las properties protegen los atributos: intenta tarea.titulo = "" y verás un error, lo cual es como un guardián que asegura la integridad de tus datos.
Extendiendo con Herencia: Tipos de Tareas Especializadas
La herencia es como heredar rasgos familiares: una clase hija hereda comportamientos de la base pero puede agregar o modificarlos. Crearemos dos subclases de Tarea: TareaUrgente (con prioridad alta y notificaciones) y TareaRecurrente (que se repite automáticamente al completarse).
Explico paso a paso: la herencia permite reutilizar código de Tarea sin duplicarlo. Sobrescribiremos __init__ para agregar atributos específicos y __repr__ para una representación personalizada. Esto hace nuestro gestor más flexible, como diferenciar tareas diarias de emergencias en una app real.
Agrega esto al mismo archivo gestor_tareas.py:
class TareaUrgente(Tarea):
def __init__(self, titulo, descripcion, fecha_limite=None, prioridad="Alta"):
"""Inicializa una tarea urgente con prioridad adicional."""
super().__init__(titulo, descripcion, fecha_limite) # Llama al init de la clase base
self._prioridad = prioridad # Atributo específico para urgencia
@property
def prioridad(self):
"""Property para obtener la prioridad."""
return self._prioridad
@prioridad.setter
def prioridad(self, nueva_prioridad):
"""Setter para validar niveles de prioridad (Alta, Media, Baja)."""
if nueva_prioridad not in ["Alta", "Media", "Baja"]:
raise ValueError("Prioridad inválida. Debe ser Alta, Media o Baja.")
self._prioridad = nueva_prioridad
def notificar(self):
"""Método específico: Simula una notificación para tareas urgentes."""
print(f"Notificación: ¡Tarea urgente '{self.titulo}' con prioridad {self.prioridad} pendiente!")
def __repr__(self):
"""Representación personalizada para TareaUrgente."""
return f"TareaUrgente('{self.titulo}', '{self.descripcion}', límite: {self.fecha_limite}, prioridad: {self.prioridad}, estado: {'Completada' if self.completada else 'Pendiente'})"
class TareaRecurrente(Tarea):
def __init__(self, titulo, descripcion, fecha_limite=None, intervalo_dias=7):
"""Inicializa una tarea recurrente con intervalo de repetición."""
super().__init__(titulo, descripcion, fecha_limite)
self._intervalo_dias = intervalo_dias # Días para repetir la tarea
@property
def intervalo_dias(self):
"""Property para obtener el intervalo de repetición."""
return self._intervalo_dias
@intervalo_dias.setter
def intervalo_dias(self, nuevo_intervalo):
"""Setter para validar que el intervalo sea positivo."""
if nuevo_intervalo <= 0:
raise ValueError("El intervalo debe ser un número positivo de días.")
self._intervalo_dias = nuevo_intervalo
def marcar_completada(self):
"""Sobrescribe para resetear la fecha límite al completarse."""
super().marcar_completada() # Llama al método base
self._completada = False # Reinicia el estado
self._fecha_limite += datetime.timedelta(days=self._intervalo_dias) # Avanza la fecha
def __repr__(self):
"""Representación personalizada para TareaRecurrente."""
return f"TareaRecurrente('{self.titulo}', '{self.descripcion}', límite: {self.fecha_limite}, intervalo: {self.intervalo_dias} días, estado: {'Completada' if self.completada else 'Pendiente'})"
PythonPrueba agregando al final: tarea_urgente = TareaUrgente("Llamar al médico", "Cita urgente") ; print(tarea_urgente) ; tarea_urgente.notificar(). Ejecuta con python gestor_tareas.py. Nota cómo la herencia mantiene el código DRY (Don’t Repeat Yourself), evitando copiar métodos como marcar_completada.
Integrando Todo: La Clase TaskManager
Ahora, unimos todo en TaskManager, que actúa como el “jefe” de las tareas – una lista inteligente que agrega, elimina y lista tareas. Es como un cuaderno digital que organiza tus notas adhesivas.
Explico: esta clase no hereda, pero compone (usa) objetos de Tarea y sus subclases. Usaremos una lista interna para almacenar tareas, métodos para manipularla, y __repr__ para una vista general.
Agrega esto a gestor_tareas.py:
class TaskManager:
def __init__(self):
"""Inicializa el gestor con una lista vacía de tareas."""
self._tareas = [] # Lista privada para almacenar tareas
def agregar_tarea(self, tarea):
"""Agrega una tarea al gestor."""
if not isinstance(tarea, Tarea):
raise ValueError("Solo se pueden agregar objetos de tipo Tarea o sus subclases.")
self._tareas.append(tarea)
def eliminar_tarea(self, titulo):
"""Elimina una tarea por título."""
self._tareas = [t for t in self._tareas if t.titulo != titulo]
def listar_tareas(self):
"""Devuelve una lista de representaciones de tareas."""
return [repr(t) for t in self._tareas]
def __repr__(self):
"""Representación del gestor con conteo de tareas."""
return f"TaskManager con {len(self._tareas)} tareas."
PythonPara un ejemplo completo, agrega:
if __name__ == "__main__":
manager = TaskManager()
tarea1 = Tarea("Estudiar Python", "Capítulo 32")
tarea2 = TareaUrgente("Pagar facturas", "Antes de fin de mes")
tarea3 = TareaRecurrente("Hacer ejercicio", "Rutina diaria", intervalo_dias=1)
manager.agregar_tarea(tarea1)
manager.agregar_tarea(tarea2)
manager.agregar_tarea(tarea3)
print(manager) # Salida: TaskManager con 3 tareas.
print(manager.listar_tareas()) # Lista las representaciones de cada tarea
tarea3.marcar_completada() # Demuestra la recurrencia
print(tarea3) # Muestra la fecha actualizada
PythonEjecuta con python gestor_tareas.py. Has construido un sistema completo: crea tareas variadas, agrégalas al manager, y manipúlalas. Si intentas agregar algo no válido, las validaciones (de properties y métodos) lo previenen.
Optimizando y Reflexionando sobre el Diseño OOP
Hemos optimizado el código para legibilidad y eficiencia: atributos privados evitan accesos directos, properties aseguran validaciones, y dunders mejoran la depuración. Piensa en esto como un edificio: Tarea es la fundación, herencia agrega pisos especializados, y TaskManager es el techo que lo une todo.
Si expandes esto, podrías agregar persistencia (guardar en archivos), pero por ahora, domina estos bloques. Prueba modificando: crea una subclase nueva, como TareaColaborativa con un atributo para colaboradores.
Resumen del capítulo
- Clase Tarea base: Definimos una clase fundamental con atributos encapsulados, properties para control seguro (getters y setters con validaciones), y
__repr__para representación legible. - Herencia aplicada: Creamos subclases
TareaUrgenteyTareaRecurrenteque heredan deTarea, agregando comportamientos específicos como notificaciones o recurrencia automática, demostrando reutilización de código. - Properties en acción: Usamos decoradores
@propertyy setters para manejar accesos a atributos, previniendo errores como fechas inválidas o títulos vacíos, similar a reglas de negocio en apps reales. - Dunders implementados:
__repr__personalizado en cada clase proporciona salidas claras y útiles, facilitando la depuración y comprensión de objetos. - Clase TaskManager: Integra todo en un gestor que maneja colecciones de tareas, con métodos para agregar, eliminar y listar, culminando en un proyecto OOP completo y funcional.
- Ejecución y pruebas: Instrucciones claras para correr el código con
python gestor_tareas.py, asegurando que puedas experimentar y dominar cada concepto paso a paso.