Saltar al contenido
comprensiones.py · devschool

Comprensiones de listas en Python

Lección 10 de 19 · 11 min de lectura · Actualizado el

En esta lección
  1. Del bucle for a la comprensión
  2. Filtrar con if
  3. Elegir un valor con if-else
  4. Comprensiones anidadas
  5. Comprensiones de diccionarios
  6. Comprensiones de conjuntos
  7. Expresiones generadoras
  8. Cuándo NO usar comprensiones
  9. Errores frecuentes
  10. Resumen

Muchas veces un bucle for solo sirve para construir una lista nueva a partir de otra: pasar precios a euros con IVA, quedarte con los aprobados, poner nombres en mayúsculas. Python tiene una sintaxis compacta para eso: las comprensiones (comprehensions). En esta lección aprenderás a escribirlas para listas, diccionarios y conjuntos, a usar expresiones generadoras con sum, any y all, y, igual de importante, a reconocer cuándo no conviene usarlas.

Necesitas tener claros los bucles y las listas, donde ya viste un primer ejemplo.

Del bucle for a la comprensión

Imagina una tienda que guarda los precios sin IVA y quiere calcularlos con el 21 % incluido. Con lo que ya sabes lo harías así:

precios = [10, 25, 8, 40]

con_iva = []
for p in precios:
    con_iva.append(p * 1.21)

print(con_iva)   # [12.1, 30.25, 9.68, 48.4]

Son tres líneas con un patrón que se repite muchísimo: crear una lista vacía, recorrer otra y hacer append. La comprensión hace lo mismo en una línea:

con_iva = [p * 1.21 for p in precios]
print(con_iva)   # [12.1, 30.25, 9.68, 48.4]

La estructura es siempre:

[expresión  for  variable  in  iterable]

Se lee casi como una frase: “p * 1.21 para cada p en precios”. Fíjate en que la expresión va primero: es lo que acabará dentro de la lista nueva.

¿Por qué usarla? Porque dice de un vistazo qué se construye, sin tener que seguir el append por el bucle. Además es algo más rápida, porque Python la optimiza internamente. La lista original no se modifica: siempre se crea una nueva.

Más ejemplos de transformación:

nombres = ["ana", "luis", "marta"]

print([n.capitalize() for n in nombres])   # ['Ana', 'Luis', 'Marta']
print([len(n) for n in nombres])           # [3, 4, 5]
print([n * n for n in range(1, 6)])        # [1, 4, 9, 16, 25]

Sirve con cualquier cosa que se pueda recorrer: listas, range, cadenas, tuplas, diccionarios…

Filtrar con if

Si añades un if al final, solo entran los elementos que cumplen la condición:

notas = [7, 3, 9, 4.5, 5, 8]

aprobadas = [n for n in notas if n >= 5]
print(aprobadas)   # [7, 9, 5, 8]

Equivale a:

aprobadas = []
for n in notas:
    if n >= 5:
        aprobadas.append(n)

Puedes filtrar y transformar a la vez, y también desempaquetar tuplas en la variable del bucle:

productos = [("teclado", 25), ("raton", 12), ("monitor", 180), ("cable", 5)]

baratos = [nombre for nombre, precio in productos if precio < 30]
print(baratos)   # ['teclado', 'raton', 'cable']

Con cadenas es muy útil para limpiar texto:

texto = "Hola, qué tal 2024!"
solo_letras = "".join([c for c in texto if c.isalpha()])
print(solo_letras)   # Holaquétal

Elegir un valor con if-else

A veces no quieres descartar elementos, sino transformar cada uno de una forma u otra. Para eso se usa la expresión condicional valor_si if condición else valor_no, y va al principio, como parte de la expresión:

notas = [7, 3, 9, 4.5, 5, 8]

resultado = ["aprobado" if n >= 5 else "suspenso" for n in notas]
print(resultado)
# ['aprobado', 'suspenso', 'aprobado', 'suspenso', 'aprobado', 'aprobado']

Esta es la duda más frecuente con las comprensiones, así que compáralas bien:

FormaPosición del ifQué haceLongitud del resultado
[x for x in datos if cond]Al finalFiltra: descarta elementosIgual o menor
[a if cond else b for x in datos]Al principio, con elseElige qué valor ponerSiempre igual

Cuidado: [n for n in notas if n >= 5 else 0] da SyntaxError. El if del final no admite else. Si necesitas else, el if va delante del for.

Comprensiones anidadas

Una comprensión puede tener varios for. Se leen de izquierda a derecha en el mismo orden que los bucles anidados.

