Comprensión de listas y otras colecciones

Comprensión de listas y otras colecciones



La comprensión de listas en Python es un método sintáctico para crear listas (y por extensión también otras colecciones que veremos más abajo) a partir de los elementos de otras listas (o colecciones) de una forma rápida de escribir, muy legible y funcionalmente eficiente.

Consideremos la siguiente lista de lenguajes:

>>> languages = ["python", "c", "c++", "java"]

Usando comprensión de listas, podemos crear una nueva lista con las mismas cadenas pero con su primera letra en mayúscula (es decir, aplicar el método capitalize() de las cadenas).

>>> cap_languages = [language.capitalize() for language in languages]

Que es funcionalmente similar a lo siguiente:

cap_languages = []
for language in languages:
    cap_languages.append(language.capitalize())

En ambos casos, la nueva lista cap_languages resulta en:

['Python', 'C', 'C++', 'Java']

Lo interesante del primer método es que, como decíamos al principio, se escribe más rápido y se lee mejor. Por otra parte, a través de este método podríamos incluso haber trabajado sobre la misma lista, sin necesidad de duplicar la información.

>>> languages = [language.capitalize() for language in languages]
>>> languages
['Python', 'C', 'C++', 'Java']

Algo que en un bucle convencional deberíamos expresar más o menos así (mucho más trabajoso y ofuscado):

for language in languages[::]:
    languages.append(language.capitalize())
    del languages[0]

Veamos otro ejemplo. Consderemos esta lista de números:

>>> numbers = [1, 2, 3, 4, 5]

Siguiendo la misma sintaxis que antes, vamos a crear una nueva lista doubled_numbers que contenga el doble de cada uno de los números de numbers.

>>> doubled_numbers = [n * 2 for n in numbers]
>>> doubled_numbers
[2, 4, 6, 8, 10]

En estos dos ejemplos trabajados todos los elementos de la lista primigenia aparecen de algún modo transformados en la nueva lista que generamos: en el primer caso se modificaba la primera letra de la cadena para que fuese mayúscula; en este segundo, se multiplica el número por dos. Ahora bien, si queremos indicar que los elementos deben incluirse en la nueva lista en función de una condición, podemos agregar ─justamente─ un condicional. Por ejemplo, el siguiente código crea una lista con números del 1 al 100 que sean múltiplos de 5.

>>> multiples = [n for n in range(1, 101) if n % 5 == 0]
>>> multiples
[5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]

Este código es funcionalmente igual al siguiente:

multiples = []
for n in range(1, 101):
    if n % 5 == 0:
        multiples.append(n)

Además, para quienes están versados en la programación funcional, también se obtiene el mismo resultado aplicando la función incorporada filter():

>>> multiples = list(filter(lambda n: n % 5 == 0, range(1, 101)))

Creo que hay consenso en que la comprensión de listas es mucho más elocuente en casos de estas características.

En función de estos tres ejemplos de comprensión de listas vamos a generalizar su sintaxis del siguiente modo:

[expresion for variable in colección if condición]

A menudo la expresión (es decir, aquello que terminará inserto en la lista resultante) es igual a la variable (como vimos en el último ejemplo), y la condición es opcional. La colección puede ser una lista o cualquier otro objeto iterable (esto es, cualquier cosa sobre lo que podamos aplicar un bucle «for»).

A través de la comprensión de listas también podemos expresar de forma compacta un conjunto de bucles anidados. Por ejemplo, el siguiente código crea una lista points que contiene (en forma de tuplas de dos elementos) la posición de todos los puntos bidimencionales entre las coordenadas (0, 0) y (5, 10).

points = []
for x in range(0, 5 + 1):
    for y in range(0, 10 + 1):
        points.append((x, y))
print(points)

Imprimiendo:

[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), (0, 10), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (2, 10), (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (3, 10), (4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9), (4, 10), (5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (5, 10)]

¿Cómo traducimos esto usando el nuevo método? Sencillamente así:

points = [(x, y) for y in range(0, 5 + 1) for x in range(0, 10 + 1)]
print(points)

Podríamos incluso agregar una condición usando ambas variables, por ejemplo, para mostrar solo los puntos en los que x es igual a y.

points = [(x, y) for y in range(0, 5 + 1) for x in range(0, 10 + 1)
          if x == y]

Otras colecciones

Esto que acabamos de decir se aplica por extensión a otras colecciones. Por ejemplo, podemos crear un diccionario de la misma forma, pero en este caso utilizamos llaves en lugar de corchetes.

>>> doubles = {n: n * 2 for n in range(1, 11)}
>>> doubles
{1: 2, 2: 4, 3: 6, 4: 8, 5: 10, 6: 12, 7: 14, 8: 16, 9: 18, 10: 20}

Este fragmento genera un diccionario (doubles) donde las claves son números enteros del 1 al 10 y los valores, el doble de cada una de esas claves.

La sintaxis para la comprensión de diccionarios es muy similar:

{clave: valor for variable in coleccion if condicion}

Algo casi idéntico sintácticamente es la comprensión de conjuntos (sets), que también se realiza con llaves pero prescindiendo de los dos puntos.

# ¡Esto crea un conjunto, no un diccionario!
>>> doubles = {n * 2 for n in range(1, 11)}
>>> doubles
{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}

Por último, una de las cosas más interesantes de esta técnica es la comprensión de generadores. Se trata de colecciones cuyos elementos se crean a medida que se recorren en un bucle «for» (o bien a medida que son pasados como argumento a la función incorporada next()). La sintaxis para ello es usando paréntesis:

>>> doubles = (n * 2 for n in range(1, 1000000000000000000))

Esto crea un objeto que genera progresivamente (a medida que son utilizados) dobles de números del 1 al 1.000.000.000.000.000.000 (un trillón), pero que se ejecuta casi instantáneamente porque los elementos no son creados hasta que no sean obtenidos. Es similar a:

def get_doubles():
    for n in range(1, 1000000000000000000):
        yield n * 2

doubles = get_doubles()

Dado que los paréntesis se usan para esa situación en particular (en lugar de los corchetes o las llaves), si queremos generar una tupla usando el método de comprensión lo debemos indicar específicamente así:

>>> doubles = tuple(n * 2 for n in range(1, 11))
>>> doubles
(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)



Deja un comentario