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.

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. En media y en contar_aprobados desempaquetamos con for nombre, nota in alumnos, que se lee mucho mejor que alumno[0] y alumno[1].
  • media es un acumulador: parte de 0 (el neutro de la suma), suma todas las notas y divide entre len(alumnos). En contar_aprobados el esquema es casi el mismo, pero suma siempre 1 y solo cuando la nota llega al aprobado: un contador selectivo.
  • mejor y peor son 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 escrito candidato = ("", 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.
  • mejor y peor devuelven la tupla entera, y el programa la desempaqueta en nombre_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 :.2f muestra como 6.42. Aprueban Lucía, Aitana, Carla y Dani.

Pruébalo tú

  • Cambia candidato = alumnos[0] en peor por candidato = (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:

  • posicion recorre la lista con enumerate, que da parejas (pos, item). Cuando encuentra el valor, devuelve ya la posición; solo si termina el bucle sin encontrarlo devuelve -1. Ese return -1 está fuera del bucle: es el error clásico del else.
  • -1 es una buena marca de «no encontrado» porque nunca puede ser una posición. Si hubiéramos devuelto 0 o False, no podríamos distinguir «no está» de «está el primero».
  • nota_de aprovecha la posición para ir a la otra lista: NOTAS[pos]. Si no está, devuelve None, y el programa lo comprueba con is None antes de mostrar nada.
  • Fíjate en el orden de las comprobaciones: primero se pregunta por None y después se usa el resultado. Si hubieras mostrado nota sin comprobar, habrías impreso None en lugar de un mensaje útil.
  • Los print de 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 NOMBRES hace una búsqueda y devuelve True o False; por dentro es el mismo bucle de posicion, 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 con range(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_moviles recorre los inicios posibles de cada grupo: con 7 notas y una ventana de 3 hay 5 grupos, y por eso el rango es len(notas) - ventana + 1. Para cada inicio, el slice notas[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_mejoras necesita comparar cada nota con la anterior, así que no sirve un for por elemento: recorre las posiciones desde la 1 (la 0 no tiene anterior) y compara notas[pos] con notas[pos - 1]. Mejoran en 7.4, 8.1 y 9.3, así que son 3.
  • redondear_en_sitio es el ejemplo de la teoría: como tiene que cambiar la lista, recorre por índice y asigna a notas[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_sitio para que recorra con for nota in notas: y asigne nota = round(nota). Ejecuta y explica por qué la lista no se redondea.
  • Prueba con VENTANA = 5. ¿Cuántas medias salen? ¿Y con VENTANA = 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:

  1. Al empezar, las variables globales muestran NOMBRES y NOTAS como dos cajas de lista con tres elementos cada una. Todavía no se ha llamado a nada.
  2. Al entrar en posicion, se crea un marco nuevo con secuencia apuntando a la misma lista que NOMBRES. Mientras recorre, fíjate en cómo pos y item cambian en cada vuelta de enumerate.
  3. En la vuelta de pos = 1, item vale "Marcos" y el if se cumple: la función sale en ese momento con return pos. Observa que no llega a mirar a «Carla».
  4. 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 el return de dentro y el de fuera del bucle.
  5. Al terminar, pos global vale -1. Si ahora escribieras NOTAS[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 -1 antes de usarlo, como hace nota_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.

Siguiente paso: ejerciciosListas y tuplas en Python — ejercicios con recortes, un resumen de clase y aprobados contra suspensos (y la tupla de seis elementos que solo tenía uno)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *