Al cerrar la lección anterior quedaba un ensamblaje pendiente. La v0.8 sabía llevar muchas tareas, pero repartidas en listas paralelas que había que sincronizar a mano; la v0.9 sabe describir una tarea impecablemente, pero solo lleva una. Las dos piezas encajan de forma casi obvia: si una tarea es un diccionario y una lista guarda muchas cosas del mismo tipo, entonces la agenda de Estudio Alba es una lista de diccionarios. Esta lección trata de eso —de combinar estructuras— y de la última que faltaba por conocer: la tupla, una secuencia inmutable que ya te has encontrado sin saberlo cada vez que una función devolvía dos valores en 04-02. Al terminar sabrás leer sin dudar expresiones como agenda[0]["responsable"], elegir con criterio entre lista, diccionario y tupla, y TareaFácil habrá alcanzado la v0.10 con todo su menú funcionando sobre una colección real.
Contenido
- Qué es una tupla
- Inmutabilidad: cuándo tupla y cuándo lista
- Desempaquetado
- Devolver varios valores es devolver una tupla
- Tuplas como claves y
namedtuple - Estructuras anidadas: los tres patrones
- Leer una expresión encadenada
- Recorrer estructuras anidadas
- Copia superficial y copia profunda
- Elegir la estructura adecuada
- TareaFácil v0.10: la agenda del estudio
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Qué es una tupla
Una tupla es una secuencia ordenada de valores, como una lista, pero inmutable: una vez creada no se puede añadir, quitar ni cambiar nada. Se escribe con paréntesis, aunque lo que de verdad crea la tupla son las comas.
tarea = ("Cartel feria", "Marta", "alta", 3)
print(tarea[0], tarea[-1], len(tarea)) # Cartel feria 3 4
print(tarea[1:3], "Marta" in tarea) # ('Marta', 'alta') True
tarea[3] = 5 # TypeError: 'tuple' object does not support item assignment
sin_parentesis = "Cartel feria", "Marta", 3 # tambien es una tupla
uno_solo = ("Cartel feria",) # tupla de UN elemento: la coma es obligatoria
no_es_tupla = ("Cartel feria") # solo una cadena entre parentesis
print(type(uno_solo), type(no_es_tupla)) # <class 'tuple'> <class 'str'>Índices, rebanadas, len(), in y el recorrido con for funcionan exactamente igual que en una lista; lo único que no funciona es modificar. Las tres últimas asignaciones son las que despistan a todo el mundo: los paréntesis son opcionales, la tupla vacía se escribe () y la tupla de un elemento exige la coma final, porque sin ella los paréntesis son de agrupación y el resultado es el valor de dentro. Las constantes que llevas usando desde 04-04 —PRIORIDADES = ("alta", "media", "baja")— son precisamente tuplas, y ahora sabes por qué: son datos fijos que el programa no debe cambiar nunca.
- Inmutabilidad: cuándo tupla y cuándo lista
| Criterio | Tupla | Lista |
|---|---|---|
| Sintaxis | (1, 2, 3) |
[1, 2, 3] |
| ¿Se puede modificar? | No | Sí |
| Métodos disponibles | Solo count e index |
append, pop, sort… |
| ¿Clave de diccionario o elemento de un conjunto? | Sí | No |
| Uso típico | Registro de campos fijos, constantes, valores devueltos | Colección que crece y cambia |
La pregunta que decide es: ¿los elementos son cosas distintas con un papel fijo, o muchas cosas iguales? Una coordenada (x, y), una fecha (2026, 8, 4) o una fila de datos son tuplas: número fijo de posiciones, cada una con su significado. Los responsables del estudio son una lista: puede crecer, menguar y reordenarse. La inmutabilidad tiene tres ventajas prácticas: protege los datos que no deben cambiar, permite usar la tupla como clave de diccionario o elemento de un conjunto, y documenta la intención a quien lee el código, porque una tupla dice «esto es un registro cerrado». La contrapartida es que para cambiar algo hay que construir una tupla nueva.
- Desempaquetado
Desempaquetar es repartir los elementos de una secuencia en varias variables de una sola vez. Es una de las cosas que hacen a Python cómodo, y ya lo has usado sin nombrarlo en for clave, valor in tarea.items().
tarea = ("Cartel feria", "Marta", "alta", 3)
titulo, responsable, prioridad, dias = tarea
print(responsable, dias) # Marta 3
a, b = 3, 5
a, b = b, a # intercambio sin variable auxiliar: 5 3
primero, *resto = ["Marta", "Luis", "Nuria", "Alba"]
print(primero, resto) # Marta ['Luis', 'Nuria', 'Alba']
primero, *medio, ultimo = ["Marta", "Luis", "Nuria", "Alba"] # medio: ['Luis','Nuria']
titulo, _, prioridad, _ = tarea # el guion bajo: 'este valor no me interesa'El número de nombres a la izquierda debe coincidir exactamente con el de elementos; si no, ValueError: too many values to unpack. El intercambio en una línea funciona porque el lado derecho se evalúa entero antes de asignar: primero se construye la tupla (5, 3) y luego se reparte. El asterisco recoge «el resto» en una lista, y solo puede haber uno por desempaquetado. El guion bajo _ es una convención, no una regla del lenguaje: una variable normal cuyo nombre anuncia que su valor se va a ignorar.
- Devolver varios valores es devolver una tupla
En 04-02 aprendiste que una función puede devolver varios valores y quedó dicho que se explicaría aquí. Ya tienes todas las piezas: return a, b construye una tupla y la devuelve; la asignación múltiple en la llamada es un desempaquetado.
def analizar(dias):
"""Devuelve el total, la media y el maximo de una lista de dias."""
return sum(dias), sum(dias) / len(dias), max(dias)
total, media, maximo = analizar([3, 5, 2, 8]) # desempaquetado
resultado = analizar([3, 5, 2, 8]) # sin desempaquetar
print(resultado, type(resultado)) # (18, 4.5, 8) <class 'tuple'>
print(resultado[1]) # 4.5Es decir: una función siempre devuelve un único valor; lo que pasa es que ese valor puede ser una tupla con varias cosas dentro. Aquella registrar_tarea() de la v0.7, que «devolvía cuatro valores», devolvía en realidad una tupla de cuatro elementos que se desempaquetaba en la llamada. Consejo de diseño: si vuelves de una función con más de tres o cuatro valores, o si quien llama tiene que recordar el orden exacto, probablemente lo que quieres devolver es un diccionario, donde cada dato lleva su nombre. Es justo el cambio que hizo la v0.9.
- Tuplas como claves y
namedtuple
namedtupleAl ser inmutable, una tupla puede ser clave de un diccionario. Eso permite indexar por una combinación de datos:
horas = {("Marta", "lunes"): 6, ("Marta", "martes"): 4, ("Luis", "lunes"): 8}
print(horas[("Marta", "martes")]) # 4
print(horas.get(("Nuria", "lunes"), 0)) # 0 -> no trabajo ese diaCon una lista como clave sería imposible (TypeError: unhashable type). Este patrón es habitual para tablas de doble entrada: persona y día, producto y mes, fila y columna. Existe también namedtuple, del módulo collections, que es una tupla cuyos campos tienen nombre: Tarea = namedtuple("Tarea", "titulo responsable dias") permite escribir t.responsable en lugar de t[1]. La menciono para que la reconozcas si la encuentras; en este curso usaremos diccionarios para lo mismo, y en el módulo 7 verás la herramienta definitiva, las clases.
- Estructuras anidadas: los tres patrones
Listas, diccionarios y tuplas se pueden meter unos dentro de otros sin límite, pero en la práctica casi todo lo que necesitarás son tres combinaciones. La primera es la lista de diccionarios — «muchos registros»—, la forma natural de una agenda, un catálogo o una tabla:
agenda = [
{"titulo": "Cartel feria", "responsable": "Marta", "prioridad": "alta", "dias": 3},
{"titulo": "Menu Sole", "responsable": "Luis", "prioridad": "media", "dias": 5},
{"titulo": "Logotipo Vidal", "responsable": "Nuria", "prioridad": "baja", "dias": 2},
]Diccionario de listas — «grupos de cosas», cuando quieres ir directo al nombre del grupo. Y diccionario de diccionarios — «fichas indexadas por un identificador único»:
por_responsable = {"Marta": ["Cartel feria", "Flyer verano"],
"Luis": ["Menu Sole"], "Nuria": []}
equipo = {"Marta": {"rol": "coordinadora", "horas": 38, "etiquetas": {"diseno"}},
"Luis": {"rol": "disenador", "horas": 35, "etiquetas": {"imprenta"}}}Se accede a ellos con agenda[0]["titulo"], por_responsable["Marta"][0] y equipo["Marta"]["rol"] respectivamente. La tabla de decisión completa está en la sección 10.
- Leer una expresión encadenada
Una expresión como agenda[0]["responsable"] asusta la primera vez y deja de hacerlo en cuanto sabes que se lee de izquierda a derecha, un paso cada vez:
agenda→ la lista completa de tareas.agenda[0]→ el primer elemento de esa lista, que resulta ser un diccionario.agenda[0]["responsable"]→ el valor de la clave"responsable"de ese diccionario,"Marta".
graph TD
A["agenda (lista)"] --> B["[0] diccionario"]
A --> C["[1] diccionario"]
B --> D["titulo: Cartel feria"]
B --> E["responsable: Marta"]
C --> G["titulo: Menu Sole"]
C --> H["responsable: Luis"]
El truco para no perderte: cada corchete baja un nivel del árbol. Si dudas, imprime los pasos intermedios en el REPL: print(agenda[0]) enseña el diccionario entero y print(type(agenda[0])) dice en qué tipo estás parado. Los errores son informativos: TypeError: list indices must be integers significa que has usado una clave de texto donde había una lista, y KeyError, que la clave no existe en ese diccionario. Con la práctica se leen de un vistazo: agenda[1]["titulo"] es "Menu Sole", equipo["Marta"]["rol"] es "coordinadora", por_responsable["Marta"][1] es "Flyer verano", y agenda[0]["dias"] = 4 modifica la tarea en el sitio sin ningún problema.
- Recorrer estructuras anidadas
Recorrer una lista de diccionarios es un for normal en el que cada elemento resulta ser un diccionario:
for tarea in agenda:
print(f"{tarea['titulo']:<20}{tarea['responsable']:>8}{tarea['dias']:>4}d")
pendientes = [t for t in agenda if t["dias"] > 2] # filtrar
titulos = [t["titulo"] for t in agenda] # extraer una columna
por_dias = sorted(agenda, key=lambda t: t["dias"], reverse=True) # ordenarAquí se ve por qué merecía la pena el cambio: ordenar por días, que en 05-01 era una expresión retorcida con titulos.index(t), ahora es simplemente key=lambda t: t["dias"]. Recorrer un diccionario de listas exige dos niveles de bucle, el segundo dentro del primero; con un diccionario de diccionarios el patrón es el mismo:
for responsable, titulos in por_responsable.items():
print(f"{responsable} ({len(titulos)} tareas):")
for titulo in titulos:
print(f" - {titulo}")
for nombre, ficha in equipo.items():
print(f"{nombre:<8}{ficha['rol']:<15}{ficha['horas']:>3}h")Fíjate en el desempaquetado directo en el encabezado: for responsable, titulos in ...items() separa clave y valor sin líneas extra, y funciona siempre que la estructura interna sea conocida.
- Copia superficial y copia profunda
Aquí aparece un error clásico que solo se manifiesta con estructuras anidadas: .copy() copia la lista, pero no copia lo que hay dentro.
import copy
copia = agenda.copy() # copia superficial
copia[0]["dias"] = 99 # modificamos la tarea a traves de la copia
print(agenda[0]["dias"]) # 99 -> la original tambien ha cambiado
de_verdad = copy.deepcopy(agenda) # copia profunda
de_verdad[0]["dias"] = 1
print(agenda[0]["dias"]) # 99 -> ahora si, la original no se ha tocadoLa lista copia es nueva, pero sus elementos son los mismos diccionarios que los de agenda: se ha duplicado la caja, no el contenido.
| Tipo de copia | Cómo | Qué duplica |
|---|---|---|
| Referencia (aliasing) | b = a |
Nada: dos nombres, un objeto |
| Superficial | a.copy(), list(a), a[:] |
El contenedor exterior |
| Profunda | copy.deepcopy(a) |
Todo el árbol, a cualquier profundidad |
La regla práctica: si la estructura tiene un solo nivel, la copia superficial basta; si hay listas o diccionarios dentro y vas a modificarlos, necesitas deepcopy. Y no uses deepcopy por defecto: es más lenta, y la mayoría de las veces lo que quieres precisamente es que la función modifique la agenda de verdad.
- Elegir la estructura adecuada
| Necesito… | Estructura | Ejemplo del estudio |
|---|---|---|
| Muchas cosas del mismo tipo, en orden | Lista | Títulos de tareas |
| Los campos de una cosa, cada uno con su nombre | Diccionario | Una tarea |
| Un registro fijo que no debe cambiar | Tupla | PRIORIDADES, una coordenada |
| Valores únicos, solo comprobar pertenencia | Conjunto | Etiquetas del estudio |
| Muchos registros con los mismos campos | Lista de diccionarios | La agenda completa |
| Agrupar registros por una categoría | Diccionario de listas | Tareas por responsable |
| Buscar una ficha por identificador único | Diccionario de diccionarios | Ficha del equipo |
| Indexar por una combinación de datos | Diccionario con claves tupla | Horas por persona y día |
Un consejo que ahorra mucho trabajo: elige la estructura pensando en la operación más frecuente. Si lo que más haces es recorrer todo y ordenar, lista de diccionarios; si lo que más haces es «dame la ficha de X», diccionario de diccionarios. Y si necesitas las dos cosas, mantener una lista y construir el agrupamiento cuando haga falta suele ser más simple que sostener dos estructuras sincronizadas —el error que ya cometimos con las listas paralelas.
- TareaFácil v0.10: la agenda del estudio
Llegamos al ensamblaje: la agenda es una lista de diccionarios y todas las opciones del menú se rehacen sobre ella, que crece a seis para incluir el resumen por responsable. Las listas paralelas de la v0.8 desaparecen para siempre.
# tareafacil.py - Estudio Alba / Version 0.10: la agenda es una lista de diccionarios
ANCHO = 52
PRIORIDADES = ("alta", "media", "baja")
EQUIPO = ("marta", "luis", "nuria")
OPCIONES = ("1", "2", "3", "4", "5", "6")
ORDEN_PRIORIDAD = {"alta": 0, "media": 1, "baja": 2}
# --- Sin cambios: pedir_texto, pedir_opcion, pedir_entero, confirmar,
# clasificar_urgencia, mostrar_menu (ahora con 6 opciones) y pausar ---
def registrar_tarea(agenda):
"""Pide una tarea nueva y la anade al final de la agenda."""
agenda.append({
"titulo": pedir_texto("Titulo : "),
"responsable": pedir_opcion("Responsable : ", EQUIPO).capitalize(),
"prioridad": pedir_opcion("Prioridad : ", PRIORIDADES),
"dias": pedir_entero("Dias (1-365) : ", 1, 365),
"completada": False})
print(f"Tarea {len(agenda)} registrada correctamente.")
def mostrar_listado(agenda):
"""Pinta la agenda ordenada por prioridad, con el numero real de cada tarea."""
if not agenda:
print("No hay ninguna tarea registrada.")
return False
pares = sorted(enumerate(agenda, start=1),
key=lambda p: ORDEN_PRIORIDAD[p[1]["prioridad"]])
print("-" * ANCHO)
for numero, tarea in pares:
marca = "[X]" if tarea["completada"] else "[ ]"
print(f"{numero:>2}. {marca} {tarea['titulo']:<24}"
f"{tarea['responsable']:>8}{tarea['prioridad']:>7}{tarea['dias']:>4}d")
hechas = sum(1 for t in agenda if t["completada"])
print(f"Total: {len(agenda)} tareas, {hechas} completadas, "
f"{sum(t['dias'] for t in agenda)} dias de trabajo.")
return True
def elegir_tarea(agenda):
"""Devuelve la tarea que elija el usuario, o None si la agenda esta vacia."""
if not mostrar_listado(agenda):
return None
numero = pedir_entero(f"Numero de tarea (1-{len(agenda)}): ", 1, len(agenda))
return agenda[numero - 1]
# cambiar_prioridad(tarea) y marcar_completada(tarea) se quedan tal cual estaban
# en la v0.9: ya recibian un diccionario y lo modificaban en el sitio.
def resumen_por_responsable(agenda):
"""Pinta cuantas tareas y cuantos dias acumula cada persona del equipo."""
resumen = {}
for tarea in agenda:
datos = resumen.setdefault(tarea["responsable"], {"tareas": 0, "dias": 0})
datos["tareas"] += 1
datos["dias"] += tarea["dias"]
for responsable, datos in sorted(resumen.items(), key=lambda p: -p[1]["dias"]):
print(f"{responsable:<12}{datos['tareas']:>3} tareas{datos['dias']:>5} dias")
sin_tareas = {n.capitalize() for n in EQUIPO} - set(resumen)
if sin_tareas:
print("Sin tareas asignadas:", ", ".join(sorted(sin_tareas)))
def main():
"""Ejecuta el bucle principal de la aplicacion."""
agenda = []
while True:
mostrar_menu()
opcion = pedir_opcion("Elige una opcion (1-6): ", OPCIONES)
if opcion == "6":
if confirmar("Seguro que quieres salir?"):
break
elif opcion == "1":
registrar_tarea(agenda)
elif opcion == "2":
mostrar_listado(agenda)
elif opcion == "3":
tarea = elegir_tarea(agenda)
if tarea is not None:
cambiar_prioridad(tarea)
elif opcion == "4":
tarea = elegir_tarea(agenda)
if tarea is not None:
marcar_completada(tarea)
elif opcion == "5":
resumen_por_responsable(agenda)
pausar()
if __name__ == "__main__":
main()Los puntos que conviene entender bien:
elegir_tareadevuelve el diccionario, no un índice. Y como el diccionario es mutable y se pasa por referencia,cambiar_prioridad(tarea)modifica la tarea dentro de la agenda sin necesidad de saber en qué posición estaba. Compara esto con los cincopopcoordinados de la v0.8.sorted(enumerate(agenda, start=1), key=lambda p: ORDEN_PRIORIDAD[p[1]["prioridad"]])ordena por el orden lógico de las prioridades —usando un diccionario como tabla de traducción— sin perder el número real de cada tarea:enumerateproduce pares(numero, tarea)y elkeymira dentro del segundo elemento del par. Así el listado sale ordenado pero los números siguen sirviendo para elegir, yelegir_tareapuede reutilizarmostrar_listadoen lugar de repetir el bucle (DRY, 04-04).sum(1 for t in agenda if t["completada"])cuenta las completadas sin bucle explícito, ysum(t['dias'] for t in agenda)acumula los días: comprensiones de 05-01 usadas dentro desum.resumen.setdefault(responsable, {...})crea la ficha del responsable la primera vez que aparece y la devuelve las siguientes: el patrón de conteo de 05-03, aquí construyendo un diccionario de diccionarios sobre la marcha. Y{n.capitalize() for n in EQUIPO} - set(resumen)es un conjunto por comprensión menos otro conjunto: quién del equipo no tiene nada asignado, en una línea. Por último,if not agenda:aprovecha que una lista vacía es falsa (02-04) y se lee mejor quelen(agenda) == 0.
Con esto TareaFácil hace por fin lo que Marta necesita, y queda un solo cabo suelto, el que da título a la próxima lección: al cerrar el programa, todo desaparece.
Errores Comunes y Consejos
Olvidar la coma en la tupla de un elemento. ("Marta") es una cadena; ("Marta",) es una tupla. Se nota cuando len() devuelve 5 en vez de 1. Y modificar una tupla da TypeError: si necesitas cambiar valores, usa una lista.
Desempaquetar con un número de nombres distinto. a, b = (1, 2, 3) lanza ValueError; usa a, *resto = ... si no sabes cuántos elementos vendrán. Confundir los niveles en una expresión encadenada. agenda["titulo"] falla porque agenda es una lista: primero hay que elegir un elemento (agenda[0]) y después la clave. Si te pierdes, imprime los pasos intermedios. Y copiar en superficie una estructura anidada no protege los diccionarios de dentro: usa copy.deepcopy cuando quieras un árbol independiente.
Consejo: mantén homogénea la lista de diccionarios. Que todos tengan las mismas claves es lo que permite recorrerla sin comprobaciones; si un registro puede no tener un campo, dale un valor por defecto al crearlo en lugar de omitir la clave. Y no anides más de tres niveles: datos["a"][0]["b"][2]["c"] es indescifrable y frágil. Extrae subestructuras a variables con nombre (tarea = agenda[i]) o replantea el diseño; a partir de cierta complejidad, lo que pide el problema son clases.
Ejercicios
Ejercicio 1: Predecir la salida
Di qué imprime cada línea y qué errores se producen.
t = ("Cartel feria", "Marta", 3)
titulo, responsable, dias = t
print(dias, t[-1], len(("Marta",)), len(("Marta")))
a, b = 1, 2
a, b = b, a
print(a, b)
primero, *resto = ["Marta", "Luis", "Nuria"]
print(primero, resto)
agenda = [{"titulo": "Cartel", "dias": 3}, {"titulo": "Menu", "dias": 5}]
copia = agenda.copy()
copia[0]["dias"] = 99
print(agenda[0]["dias"])
print(agenda["titulo"])Ejercicio 2: Consultas sobre la agenda
Con la agenda de abajo, escribe las expresiones o el código necesarios para obtener: los títulos de las tareas de prioridad alta; el total de días pendientes (los de las tareas no completadas); la tarea con más días; la lista de títulos ordenada alfabéticamente ignorando mayúsculas; y un diccionario {responsable: numero_de_tareas}.
agenda = [
{"titulo": "Cartel feria", "responsable": "Marta", "prioridad": "alta",
"dias": 3, "completada": False},
{"titulo": "menu Sole", "responsable": "Luis", "prioridad": "media",
"dias": 5, "completada": True},
{"titulo": "Logotipo Vidal", "responsable": "Marta", "prioridad": "alta",
"dias": 2, "completada": False}]Ejercicio 3: Agrupar por responsable
Escribe agrupar_por_responsable(agenda) que devuelva un diccionario de listas con los títulos de cada persona, incluyendo a los miembros de EQUIPO que no tengan ninguna tarea (con lista vacía). Después escribe pintar_grupos(grupos) que lo imprima con los responsables ordenados por número de tareas descendente y los títulos alfabéticamente.
Soluciones
Solución 1.
("Marta",) tiene un elemento; ("Marta") es la cadena "Marta", cuya longitud es 5. El intercambio deja a = 2 y b = 1 porque el lado derecho se evalúa entero antes de asignar. *resto recoge lo que sobra en una lista. copia es una copia superficial: sus elementos son los mismos diccionarios, así que el 99 se ve en la original. Y la última línea falla porque agenda es una lista y "titulo" no es un índice: faltaba bajar un nivel con agenda[0]["titulo"].
Solución 2.
altas = [t["titulo"] for t in agenda if t["prioridad"] == "alta"]
pendientes = sum(t["dias"] for t in agenda if not t["completada"])
mas_larga = max(agenda, key=lambda t: t["dias"])
alfabetico = sorted([t["titulo"] for t in agenda], key=str.lower)
conteo = {}
for t in agenda:
conteo[t["responsable"]] = conteo.get(t["responsable"], 0) + 1
print(altas, pendientes) # ['Cartel feria', 'Logotipo Vidal'] 5
print(mas_larga["titulo"]) # menu Sole
print(alfabetico) # ['Cartel feria', 'Logotipo Vidal', 'menu Sole']
print(conteo) # {'Marta': 2, 'Luis': 1}Dos detalles. max(agenda, key=...) devuelve el diccionario entero, no los días, por eso hace falta ["titulo"] para ver el nombre. Y el key=str.lower del orden alfabético es el de 05-02: sin él, «menu Sole» iría primero por empezar en minúscula.
Solución 3.
EQUIPO = ("Marta", "Luis", "Nuria")
def agrupar_por_responsable(agenda): # EQUIPO capitalizado para este ejercicio
"""Devuelve {responsable: [titulos]} incluyendo a quien no tiene tareas."""
grupos = {nombre: [] for nombre in EQUIPO} # diccionario por comprension
for tarea in agenda:
grupos.setdefault(tarea["responsable"], []).append(tarea["titulo"])
return grupos
def pintar_grupos(grupos):
"""Pinta cada responsable con sus tareas ordenadas."""
for responsable, titulos in sorted(grupos.items(), key=lambda p: -len(p[1])):
print(f"{responsable} ({len(titulos)}):")
for titulo in sorted(titulos, key=str.lower):
print(f" - {titulo}")
pintar_grupos(agrupar_por_responsable(agenda))El diccionario por comprensión inicial garantiza que todos los miembros del equipo aparezcan, aunque no tengan tareas: sin él, Nuria se quedaría fuera del informe. El setdefault que viene después cubre el caso contrario —un responsable que no esté en EQUIPO—, así que la función no se rompe con datos inesperados. Y key=lambda p: -len(p[1]) ordena de más a menos usando el truco del signo negativo, alternativa a reverse=True cuando el criterio es numérico.
Conclusión
Una tupla es una secuencia inmutable: lo que la crea son las comas, la de un elemento necesita la coma final (x,), y su papel natural es el registro de campos fijos, la constante y el valor devuelto. El desempaquetado reparte sus elementos en variables (titulo, responsable, dias = tarea), permite el intercambio en una línea y admite *resto para recoger lo que sobra. Con eso queda cerrada la promesa de 04-02: devolver varios valores es devolver una tupla, y una función siempre devuelve un único objeto. Al ser inmutables, las tuplas sirven como claves de diccionario para indexar por combinaciones, y namedtuple existe por si te la encuentras.
Las estructuras anidadas se reducen en la práctica a tres patrones: lista de diccionarios para muchos registros, diccionario de listas para agrupar y diccionario de diccionarios para fichas indexadas. Se leen de izquierda a derecha bajando un nivel por cada corchete, se recorren con bucles anidados y desempaquetado en el for, y esconden una trampa: la copia superficial no duplica el interior, para eso está copy.deepcopy. TareaFácil ha alcanzado la v0.10, y es un programa de verdad: la agenda es una lista de diccionarios, el listado sale numerado y ordenado por prioridad, elegir_tarea devuelve la tarea que después se modifica en el sitio, y un resumen agrupa el trabajo por responsable señalando a quien está libre. Las listas paralelas de la v0.8 y sus riesgos de desincronización han desaparecido por completo. Pero sigue habiendo un final infeliz: cuando Marta cierra el programa, la agenda entera se evapora. Toda esa estructura vive solo en la memoria. En Guardar datos en archivos: texto, CSV y JSON aprenderás a escribirla en disco y a recuperarla al arrancar, de modo que el trabajo del estudio siga ahí mañana por la mañana. Es el último cabo suelto del módulo.
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