Aplanar una lista de listas

grupos = [["Ana", "Luis"], ["Marta"], ["Pablo", "Sara", "Eva"]]

todos = [alumno for grupo in grupos for alumno in grupo]
print(todos)   # ['Ana', 'Luis', 'Marta', 'Pablo', 'Sara', 'Eva']

Es exactamente lo mismo que:

todos = []
for grupo in grupos:          # primer for
    for alumno in grupo:      # segundo for
        todos.append(alumno)

Un truco para no liarte: escribe los for en el mismo orden en que los pondrías con bucles normales y pon la expresión delante.

Combinaciones

Dos for generan todas las combinaciones posibles, por ejemplo, las variantes de una camiseta:

tallas = ["S", "M", "L"]
colores = ["rojo", "azul"]

variantes = [f"{t}-{c}" for t in tallas for c in colores]
print(variantes)
# ['S-rojo', 'S-azul', 'M-rojo', 'M-azul', 'L-rojo', 'L-azul']

Matrices

Si metes una comprensión dentro de la expresión de otra, obtienes una lista de listas (una matriz). Aquí la tabla de multiplicar del 1 al 3:

tabla = [[f * c for c in range(1, 4)] for f in range(1, 4)]
print(tabla)   # [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

Y así se traspone una matriz (las filas pasan a ser columnas):

matriz = [[1, 2, 3],
          [4, 5, 6]]

traspuesta = [[fila[i] for fila in matriz] for i in range(3)]
print(traspuesta)   # [[1, 4], [2, 5], [3, 6]]

La comprensión es además la forma correcta de crear un tablero vacío. Compara:

tablero = [[0] * 3 for _ in range(3)]
tablero[0][0] = 1
print(tablero)   # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]

malo = [[0] * 3] * 3
malo[0][0] = 1
print(malo)      # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

[[0] * 3] * 3 repite la misma fila tres veces: al cambiar una, cambian todas. La comprensión crea una fila nueva en cada vuelta. El _ es el nombre habitual para una variable que no vas a usar.

Comprensiones de diccionarios

Con llaves y la forma clave: valor construyes un diccionario:

nombres = ["Ana", "Luis", "Marta"]
longitudes = {n: len(n) for n in nombres}
print(longitudes)   # {'Ana': 3, 'Luis': 4, 'Marta': 5}

Lo más habitual es recorrer otro diccionario con .items() para transformar o filtrar sus valores:

precios = {"pan": 1.2, "leche": 0.95, "cafe": 4.5}

subida = {p: round(v * 1.1, 2) for p, v in precios.items()}
print(subida)    # {'pan': 1.32, 'leche': 1.04, 'cafe': 4.95}

baratos = {p: v for p, v in precios.items() if v < 2}
print(baratos)   # {'pan': 1.2, 'leche': 0.95}

Invertir claves y valores también es inmediato:

codigos = {"ES": "España", "FR": "Francia"}
paises = {v: k for k, v in codigos.items()}
print(paises)   # {'España': 'ES', 'Francia': 'FR'}

Y para juntar dos listas en un diccionario se usa zip:

alumnos = ["Ana", "Luis"]
notas = [8, 6]

print({a: n for a, n in zip(alumnos, notas)})   # {'Ana': 8, 'Luis': 6}
print(dict(zip(alumnos, notas)))                # {'Ana': 8, 'Luis': 6}

Si no transformas nada, dict(zip(...)) es más sencillo.

Comprensiones de conjuntos

Con llaves pero sin los dos puntos obtienes un conjunto, que elimina duplicados automáticamente. Perfecto para limpiar datos:

emails = ["Ana@Mail.com", "ana@mail.com", "luis@mail.com", "LUIS@mail.com "]

unicos = {e.strip().lower() for e in emails}
print(sorted(unicos))   # ['ana@mail.com', 'luis@mail.com']

Los conjuntos no tienen orden, por eso usamos sorted para mostrar el resultado siempre igual.

Cuidado: {} vacío es un diccionario, no un conjunto. Y no existe la “comprensión de tuplas”: (x for x in datos) con paréntesis es otra cosa, una expresión generadora.

Expresiones generadoras

Si cambias los corchetes por paréntesis, no se crea ninguna lista. Se crea un generador: un objeto que va produciendo los valores de uno en uno, cuando se le piden.

cuadrados = (n * n for n in range(4))
print(list(cuadrados))   # [0, 1, 4, 9]
print(list(cuadrados))   # []  (ya se ha agotado)

