La lección anterior terminó con dos cabos sueltos. El primero: la agenda de TareaFácil v0.15 es una lista pelada a la que cualquier parte del programa puede añadir lo que le dé la gana —un objeto Tarea, un diccionario suelto o un número—, y las operaciones que la manejan siguen repartidas por el fichero. El segundo: guardar_tareas está roto, porque json.dump no sabe escribir objetos. Esta lección resuelve los dos, y de paso te enseña a trabajar cómodamente con muchos objetos a la vez, que es lo que hace un programa real casi todo el tiempo. Verás cómo recorrer, filtrar, contar, ordenar y buscar objetos aprovechando todo lo aprendido en los módulos 5 y 6, y después darás el mismo salto de la lección anterior pero un nivel más arriba: crear una clase Agenda que contenga las tareas y ofrezca las operaciones que hoy andan sueltas. Al final aparecerán, en su versión más básica, la herencia y el polimorfismo.
Contenido
- Lista de objetos frente a lista de diccionarios
- Recorrer, filtrar y contar objetos
- Ordenar objetos
- Buscar objetos y construir índices
- Composición: la clase
Agenda - Por qué la agenda debe proteger su lista interna
- Serializar y reconstruir:
to_dictyfrom_dict - Herencia y polimorfismo, lo básico
- TareaFácil v0.16
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Lista de objetos frente a lista de diccionarios
La estructura es la misma que en el módulo 5 —una lista— pero lo que hay dentro ha cambiado:
agenda = [
Tarea("Cartel feria del libro", "Luis", "alta", 3),
Tarea("Logotipo Panaderia Sole", "Nuria", "alta", 5),
Tarea("Presupuesto cliente Vidal", "Marta", "media", 2),
]
print(len(agenda), agenda[0].titulo) # 3 Cartel feria del libroTodo lo que sabes de listas sigue valiendo: índices, rebanadas, append, len, in, comprensiones y sorted. Lo único que cambia es cómo se accede a cada campo, y ahí está la ganancia:
| Lista de diccionarios | Lista de objetos | |
|---|---|---|
| Acceso a un campo | t["titulo"] |
t.titulo |
| Nombre mal escrito | t["titulop"] → KeyError al ejecutar |
t.titulop → el editor lo subraya al escribir |
| Autocompletado del editor | Ninguno: las claves son texto | Sí: al pulsar . aparecen los atributos |
| Campos garantizados y comportamiento | Ninguna garantía; funciones sueltas | Los del constructor; métodos del objeto |
La segunda fila es la que más se nota en el día a día: con diccionarios, tarea["responsble"] es un texto perfectamente válido para Python y el fallo no aparece hasta que esa línea se ejecuta; con objetos, tarea.responsble es un nombre que el editor no reconoce y te lo marca en rojo mientras escribes. Es la diferencia entre encontrar un error en dos segundos o en dos semanas.
- Recorrer, filtrar y contar objetos
Los tres patrones del módulo 5 se traducen directamente: recorrer con for, filtrar con comprensión y contar con sum.
for tarea in agenda: # recorrer
print(tarea) # usa __str__
pendientes = [t for t in agenda if not t.completada] # filtrar
cuantas = sum(1 for t in agenda if not t.completada) # contar
dias = sum(t.dias for t in agenda if not t.completada) # sumar
print(f"{cuantas} tareas pendientes, {dias} dias de trabajo.")Tres apuntes sobre estas líneas, porque condensan medio curso:
sum(1 for t in agenda if ...)cuenta sin construir ninguna lista intermedia: el generador va produciendo unos ysumlos acumula. Equivale alen([t for t in agenda if ...])sin gastar memoria, y es la forma idiomática de contar con condición.- El filtro puede usar métodos, no solo atributos:
[t for t in agenda if t.esta_retrasada()]es válido y mucho más expresivo que repetir la comparación de días en cada sitio. Esa es la ventaja de haber metido el comportamiento dentro de la clase. - Los objetos filtrados no se copian.
pendientescontiene referencias a las mismas tareas, así quependientes[0].completar()afecta también a la tarea que está enagenda: es el aliasing de 05-01, ahora con objetos.
- Ordenar objetos
En 06-02 aprendiste a ordenar con sorted(..., key=...). Con objetos, la clave se escribe con la notación del punto:
from operator import attrgetter
por_dias = sorted(agenda, key=lambda t: t.dias) # una lambda
por_dias = sorted(agenda, key=attrgetter("dias")) # equivalente
por_persona = sorted(agenda, key=attrgetter("responsable", "dias")) # tupla-clave
ORDEN_PRIORIDAD = {"alta": 0, "media": 1, "baja": 2}
por_urgencia = sorted(agenda, key=lambda t: (ORDEN_PRIORIDAD[t.prioridad], t.dias))operator.attrgetter es al atributo lo que itemgetter era a la clave del diccionario: attrgetter("dias") devuelve una función que, dado un objeto, extrae su atributo dias. Con attrgetter("responsable", "dias") se obtiene directamente la tupla-clave de dos criterios. Y la última línea recupera la técnica de 06-02: una tupla ordena primero por su primer elemento y, en caso de empate, por el segundo. Existe una alternativa más: enseñar a la propia clase cómo compararse, definiendo el método especial __lt__ (de less than, «menor que»).
def __lt__(self, otra):
"""Orden natural: primero la mas urgente; a igual urgencia, la mas corta."""
return ((ORDEN_PRIORIDAD[self.prioridad], self.dias)
< (ORDEN_PRIORIDAD[otra.prioridad], otra.dias))
print(sorted(agenda)[0].titulo) # sin key: usa __lt__
print(min(agenda).titulo) # min y max tambien lo usan| Forma de ordenar | Cuándo usarla |
|---|---|
key=lambda t: ... o key=attrgetter("campo") |
Ordenaciones puntuales; la segunda, más legible |
__lt__ en la clase |
Cuando el tipo tiene un orden natural evidente |
La regla práctica es esta: define __lt__ solo si existe un orden que cualquiera daría por descontado para ese objeto (una fecha, un número de factura, la urgencia de una tarea); si hay varios órdenes igual de razonables, es mejor key, porque deja explícito en cada llamada qué criterio se usa.
- Buscar objetos y construir índices
La búsqueda lineal de 06-01 tiene en Python una forma abreviada muy cómoda: next() sobre un generador.
def buscar(agenda, titulo):
"""Devuelve la primera tarea con ese titulo, o None si no hay ninguna."""
return next((t for t in agenda if t.titulo.lower() == titulo.lower()), None)
encontrada = buscar(agenda, "logotipo panaderia sole")
print(encontrada.responsable if encontrada else "No encontrada") # Nurianext(generador, valor_por_defecto) pide el primer elemento que produzca el generador y, si no hay ninguno, devuelve el valor por defecto en lugar de fallar. Como el generador es perezoso, deja de recorrer en cuanto encuentra la primera coincidencia: es exactamente la búsqueda lineal con salida temprana, escrita en una línea. El segundo argumento no es opcional en la práctica: sin él, un generador vacío lanza StopIteration. Y cuando una consulta se repite mucho, se construye un índice, igual que en 06-01, pero ahora de responsable a lista de objetos:
indice = {}
for tarea in agenda:
indice.setdefault(tarea.responsable, []).append(tarea) # responsable -> [Tarea, ...]
print([t.titulo for t in indice.get("Nuria", [])]) # get: [] si no tiene ningunaEl compromiso es el mismo de entonces: construir el índice cuesta O(n) una vez y después cada consulta es O(1), pero el índice queda obsoleto en cuanto se añade o se borra una tarea. En la clase Agenda de la sección siguiente eso deja de ser un problema, porque la propia agenda puede reconstruirlo cuando cambia.
- Composición: la clase
Agenda
AgendaTodas las funciones anteriores reciben agenda como primer parámetro, y esa es exactamente la señal que reconocimos en 07-02: un montón de funciones que giran alrededor del mismo dato quieren ser una clase. La Agenda no es una lista: tiene una lista. Esa relación —un objeto que contiene a otros— se llama composición, y es la forma más común y más sana de combinar clases.
classDiagram
class Agenda {
-_tareas: list
+agregar(tarea)
+eliminar(titulo)
+buscar(titulo)
+filtrar_por(criterio)
+ordenadas(clave)
}
class Tarea {
+titulo
+completar()
+to_dict()
}
Agenda "1" o-- "0..*" Tarea : contiene
class Agenda:
"""Coleccion de tareas del estudio, con las operaciones que le son propias."""
def __init__(self, tareas=None):
self._tareas = list(tareas) if tareas else [] # copia lo que recibe
def __len__(self):
return len(self._tareas) # habilita len(agenda)
def __iter__(self):
return iter(self._tareas) # habilita: for t in agenda
def agregar(self, tarea): # rechaza lo que no sea Tarea
if not isinstance(tarea, Tarea) or tarea in self._tareas: # y los duplicados
return False
self._tareas.append(tarea)
return True
def eliminar(self, titulo): # devuelve si borro algo
tarea = self.buscar(titulo)
if tarea is not None:
self._tareas.remove(tarea) # remove usa __eq__ por dentro
return tarea is not None
def buscar(self, titulo): # primera con ese titulo, o None
return next((t for t in self._tareas if t.titulo.lower() == titulo.lower()), None)
def filtrar_por(self, criterio): # criterio: funcion que da bool
return [t for t in self._tareas if criterio(t)]
def ordenadas(self, clave=None):
"""Copia ordenada; por orden natural (__lt__) si no se da clave."""
return sorted(self._tareas) if clave is None else sorted(self._tareas, key=clave)
def resumen_por_responsable(self):
"""Diccionario responsable -> {'tareas': n, 'dias': d} de lo pendiente."""
resumen = {}
for t in self._tareas:
if not t.completada:
datos = resumen.setdefault(t.responsable, {"tareas": 0, "dias": 0})
datos["tareas"], datos["dias"] = datos["tareas"] + 1, datos["dias"] + t.dias
return resumenLo que se gana es enorme, y conviene verlo en uso:
agenda = Agenda()
agenda.agregar(Tarea("Cartel feria del libro", "Luis", "alta", 3))
agenda.agregar(Tarea("Logotipo Panaderia Sole", "Nuria", "alta", 5))
agenda.agregar("una cadena cualquiera") # False: no es una Tarea
print(len(agenda)) # 2, gracias a __len__
for tarea in agenda: print(tarea) # gracias a __iter__
print(agenda.resumen_por_responsable())
# {'Luis': {'tareas': 1, 'dias': 3}, 'Nuria': {'tareas': 1, 'dias': 5}}Los dos métodos especiales son los que hacen que la agenda se comporte como una colección de Python: __len__ habilita len(agenda) y también que if agenda: sea falso cuando está vacía; __iter__ habilita for tarea in agenda, y con él funcionan de propina in, list(agenda), sum(...) y las comprensiones. Fíjate además en que agregar valida, igual que hacía el constructor de Tarea: rechaza lo que no sea una tarea y evita duplicados aprovechando el __eq__ que definimos en 07-02, porque el operador in lo usa por dentro. El agujero por el que se colaba un diccionario suelto está cerrado.
- Por qué la agenda debe proteger su lista interna
El atributo se llama _tareas con un guion bajo delante. Es una convención de Python, no una prohibición: significa «esto es interior de la clase, no lo toques desde fuera». Python no lo impide, pero cualquier programador que lo lea sabe que ahí no debe meter mano. ¿Por qué importa? Porque si la agenda entrega su lista real, cualquiera puede saltarse todas sus reglas:
def tareas(self):
return self._tareas # MAL: entrega la lista de verdad
lista = agenda.tareas()
lista.append("esto no es una tarea") # el control de agregar() se ha esfumado
lista.clear() # y la agenda se ha quedado vaciaEs el aliasing de 05-01 en su forma más peligrosa: lista y agenda._tareas son el mismo objeto, y quien tenga una tiene la otra. La solución es devolver una copia, con return list(self._tareas) en lugar de return self._tareas. Conviene precisar hasta dónde llega esa protección, porque es una copia superficial (05-04): la lista es nueva, pero las tareas de dentro son las mismas. Añadir o quitar elementos de la copia ya no afecta a la agenda; en cambio, copia[0].completar() sí modifica la tarea original. Y está bien que sea así: normalmente quieres proteger la estructura, no impedir que se trabaje con las tareas. Si necesitaras aislamiento total, ahí entraría copy.deepcopy, con su coste. Observa que ordenadas y filtrar_por ya cumplen la regla sin esfuerzo: sorted y las comprensiones siempre construyen listas nuevas.
- Serializar y reconstruir:
to_dict y from_dict
to_dict y from_dictLlegamos al segundo cabo suelto. Al intentar json.dump(agenda, fichero) con objetos dentro, Python responde TypeError: Object of type Tarea is not JSON serializable. El motivo es que json solo sabe escribir tipos básicos —diccionarios, listas, cadenas, números, booleanos y None— y no tiene forma de adivinar qué parte de un objeto hay que guardar. Así que la conversión la hacemos nosotros, con un método a la ida y un constructor alternativo a la vuelta:
def to_dict(self): # en la clase Tarea
"""Devuelve la tarea como diccionario, listo para el JSON."""
return {"titulo": self.titulo, "responsable": self.responsable,
"prioridad": self.prioridad, "dias": self.dias,
"hechos": self.hechos, "completada": self.completada}
@classmethod
def from_dict(cls, datos):
"""Crea una Tarea a partir de un diccionario leido del JSON."""
... # el desde_diccionario de 07-02, con su nombre definitivoto_dict recorre los atributos que hay que persistir y devuelve el diccionario; from_dict hace el camino inverso pasando por el constructor. Con esas dos piezas, la agenda recupera la persistencia:
def guardar(self, ruta=RUTA_JSON): # en la clase Agenda
"""Guarda todas las tareas en el fichero JSON."""
with open(ruta, "w", encoding="utf-8") as f:
json.dump([t.to_dict() for t in self._tareas], f, indent=2, ensure_ascii=False)
@classmethod
def cargar(cls, ruta=RUTA_JSON):
"""Devuelve la Agenda guardada, o una vacia si no hay fichero."""
if not ruta.exists():
return cls() # agenda vacia: primera ejecucion
with open(ruta, "r", encoding="utf-8") as f:
return cls(Tarea.from_dict(d) for d in json.load(f))Tres detalles que merecen atención. La comprensión [t.to_dict() for t in self._tareas] convierte la lista entera de objetos a lista de diccionarios en una línea; el JSON resultante es idéntico al de la v0.14, así que los ficheros antiguos siguen sirviendo. cargar es un @classmethod porque fabrica una agenda, igual que from_dict fabrica una tarea: se llama Agenda.cargar(), sin tener una agenda antes. Y la reconstrucción pasa cada diccionario por Tarea.from_dict, es decir, por el constructor: los datos sucios que hubiera en el fichero —una prioridad "Alta", un campo inventado que se ignora— se normalizan al entrar. El programa deja de heredar la basura de su propio historial.
- Herencia y polimorfismo, lo básico
Marta necesita algo nuevo: tareas que se repiten cada cierto tiempo, como el mantenimiento mensual de la web de la Panadería Solé. Son tareas normales más una periodicidad. La herencia permite decir exactamente eso: crear una clase a partir de otra, quedándose con todo lo que la primera ya sabe hacer.
class TareaRecurrente(Tarea): # entre parentesis, la clase base
"""Una tarea que se repite cada cierto numero de dias."""
def __init__(self, titulo, responsable, prioridad="media", dias=1, cada_dias=30):
super().__init__(titulo, responsable, prioridad, dias) # el constructor de Tarea
self.cada_dias = cada_dias if cada_dias > 0 else 30
def __str__(self):
return f"{super().__str__()} (cada {self.cada_dias}d)" # reutiliza y anademantenimiento = TareaRecurrente("Mantenimiento web Sole", "Luis", "media", 1, 30)
print(mantenimiento.completar()) # True: metodo heredado, no lo hemos escrito
print(mantenimiento.responsable, isinstance(mantenimiento, Tarea)) # Luis TrueLas tres ideas imprescindibles:
class Hija(Madre)hereda todos los atributos y métodos de la clase base:completar,reasignar,to_dicto__eq__funcionan sin escribir una línea.super()da acceso a la clase base.super().__init__(...)ejecuta el constructor deTarea—con toda su validación— y luego la hija añade lo suyo; olvidar esa llamada es el error clásico, y deja la tarea sintitulo, sinresponsabley sin nada. Sobrescribir un método es volver a definirlo en la hija, como hace__str__, que aun así reaprovecha el de la madre consuper().__str__().
Y ahora el polimorfismo, que suena grandilocuente y es de una sencillez desarmante:
mixta = [Tarea("Cartel feria del libro", "Luis", "alta", 3),
TareaRecurrente("Mantenimiento web Sole", "Luis", "media", 1, 30)]
for t in mixta:
print(t) # cada objeto usa SU propia version de __str__El bucle no pregunta de qué clase es cada elemento ni tiene un solo if: cada objeto sabe imprimirse a su manera. Eso es el polimorfismo, funciona igual con to_dict o completar, y es lo que permitirá añadir mañana una TareaUrgente sin tocar el código que recorre la agenda. Dicho esto, la herencia se usa mucho menos de lo que parece:
Herencia (TareaRecurrente(Tarea)) |
Composición (Agenda tiene tareas) |
|
|---|---|---|
| Relación | «es un»: una tarea recurrente es una tarea | «tiene un»: una agenda tiene tareas |
| Acoplamiento | Fuerte: un cambio en la madre afecta a las hijas | Débil: cada clase evoluciona sola |
| Frecuencia real | Poca | Casi siempre |
La regla práctica es preferir composición, y reservar la herencia para cuando la frase «una X es una Y» sea cierta sin forzarla y la hija no necesite deshacer nada de lo que hace la madre. Agenda no hereda de list precisamente por eso: una agenda no es una lista —no tiene sentido agenda.sort(reverse=True) ni agenda + [3, 4]—, sino que tiene una.
- TareaFácil v0.16
Se junta todo. El programa ya no maneja una lista suelta, sino un objeto Agenda, y las funciones que la recorrían se han mudado dentro:
# tareafacil.py - Estudio Alba / Version 0.16: la agenda es un objeto
# --- Tarea (v0.15) con to_dict/from_dict, TareaRecurrente y Agenda: secciones 5 a 8 ---
def mostrar_listado(agenda):
"""Muestra la agenda ordenada por urgencia."""
if not agenda: # __len__ hace esto posible
print("La agenda esta vacia.")
return
for numero, tarea in enumerate(agenda.ordenadas(), start=1):
print(f"{numero:>2}. {tarea}")
hechas = sum(1 for t in agenda if t.completada) # __iter__ hace esto posible
print(f"Total: {len(agenda)} tareas, {hechas} completadas.")
def main():
"""Ejecuta el bucle principal de la aplicacion."""
agenda = Agenda.cargar() # @classmethod: fabrica la agenda
while True:
mostrar_menu()
opcion = pedir_opcion("Elige una opcion (1-10): ", OPCIONES)
if opcion == "10" and confirmar("Seguro que quieres salir?"):
agenda.guardar()
break
# ... las demas opciones llaman a agenda.agregar(...), agenda.buscar(...),
# agenda.filtrar_por(...), agenda.eliminar(...) y a los metodos de TareaCompara el antes y el después de una operación cualquiera:
| En la v0.15 | En la v0.16 |
|---|---|
agenda.append(tarea) sin comprobar nada |
agenda.agregar(tarea), que valida y evita duplicados |
buscar_si(agenda, criterio) |
agenda.buscar(titulo) / agenda.filtrar_por(criterio) |
sorted(agenda, key=clave_orden) en tres sitios |
agenda.ordenadas() |
guardar_tareas(agenda) roto por los objetos |
agenda.guardar() con to_dict |
El programa principal ha adelgazado hasta quedarse en lo que le corresponde: hablar con el usuario. Toda la lógica de la agenda vive en Agenda, y toda la de una tarea, en Tarea.
Errores Comunes y Consejos
- Devolver la lista interna en vez de una copia. Es el error de la sección 6 y anula la validación de la clase:
return list(self._tareas), siempre. - Confundir
_tareascon algo privado de verdad. El guion bajo es una señal para humanos:agenda._tareas.append(3)funciona igual, y la disciplina la pone el equipo, no el lenguaje. - Heredar de
listodictpara «ahorrarse» la composición. Arrastra decenas de métodos que no quieres (sort,pop,extend) saltándose tus reglas. Contén la lista dentro de tu clase. - Olvidar
super().__init__(...)en el constructor de la hija: los atributos de la madre no se crean y todo falla después conAttributeError. - Usar
next(...)sin valor por defecto.next(t for t in agenda if ...)lanzaStopIterationcuando no hay coincidencias: pon siempre el, None. Y define__iter__y__len__en pareja: con los dos, tu clase se comporta como una colección de Python en casi cualquier contexto. - Consejo: mide el índice antes de construirlo. Un
indice_por_responsablesobre una agenda de veinte tareas no ahorra nada y hay que mantenerlo al día; con veinte mil, cambia el programa. Es la regla de 06-04: medir antes de optimizar.
Ejercicios
Ejercicio 1: Consultas sobre una lista de objetos
Con una lista de objetos Tarea, escribe expresiones de una línea que devuelvan: (a) los títulos de las pendientes de Nuria; (b) cuántas de prioridad alta quedan sin completar; (c) la tarea con más días de trabajo; (d) la lista ordenada por responsable y, dentro de cada persona, por días descendente.
Ejercicio 2: Ampliar la clase Agenda
Añade a Agenda tres métodos: pendientes(), con las tareas sin completar; dias_pendientes(), con el total de días de trabajo que quedan; y __contains__(self, titulo), que permita escribir "Cartel feria del libro" in agenda. Justifica por qué pendientes() no debe devolver self._tareas.
Ejercicio 3: Una subclase con super()
Crea TareaConCliente(Tarea), que añada el atributo cliente (texto obligatorio), sobrescriba __str__ para mostrarlo entre corchetes al final y amplíe to_dict() para que el diccionario incluya también el cliente. Comprueba después que una lista mixta de Tarea y TareaConCliente se imprime y se guarda correctamente sin un solo if.
Soluciones
Solución 1.
[t.titulo for t in agenda if t.responsable == "Nuria" and not t.completada] # (a)
sum(1 for t in agenda if t.prioridad == "alta" and not t.completada) # (b)
max(agenda, key=attrgetter("dias")) # (c) max con key, como sorted
sorted(agenda, key=lambda t: (t.responsable, -t.dias)) # (d)El apartado (d) tiene el truco de 06-02: como reverse=True invertiría los dos criterios, se niega solo el numérico con -t.dias. Con textos no se puede, y habría que ordenar dos veces aprovechando que Timsort es estable: primero por el criterio secundario y después por el principal.
Solución 2.
def pendientes(self):
"""Lista nueva con las tareas sin completar."""
return [t for t in self._tareas if not t.completada]
def dias_pendientes(self):
"""Dias de trabajo que quedan por hacer en toda la agenda."""
return sum(t.dias for t in self.pendientes())
def __contains__(self, titulo): # 'Cartel feria del libro' in agenda
return self.buscar(titulo) is not Nonependientes() no puede devolver self._tareas por dos motivos: sería incorrecto —incluiría las completadas— y, más de fondo, entregaría la lista interna, de modo que quien la recibiera podría vaciarla o llenarla de basura saltándose agregar. La comprensión resuelve las dos cosas, porque construye una lista nueva. Y __contains__ es otro método especial de la familia de __len__ e __iter__: sin él, in funcionaría recorriendo el __iter__ y comparando objetos, pero así podemos buscar por título, que es lo natural aquí.
Solución 3.
class TareaConCliente(Tarea):
"""Una tarea asociada a un cliente concreto del estudio."""
def __init__(self, titulo, responsable, cliente, prioridad="media", dias=1):
super().__init__(titulo, responsable, prioridad, dias)
self.cliente = cliente.strip() if cliente.strip() else "Sin cliente"
def __str__(self):
return f"{super().__str__()} [{self.cliente}]"
def to_dict(self):
datos = super().to_dict() # el diccionario de la clase base
datos["cliente"] = self.cliente # y le anadimos lo nuestro
return datos
mixta = [Tarea("Cartel feria del libro", "Luis", "alta", 3),
TareaConCliente("Logotipo", "Nuria", "Panaderia Sole", "alta", 5)]
for t in mixta: print(t) # cada una usa su propio __str__El patrón datos = super().to_dict() y luego añadir es el mismo de __str__: reutilizar lo que la clase base ya hace bien y ampliarlo, en lugar de copiar sus seis campos y arriesgarse a que se desincronicen el día que Tarea gane uno nuevo. Y el bucle final demuestra el polimorfismo: no hay ni un isinstance ni un if, y cada objeto se comporta como le corresponde.
Conclusión
Trabajar con listas de objetos es igual que trabajar con listas de diccionarios, salvo que el acceso es t.titulo en vez de t["titulo"], el editor autocompleta y avisa de los nombres mal escritos, y los campos están garantizados por el constructor. Los patrones de siempre se traducen sin esfuerzo: recorrer con for, filtrar con comprensiones —incluso llamando a métodos como t.esta_retrasada()—, contar con sum(1 for t in ... if ...), ordenar con key=lambda t: t.dias o attrgetter, combinar criterios con tupla-clave y, si el tipo tiene un orden natural evidente, definir __lt__ para que sorted, min y max funcionen sin key. Buscar es next((t for t in agenda if ...), None), y cuando una consulta se repite mucho, un índice de responsable a lista de objetos. Todo eso, agrupado alrededor del mismo dato, pedía a gritos una clase: la Agenda, que por composición contiene la lista y expone agregar —que valida y evita duplicados usando __eq__—, eliminar, buscar, filtrar_por, ordenadas y resumen_por_responsable, más __len__ e __iter__ para que se comporte como una colección de Python. La regla que sostiene toda esa protección es no entregar nunca la lista interna, sino una copia. Y como json.dump no sabe escribir objetos, la ida y la vuelta se hacen a mano con to_dict() y el @classmethod from_dict(), lo que además limpia los datos antiguos al pasarlos por el constructor. Por último has visto la herencia en su forma mínima —TareaRecurrente(Tarea), super().__init__(...), sobrescribir __str__— y el polimorfismo, que permite recorrer una lista mixta sin un solo if; con la advertencia de que en la práctica se prefiere composición, y que la herencia se reserva para cuando «una X es una Y» sea cierto sin forzarlo.
TareaFácil es la v0.16: Agenda.cargar() al arrancar, agenda.guardar() al salir, y un main() que ya solo se ocupa de hablar con el usuario. Pero el proyecto tiene ahora un problema de tamaño: tareafacil.py acumula tres clases, una docena de funciones de entrada y salida, las constantes, la persistencia y el menú, todo en un único fichero que ya cuesta recorrer para encontrar dónde tocar. Los programas de verdad no se escriben así. En Módulos, paquetes e importaciones veremos cómo repartir el proyecto en varios ficheros que se importan entre sí, qué ocurre exactamente cuando Python importa algo —y por fin, la explicación completa del if __name__ == "__main__": que arrastramos desde 04-04—, cómo se organiza un paquete, qué trae la biblioteca estándar y cómo se instalan librerías de terceros. TareaFácil dejará de ser un fichero para convertirse en el paquete tareafacil/.
Fundamentos de la Programación
Módulo 1: Introducción a la Programación
- ¿Qué es la programación?
- Historia de la programación
- Lenguajes de programación
- Entornos de desarrollo
- Del problema al algoritmo
Módulo 2: Conceptos Básicos
- Variables y tipos de datos
- Operadores y expresiones
- Entrada y salida de datos
- Conversión de tipos y validación de datos
Módulo 3: Estructuras de Control
Módulo 4: Funciones y Procedimientos
- Definición y uso de funciones
- Parámetros y retorno de valores
- Ámbito de variables
- Descomponer un programa en funciones
- Funciones como valores: lambda y orden superior
Módulo 5: Estructuras de Datos
- Listas y arreglos
- Cadenas de caracteres
- Diccionarios y conjuntos
- Tuplas y estructuras anidadas
- Guardar datos en archivos: texto, CSV y JSON
Módulo 6: Algoritmos Básicos
Módulo 7: Objetos y Organización del Código
- De los datos a los objetos: clases e instancias
- Atributos, métodos y constructor
- Colecciones de objetos
- Módulos, paquetes e importaciones
Módulo 8: Buenas Prácticas y Herramientas
- Documentación y comentarios
- Depuración y manejo de errores
- Control de versiones
- Pruebas automatizadas
- Estilo, legibilidad y refactorización
