Listas y tuplas en Python — práctica con estadísticas de clase, un buscador de alumnos y medias móviles (y la nota mínima que era un cero que nadie había sacado)
Estaba calculando las estadísticas de mi clase: la media, la nota más alta, la más baja. Todo salía bien… menos la mínima. El programa me decía que la peor nota era un 0. Miré la lista de arriba abajo: nadie había sacado un 0, la peor era un 3.5. No había ningún error en la pantalla, solo un número que no existía en mis datos.
Lo que había hecho era empezar la búsqueda del mínimo con minima = 0, «por poner algo». El programa nunca encontró una nota menor que 0 (claro), así que se quedó con mi valor inventado. Esa es la clase de fallo que se corrige con una línea y que cuesta una tarde encontrar si no sabes dónde mirar.
Hoy practicamos los esquemas de las secuencias con tres programas completos: estadísticas de una clase, un buscador de alumnos con slices y un análisis de las notas de un trimestre. Esta práctica se apoya en la teoría de listas y tuplas: índices, slices, desempaquetado y los esquemas de recorrido. Si te sientes inseguro con las funciones, repasa antes funciones en Python.
Tabla de Contenidos
Cómo usar este artículo
Cada programa va completo en un solo archivo. Cópialo, guárdalo con el nombre que se indica y lánzalo desde la terminal con python3 nombre.py. Antes de mirar la salida, intenta predecirla: es la mejor forma de saber si has entendido el código.
Ningún programa usa input(). Los datos van fijos en el código para que tu salida sea idéntica a la que ves aquí.
Programa 1 · Estadísticas de una clase
Tenemos los alumnos como una lista de tuplas (nombre, nota), y queremos la media, la mejor y la peor nota, y cuántos han aprobado. Cada resultado es un esquema distinto: acumulador, dos búsquedas de extremo y un contador.
clase.py
ALUMNOS = [
("Lucía", 7.5),
("Marcos", 4.0),
("Aitana", 9.0),
("Iván", 3.5),
("Carla", 6.0),
("Dani", 8.5),
]
APROBADO = 5.0
def media(alumnos):
"""Media de las notas de la clase."""
acumulador = 0
for nombre, nota in alumnos:
acumulador += nota
return acumulador / len(alumnos)
def mejor(alumnos):
"""Alumno con la nota más alta (el primero si hay empate)."""
candidato = alumnos[0]
for alumno in alumnos:
if alumno[1] > candidato[1]:
candidato = alumno
return candidato
def peor(alumnos):
"""Alumno con la nota más baja (el primero si hay empate)."""
candidato = alumnos[0]
for alumno in alumnos:
if alumno[1] < candidato[1]:
candidato = alumno
return candidato
def contar_aprobados(alumnos):
"""Cuántos alumnos llegan al aprobado."""
contador = 0
for nombre, nota in alumnos:
if nota >= APROBADO:
contador += 1
return contador
nombre_mejor, nota_mejor = mejor(ALUMNOS)
nombre_peor, nota_peor = peor(ALUMNOS)
print(f"Alumnos: {len(ALUMNOS)}")
print(f"Media de la clase: {media(ALUMNOS):.2f}")
print(f"Mejor nota: {nombre_mejor} ({nota_mejor})")
print(f"Peor nota: {nombre_peor} ({nota_peor})")
print(f"Aprobados: {contar_aprobados(ALUMNOS)} de {len(ALUMNOS)}")
Lánzalo con python3 clase.py:
Alumnos: 6 Media de la clase: 6.42 Mejor nota: Aitana (9.0) Peor nota: Iván (3.5) Aprobados: 4 de 6
Entre líneas:
- Cada alumno es una tupla
(nombre, nota), y la clase una lista de tuplas. Enmediay encontar_aprobadosdesempaquetamos confor nombre, nota in alumnos, que se lee mucho mejor quealumno[0]yalumno[1]. mediaes un acumulador: parte de 0 (el neutro de la suma), suma todas las notas y divide entrelen(alumnos). Encontar_aprobadosel esquema es casi el mismo, pero suma siempre 1 y solo cuando la nota llega al aprobado: un contador selectivo.mejorypeorson el mismo código con un solo símbolo cambiado (>por<). Fíjate en cómo empiezan:candidato = alumnos[0], el primer alumno, no un valor inventado. Es justo el fallo de mi historia: si hubiera escritocandidato = ("", 0), la peor nota habría salido siempre 0.- Los empates los gana el primero que aparece, porque la comparación es estricta (
>y no>=). Para nuestros datos no hay empates, pero conviene saber qué decide el código en ese caso. mejorypeordevuelven la tupla entera, y el programa la desempaqueta ennombre_mejor, nota_mejor = mejor(ALUMNOS).- Las cuentas cuadran: 7.5 + 4.0 + 9.0 + 3.5 + 6.0 + 8.5 = 38.5, y 38.5 / 6 = 6.4166… que
:.2fmuestra como 6.42. Aprueban Lucía, Aitana, Carla y Dani.
Pruébalo tú
- Cambia
candidato = alumnos[0]enpeorporcandidato = (None, 0)y mira qué sale. ¿Por qué ya no es el peor alumno de la clase? - Añade una función
cuantos_notables(alumnos)que cuente los alumnos con 7 o más.
Programa 2 · Un buscador de alumnos
Ahora los datos están en dos listas paralelas: los nombres y las notas, en el mismo orden. El nombre del alumno i está en NOMBRES[i] y su nota en NOTAS[i]. Para buscar a alguien necesitamos su posición, y con ella sacamos su nota. Además, usamos slices para mostrar partes de la lista.
buscador.py
NOMBRES = ["Lucía", "Marcos", "Aitana", "Iván", "Carla", "Dani"]
NOTAS = [7.5, 4.0, 9.0, 3.5, 6.0, 8.5]
def posicion(secuencia, buscado):
"""Posición de la primera aparición de buscado, o -1 si no está."""
for pos, item in enumerate(secuencia):
if item == buscado:
return pos
return -1
def nota_de(nombre):
"""Nota del alumno, o None si no está en la lista."""
pos = posicion(NOMBRES, nombre)
if pos == -1:
return None
return NOTAS[pos]
for nombre in ("Carla", "Pablo", "Lucía"):
nota = nota_de(nombre)
if nota is None:
print(f"{nombre}: no está en la lista")
else:
print(f"{nombre}: posición {posicion(NOMBRES, nombre)}, nota {nota}")
print("Primeros tres:", NOMBRES[:3])
print("Últimos dos:", NOMBRES[-2:])
print("Orden inverso:", NOMBRES[::-1])
print("¿Está Dani?", "Dani" in NOMBRES)
Lánzalo con python3 buscador.py:
Carla: posición 4, nota 6.0 Pablo: no está en la lista Lucía: posición 0, nota 7.5 Primeros tres: ['Lucía', 'Marcos', 'Aitana'] Últimos dos: ['Carla', 'Dani'] Orden inverso: ['Dani', 'Carla', 'Iván', 'Aitana', 'Marcos', 'Lucía'] ¿Está Dani? True
Entre líneas:
posicionrecorre la lista conenumerate, que da parejas(pos, item). Cuando encuentra el valor, devuelve ya la posición; solo si termina el bucle sin encontrarlo devuelve-1. Esereturn -1está fuera del bucle: es el error clásico delelse.-1es una buena marca de «no encontrado» porque nunca puede ser una posición. Si hubiéramos devuelto0oFalse, no podríamos distinguir «no está» de «está el primero».nota_deaprovecha la posición para ir a la otra lista:NOTAS[pos]. Si no está, devuelveNone, y el programa lo comprueba conis Noneantes de mostrar nada.- Fíjate en el orden de las comprobaciones: primero se pregunta por
Noney después se usa el resultado. Si hubieras mostradonotasin comprobar, habrías impresoNoneen lugar de un mensaje útil. - Los
printde después del bucle son tres slices y una búsqueda:[:3]son los tres primeros,[-2:]los dos últimos (el fin vacío llega hasta el final) y[::-1]la lista al revés. "Dani" in NOMBREShace una búsqueda y devuelveTrueoFalse; por dentro es el mismo bucle deposicion, sin que tengas que escribirlo.
Pruébalo tú
- ¿Qué pasa si buscas a «Lucía» y a «lucía» (con minúscula)? ¿Por qué?
- Añade una función
ultimo_suspenso()que devuelva la posición del último alumno con nota menor que 5. Pista: recorre al revés conrange(len(NOTAS) - 1, -1, -1).
Programa 3 · Las notas de un trimestre
Las notas de un alumno a lo largo del trimestre van en una lista. Queremos suavizar la curva con medias móviles (la media de cada grupo de tres notas seguidas), saber cuántas veces mejoró respecto a la nota anterior y, al final, redondear las notas a enteros modificando la propia lista.
trimestre.py
NOTAS = [6.2, 7.4, 5.6, 8.1, 9.3, 7.8, 6.6]
VENTANA = 3
def media(tramo):
"""Media de un trozo de la lista."""
acumulador = 0
for nota in tramo:
acumulador += nota
return acumulador / len(tramo)
def medias_moviles(notas, ventana):
"""Media de cada grupo de 'ventana' notas seguidas."""
resultado = []
for inicio in range(len(notas) - ventana + 1):
tramo = notas[inicio:inicio + ventana]
resultado = resultado + [round(media(tramo), 2)]
return resultado
def contar_mejoras(notas):
"""Cuántas veces una nota supera a la anterior."""
contador = 0
for pos in range(1, len(notas)):
if notas[pos] > notas[pos - 1]:
contador += 1
return contador
def redondear_en_sitio(notas):
"""Redondea cada nota a un entero, modificando la propia lista."""
for pos in range(len(notas)):
notas[pos] = round(notas[pos])
print("Notas:", NOTAS)
print("Medias móviles de 3:", medias_moviles(NOTAS, VENTANA))
print("Veces que mejoró respecto a la anterior:", contar_mejoras(NOTAS))
redondear_en_sitio(NOTAS)
print("Notas redondeadas:", NOTAS)
Lánzalo con python3 trimestre.py:
Notas: [6.2, 7.4, 5.6, 8.1, 9.3, 7.8, 6.6] Medias móviles de 3: [6.4, 7.03, 7.67, 8.4, 7.9] Veces que mejoró respecto a la anterior: 3 Notas redondeadas: [6, 7, 6, 8, 9, 8, 7]
Entre líneas:
medias_movilesrecorre los inicios posibles de cada grupo: con 7 notas y una ventana de 3 hay 5 grupos, y por eso el rango eslen(notas) - ventana + 1. Para cada inicio, el slicenotas[inicio:inicio + ventana]recoge exactamente 3 notas seguidas.- El resultado se va construyendo con
resultado = resultado + [...]: la lista vacía[]es el elemento neutro de la concatenación, igual que el 0 lo es de la suma. Es un acumulador, solo que de listas. round(media(tramo), 2)evita que salgan decimales larguísimos del tipo 7.033333333333333.contar_mejorasnecesita comparar cada nota con la anterior, así que no sirve unforpor elemento: recorre las posiciones desde la 1 (la 0 no tiene anterior) y comparanotas[pos]connotas[pos - 1]. Mejoran en 7.4, 8.1 y 9.3, así que son 3.redondear_en_sitioes el ejemplo de la teoría: como tiene que cambiar la lista, recorre por índice y asigna anotas[pos]. No devuelve nada, porque la modificación ya se ve fuera de la función.- Fíjate también en el orden de los
print: las medias móviles y el conteo se calculan antes de redondear. Si redondearas primero, los resultados serían otros.
Pruébalo tú
- Cambia
redondear_en_sitiopara que recorra confor nota in notas:y asignenota = round(nota). Ejecuta y explica por qué la lista no se redondea. - Prueba con
VENTANA = 5. ¿Cuántas medias salen? ¿Y conVENTANA = 7?
Míralo paso a paso en Python Tutor
Python Tutor ejecuta tu código línea a línea y dibuja variables, funciones y objetos en memoria. Elige Python 3 y pega una versión corta del buscador del Programa 2:
NOMBRES = ["Lucía", "Marcos", "Carla"]
NOTAS = [7.5, 4.0, 6.0]
def posicion(secuencia, buscado):
for pos, item in enumerate(secuencia):
if item == buscado:
return pos
return -1
pos = posicion(NOMBRES, "Marcos")
print("Posición:", pos, "Nota:", NOTAS[pos])
pos = posicion(NOMBRES, "Pablo")
print("Posición:", pos)
Salida esperada:
Posición: 1 Nota: 4.0 Posición: -1
Qué observar, paso a paso:
- Al empezar, las variables globales muestran
NOMBRESyNOTAScomo dos cajas de lista con tres elementos cada una. Todavía no se ha llamado a nada. - Al entrar en
posicion, se crea un marco nuevo consecuenciaapuntando a la misma lista queNOMBRES. Mientras recorre, fíjate en cómoposyitemcambian en cada vuelta deenumerate. - En la vuelta de
pos = 1,itemvale"Marcos"y elifse cumple: la función sale en ese momento conreturn pos. Observa que no llega a mirar a «Carla». - En la segunda llamada, con «Pablo», el bucle da las tres vueltas sin encontrar nada, y solo entonces ejecuta
return -1. Esa es la diferencia entre elreturnde dentro y el de fuera del bucle. - Al terminar,
posglobal vale-1. Si ahora escribierasNOTAS[pos], no daría error y te devolvería la última nota, porque el índice -1 es válido. Por eso hay que comprobar el-1antes de usarlo, como hacenota_de.
Para terminar
Hoy has visto los esquemas más típicos de las secuencias en acción: acumulador y contador, búsquedas de extremos, búsqueda de posición con enumerate, slices y recorridos por índice para modificar. Si algo no te ha cuadrado, vuelve a la teoría; y cuando te sientas cómodo, pasa a los ejercicios, donde los fallos vienen incluidos.
¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