Un generador solo se puede recorrer una vez. A cambio, casi no ocupa memoria: una lista de un millón de números ocupa unos 8 MB, mientras que el generador equivalente ocupa unos 200 bytes, tenga los elementos que tenga.

Su uso estrella es pasarlo directamente a funciones como sum, max, min o "".join. Cuando el generador es el único argumento, puedes quitar los paréntesis extra:

carrito = [("camiseta", 15.0, 2), ("pantalon", 30.0, 1), ("calcetines", 4.5, 3)]

total = sum(precio * cantidad for _, precio, cantidad in carrito)
print(total)   # 73.5

palabras = ["sol", "mar", "casa", "luna", "pez"]
print(max(len(p) for p in palabras))   # 4

Aquí no hace falta guardar la lista de importes: solo queremos la suma. Por eso sum(x for x in ...) es preferible a sum([x for x in ...]).

any y all

Dos funciones que combinan de maravilla con generadores:

  • any(...) devuelve True si al menos un elemento es verdadero.
  • all(...) devuelve True si todos los elementos son verdaderos.
notas = [7, 3, 9, 4.5, 5, 8]

print(any(n < 5 for n in notas))    # True  (hay algún suspenso)
print(all(n >= 5 for n in notas))   # False (no han aprobado todos)

contrasenia = "secreto123"
tiene_numero = any(c.isdigit() for c in contrasenia)
print(tiene_numero)   # True

Además se detienen en cuanto conocen la respuesta: any para en el primer True y all en el primer False. Con una lista vacía, any([]) es False y all([]) es True.

Cuándo NO usar comprensiones

Las comprensiones son para construir una colección. No son una forma “más pro” de escribir cualquier bucle.

  • Si no vas a usar la lista resultante, usa un for. Esto funciona, pero crea una lista de None que no sirve para nada:

    [print(n) for n in range(2)]   # mal: resultado [None, None]
    
    for n in range(2):             # bien
        print(n)
  • Si la lógica es larga, usa un for. Con más de dos for, varios if o una expresión condicional anidada, el código se vuelve ilegible. Si tienes que leerla tres veces para entenderla, reescríbela.

  • Si necesitas varias instrucciones por elemento (un try, un print de depuración, actualizar un contador), la comprensión no las admite.

  • Si solo quieres un resultado (suma, máximo, comprobar algo), usa un generador dentro de sum, max, any o all, no una lista.

Consejo: si la transformación se complica, sácala a una función con nombre y úsala en la comprensión: [calcular_precio(p) for p in productos]. Queda corta y se entiende.

Errores frecuentes

  • Poner el else en el filtro: [x for x in datos if x > 0 else 0] es un SyntaxError. El if-else va delante del for.
  • Invertir el orden de los for anidados: [alumno for alumno in grupo for grupo in grupos] da NameError, porque grupo se usa antes de definirse.
  • Reutilizar un generador agotado: la segunda vez que lo recorres está vacío.
  • Crear matrices con [[0] * n] * n: todas las filas son la misma. Usa [[0] * n for _ in range(n)].
  • Esperar orden en un conjunto: {...} sin : no garantiza el orden de los elementos.

Resumen

QuieresSintaxisEjemplo
Transformar[expr for x in datos][p * 1.21 for p in precios]
Filtrar[x for x in datos if cond][n for n in notas if n >= 5]
Elegir valor[a if cond else b for x in datos]["par" if n % 2 == 0 else "impar" for n in nums]
Aplanar[y for x in datos for y in x][a for g in grupos for a in g]
Diccionario{k: v for ...}{n: len(n) for n in nombres}
Conjunto{expr for ...}{e.lower() for e in emails}
Solo un resultadosum(expr for ...)sum(p * c for _, p, c in carrito)
¿Alguno? ¿Todos?any(...), all(...)all(n >= 5 for n in notas)

Toda comprensión equivale a un bucle for con append (o asignación en un diccionario, o add en un conjunto). Si alguna vez dudas, escríbela primero como bucle y conviértela después. En la próxima lección profundizarás en las funciones, que combinarás a menudo con las comprensiones.

Pon a prueba lo que has aprendido

[Python] ¿Qué imprime este código?
notas = [4, 7, 5, 9]
print(["A" if n >= 5 else "S" for n in notas])

[Python] ¿Qué imprime este código?
print([n * 2 for n in range(6) if n % 2])

[Python] ¿Qué imprime este código?
g = (x for x in [1, 2, 3])
print(sum(g), sum(g))

[Python] Quieres saber si TODOS los productos de una lista tienen stock mayor que 0. ¿Qué opción es la más adecuada?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.