Módulo 1 · Lección 25 de 59
Listas por comprensión
La forma compacta de construir una lista a partir de otra, cómo se deriva del bucle equivalente y cuándo no conviene usarla.
Hay un gesto que repites constantemente: partir de una lista, transformar cada elemento y guardar el resultado en otra lista. Tres líneas de bucle para algo que se dice en una frase: "los cuadrados de estos números".
Python tiene una sintaxis propia para ese caso concreto, la lista por comprensión. No hace nada que no pudieras hacer con un for; simplemente lo escribe en una línea que se lee mejor. Se usa tanto que necesitas saber leerla aunque decidas no escribirla.
De un bucle a una comprensión, paso a paso#
Este es el bucle de siempre:
numeros = [1, 2, 3, 4, 5]
cuadrados = []
for numero in numeros:
cuadrados.append(numero ** 2)
print(cuadrados)[1, 4, 9, 16, 25]Fíjate en que ahí solo hay tres datos importantes: qué guardas (numero ** 2), cómo se llama cada elemento (numero) y de dónde salen (numeros). Todo lo demás es andamiaje: crear la lista vacía, escribir append, los dos puntos, la sangría.
La comprensión coloca esos tres datos entre corchetes, en este orden:
numeros = [1, 2, 3, 4, 5]
cuadrados = [numero ** 2 for numero in numeros]
print(cuadrados)[1, 4, 9, 16, 25]Los corchetes de fuera son los que anuncian que el resultado es una lista. Dentro, la parte que va antes del for es lo que se guarda en cada vuelta, y la parte del for es exactamente la misma que escribirías en un bucle normal.
La transformación, vista una al lado de la otra:
| Bucle | Comprensión |
|---|---|
cuadrados = [] | Los corchetes de fuera |
for numero in numeros: | for numero in numeros (al final, sin dos puntos) |
cuadrados.append(numero ** 2) | numero ** 2 (al principio) |
Se lee de dentro hacia fuera, pero en español sale bastante natural: "el cuadrado de cada número, para cada número de la lista numeros".
Más ejemplos de transformación#
La expresión de la izquierda puede ser cualquier cosa que produzca un valor:
nombres = ["ana pérez", "luis gómez", "marta ruiz"]
print([nombre.title() for nombre in nombres])
print([len(nombre) for nombre in nombres])
print([nombre.split()[0] for nombre in nombres])['Ana Pérez', 'Luis Gómez', 'Marta Ruiz']
[9, 10, 10]
['ana', 'luis', 'marta']Y el origen puede ser cualquier cosa recorrible: otra lista, un range o una cadena.
print([numero * 3 for numero in range(1, 6)])
print([letra.upper() for letra in "python"])[3, 6, 9, 12, 15]
['P', 'Y', 'T', 'H', 'O', 'N']Esto resuelve por fin el problema que dejamos pendiente con join, que solo acepta cadenas:
precios = [29.99, 15.50, 189.00]
textos = [str(precio) for precio in precios]
print(" | ".join(textos))
print(", ".join([f"{precio:.2f} €" for precio in precios]))29.99 | 15.5 | 189.0
29.99 €, 15.50 €, 189.00 €Con condición: el if al final#
Añadiendo un if después del for, solo entran en la lista los elementos que lo cumplen. Es el equivalente a este bucle:
temperaturas = [18.2, 24.5, 31.0, 27.8, 15.4, 33.2]
calurosas = []
for temperatura in temperaturas:
if temperatura > 25:
calurosas.append(temperatura)
print(calurosas)[31.0, 27.8, 33.2]Y en una línea:
temperaturas = [18.2, 24.5, 31.0, 27.8, 15.4, 33.2]
calurosas = [temperatura for temperatura in temperaturas if temperatura > 25]
print(calurosas)[31.0, 27.8, 33.2]Aquí la expresión de la izquierda es el elemento tal cual, porque no queremos transformarlo, solo seleccionarlo. Nada impide hacer las dos cosas a la vez:
palabras = ["sol", "murciélago", "pez", "ventana", "mar"]
largas_en_mayusculas = [palabra.upper() for palabra in palabras if len(palabra) > 3]
print(largas_en_mayusculas)['MURCIÉLAGO', 'VENTANA']El orden de lectura es: primero el for recorre, después el if filtra y al final la expresión transforma lo que ha pasado el filtro.
Este patrón hace que any y all se lean como una frase, tal y como te anuncié en la lección anterior:
notas = [7.5, 4.0, 9.25, 6.0]
print(any([nota < 5 for nota in notas]))
print(all([nota >= 5 for nota in notas]))
print(len([nota for nota in notas if nota >= 5]))True
False
3Se leen casi solas: "¿alguna nota es menor que cinco?", "¿todas las notas llegan al cinco?", "cuántas notas llegan al cinco". Contar filtrando con len() de una comprensión es un recurso que usarás mucho.
Con expresión condicional: el if delante#
Esto es diferente y se confunde a menudo. Cuando el if va antes del for no filtra: elige qué valor guardar, y todos los elementos entran en la lista. Es el operador condicional en una línea que ya conoces.
notas = [7.5, 4.0, 9.25, 3.5]
etiquetas = ["aprobado" if nota >= 5 else "suspenso" for nota in notas]
print(etiquetas)
print(len(etiquetas))['aprobado', 'suspenso', 'aprobado', 'suspenso']
4Cuatro notas entran, cuatro etiquetas salen. Compara con la versión que filtra:
notas = [7.5, 4.0, 9.25, 3.5]
print([nota for nota in notas if nota >= 5]) # filtra: salen 2
print(["ok" if nota >= 5 else "ko" for nota in notas]) # transforma: salen 4[7.5, 9.25]
['ok', 'ko', 'ok', 'ko']La regla para no perderse:
Dónde va el if | Qué hace | Cuántos elementos salen |
|---|---|---|
Al final, después del for | Filtra | Menos o igual que los de entrada |
Al principio, antes del for | Elige el valor | Exactamente los de entrada |
Comprensión anidada#
Se puede poner más de un for, y se leen en el mismo orden en que los escribirías como bucles anidados: el primero es el de fuera.
tallas = ["S", "M"]
colores = ["rojo", "azul"]
combinaciones = [f"{talla}-{color}" for talla in tallas for color in colores]
print(combinaciones)['S-rojo', 'S-azul', 'M-rojo', 'M-azul']También sirve para aplanar una lista de listas, que es el uso más frecuente:
por_grupos = [[8.5, 6.0], [9.25, 4.0], [7.0]]
todas = [nota for grupo in por_grupos for nota in grupo]
print(todas)
print(f"Media general: {sum(todas) / len(todas):.2f}")[8.5, 6.0, 9.25, 4.0, 7.0]
Media general: 6.95Y una comprensión puede estar dentro de otra, que es la forma limpia de crear la cuadrícula que en la lección anterior te salió mal con * 3:
cuadricula = [[0] * 3 for fila in range(3)]
cuadricula[0][0] = 9
print(cuadricula)[[9, 0, 0], [0, 0, 0], [0, 0, 0]]Funciona porque la comprensión ejecuta [0] * 3 tres veces, creando una fila nueva en cada vuelta, en vez de repetir la misma.
Un ejemplo completo#
lineas = ["ana:8.5", "luis:4.0", "marta:9.25", "pedro:3.5", "sara:6.0"]
nombres = [linea.split(":")[0].title() for linea in lineas]
notas = [float(linea.split(":")[1]) for linea in lineas]
aprobados = [nota for nota in notas if nota >= 5]
estados = ["APTO" if nota >= 5 else "NO APTO" for nota in notas]
for nombre, nota, estado in zip(nombres, notas, estados):
print(f"{nombre:<8} {nota:>5.2f} {estado}")
print("-" * 24)
print(f"Aprobados: {len(aprobados)} de {len(notas)}")
print(f"Media de los aprobados: {sum(aprobados) / len(aprobados):.2f}")
print(f"Suspensos: {[nombre for nombre, nota in zip(nombres, notas) if nota < 5]}")Ana 8.50 APTO
Luis 4.00 NO APTO
Marta 9.25 APTO
Pedro 3.50 NO APTO
Sara 6.00 APTO
------------------------
Aprobados: 3 de 5
Media de los aprobados: 7.92
Suspensos: ['Luis', 'Pedro']Fíjate en la última línea: una comprensión puede recorrer un zip y desempaquetar dos variables, igual que un for normal.