Estructuras por Comprensión

Tipos de definición de Conjuntos

Recordarán (esperamos) de matemáticas, algo de teoría de conjuntos. En particular, el hecho de que existen dos formas de definir un conjunto

    • por extensión: donde enumeramos cada elemento del conjunto
    • por comprensión: donde definimos mediante una "fórmula", qué características tendrán los elementos, sin mencionarlos uno a uno.

Hasta ahora a la hora de programar, siempre trabajaron con la idea de listas por extensión.

Por ejemplo:

numeros = [ 2, 6, 9, 1, 12]
productos = [ Producto("Aceite", 9.50), Producto("Sal fina", 6.25) ]

Esta forma de definir las listas está en general asociada a listas fijas (hardcodeadas) o a la forma imperativa de programación. Donde escribimos código que opera con la lista, agregándole, o sacándole elementos.

Por otro lado, hace poquito, vimos otra forma de trabajar con listas, utilizando las funciones de orden superiod: filter, map y reduce.

En particular, filter y map, nos permitían derivar una lista a partir de otra, aplicando algún criterio para filtrar, o una transformación. Ambas cosas representan ejemplos de definición de una lista por comprensión. Porque, por ejemplo podemos definir una lista como:

todos los números enteros positivos van desde el 0 hasta divisibles por 3

Es decir, que, a través del uso de las funciones filter y map, definimos conjuntos por compresión.

Sin embargo, muchos lenguajes poséen, además de estas funciones, una sintaxis específica, para conseguir el mismo resultado, en forma más "concisa" o declarativa. Y a ésto, en programación se le llama listas por comprensión o list comprehensions (sí, es el mismo nombre que el concepto matemático, ojo!)

Listas por comprensión simples en python

En nuestro caso, Python (así como Haskell, CoffeeScript, Clojure, y muchos otros lenguajes) tiene esta sintaxis especial llamada list comprehensions.

Veamos unos ejemplos. A partir de una lista de números, queremos obtener otra que tenga el resultado de la division de cada elemento por 2, es decir las mitades.

Utilizando map() y lambdas haríamos:

numeros = [2, 4, 20, 200, 10, 22, 0]
print 'divididos por 2 con map:', map(lambda n: n / 2, numeros)

Utilizando listas por comprensión esto se puede expresar como:

print 'dividios por 2 por comprension:', [ n / 2 for n in numeros ]

Y tiene el mismo efecto.

Sintaxis

La sintaxis para definir una lista por compresión es:

[ definiciónDeUnElementoDeLaListaResultado for variable in otraLista ]

Ojo porque, de nuevo acá Python utiliza las palabras reservadas for pero fíjense que esto no es ni por lejos lo mismo que el for que ustedes vieron en C para iterar. Esta linea jústamente no tiene un sentido temporal, es decir que es mucho más declarativa en lugar de imperativa.

Explicamos algunas partes:

    • El resultado de esta expresión es una otra lista (después veremos que se pueden definir otras cosas que no son listas)
    • variable: es un nombre de variable que nosotros definimos, para nombrar a cada elemento de la secuencia original de la cual queremos derivar la lista. Este nombre de variable lo vamos a utilizar en la parte izquierda de la expresión, que es lo que sigue..
    • definiciónDeUnElementoDeLaListaResultado: es una expresión que definirá a todos los elementos de la lista resultado. Si les sirve, pueden pensarlo como el cuerpo de la expresión lambda que recibe un solo elemnto (con el nombre "variable" que hayamos definido) y retorna un elemento (no hace falta escribir el return).

Más Ejemplos Simples

Veamos más ejemplos, para entender qué cosas se pueden hacer con estas expresiones:

Obtenemos una lista con los elementos originales incrementados en 1

masUno = [n + 1 for n in numeros]

Obtenemos una lista que indica si los elementos originales eran pares. Lo interesante acá es que como parte de la expresión de "definición de los elementos", estamos invocando a una función. Sí, se puede. Al igual que el cuerpo de una expresión lambda, o de una función, podemos invocar otras.

print 'Son Pares? :', [esPar(n) for n in numeros]

En este caso esPar() es una función que teníamos definida para otro ejemplo.

También obviamente podemos acceder a miembros de un "struct" (en realidad mensajes a un objeto).

productos = [Producto("aceite", 6.8), Producto("pan", 2.5),  Producto("agua", 6.4)]
print 'Precios:', [ p.precio for p in productos ]

Empaquetado / Desempaquetado de Tuplas

Otra cosa interesante de la sintaxis de las listas por comprensión es que permiten escribir en forma más concisa expresiones de transformación/generación de tuplas. Recuerd que la tuplas son estructuras similares a las listas, pero inmutables.

Veamos un ejemplo. Dada una lista de puntos en el plano con coordenadas x e y expresadas como duplas (x,y), queremos obtener una lista que represente las distancias de esos puntos al origen.

def distancia(x, y): return sqrt(x**2 + y**2)
   
puntosComoDuplas = [ (2, 6), (-1, -1), (9, 27), (3, -3) ]
distanciasAlOrigen = [distancia(x,y) for x, y in puntosComoDuplas]
print "Distancias al origen:", distanciasAlOrigen

Lo que vemos acá es que el for de la lista por comprensión puede desempaquetar el elemento, que en realidad era uno, una Tupla, y dividirle sus dos elementos en dos variables que nosotros definimos.

Esto nos evita haber hecho lo siguiente:

distanciasAlOrigen = [distancia(dupla[0],dupla[1]) for dupla in puntosComoDuplas]

El otro caso sería el inverso, donde queremos generar una lista de tuplas. Ejemplo: dada una lista de palabas, queremos obtener otra lista cuyos elementos sean tuplas de 3 elementos: el string original, el string en mayúsculas y el string en minúsculas.

strings = [ "Hola", "mundo", "Juan Perez", "CHAU"]
stringsTransformados = [ (s, s.upper(), s.lower()) for s in strings ]
print 'Strings:', stringsTransformados

En este caso, sí tenemos que definir la tupla entre paréntesis.

Filtrado por comprensión

Ok, hasta ahora lo que hicimos, se habrán dado cuenta es lo mismo que hacíamos con la función map, es decir generar una nueva lista, aplicando una transformación. Ahora, cómo hacemos lo que antes hacíamos con el filter? Es decir, genera una lista, que tendrá solo aquellos elementos de la original que cumplan con una condición ?

Ejemplo: dada una lista de números, queremos la lista de aquellos que son mayores a 10

[n for n in numeros if n > 10 ]

Ojo de nuevo ! Utilizamos la palabra reservada if pero no es el mismo if que usábamos antes ! Fíjense que no tiene (ni puede tener) código dentro de la parte positiva (no hay) ni tampoco hay parte negativa (else). Esto es porque en realidad se usa la palabra if porque en inglés significa "sí ...." Podemos leerlo nosotros como "si se cumple que... condición".

Sintaxis extendida

Podemos ahora redefinir la sintaxis con esto

[ definiciónDeUnElementoDeLaListaResultado<variable> for variable in otraLista if condicion<variable> ]

La condición puede ser tan complicada como nosotros querramos. Y obviamente también podemos ahí escribir el llamado a una función. De hecho ambas cosas están relacionadas. Si tengo que escribir una condición muy "grande", seguramente me convenga escribir una función y acá desde el if, solo llamarla.

Ejemplo Combinatorio

Dada una lista de productos, queremos obtener tuplas con su nombre y su precio, para los que son "caros" (su precio es mayor a 6)

productos = [Producto("aceite", 6.8), Producto("pan", 2.5),  Producto("agua", 6.4)]
print 'Productos caros:', [ (p.nombre, p.precio) for p in productos if p.precio > 6 ]
>>>Productos caros: [('aceite', 6.8), ('agua', 6.4)]

Fíjense que acá en forma mucho más concisa y clara estamos haciendo dos operaciones al mismo tiempo. Por un lado filtramos la lista original, y por otro lado, transformamos cada elemento hacia la lista resultado.

Es decir que estamos haciendo un filter -> map !

Otros Ejemplos (range, enumerate)

Es bueno recordar que en python hay muchas funciones que generan listas, no necesariamente yo voy a utilizar listas por comprensión contra listas "hardcodeadas" nuestras. Un ejemplo de esto sería utiliza la funcion range:

print 'Cuadrados (range):', [x * x for x in range(10)]
>>> Cuadrados (range): [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

También podemos recordar que el enumerate también genera una lista con tuplas (indice, elemento). Entonces si para nuestro problema necesitamos generar la lista utilizando el índice podemos usar enumerate.

lista = [1, 2, 3, 4, 5, 6]
print 'Por Su Indice:', [i * x for i, x in enumerate(lista)]

Listas por comprensión anidadas

Esto de las listas por comprensión es aún más poderos que lo que vimos hasta ahora. Puedo encadenar expresiones for dentro de la definición de la lista. En general esto se usa cuando tenemos que trabajar con dos listas, derivando una, por ejemplo, para sacar el producto cartesiano:

unaLista = [10,20,30,40,50]
otraLista = [1,2,3,4,5]
print [(x,y) for x in unaLista for y in otraLista]

Si observan la definición de la lista por comprensión es muy, pero muy parecida a la explicación de qué es el producto cartesiano: "...otro conjunto cuyos elementos son todos los pares ordenados que pueden formarse tomando el primer elemento del par del primer conjunto, y el segundo elemento del segundo conjunto..." De Wikipedia

Sin embargo, los for's no tiene por qué ser sobre listas complétamente distintas, podemos declarar el segundo una como una lista que se obtiene a partir del elemento del primer for.

Acá otro ejemplo bastante arbitrario:

Dada una lista de números, obtener otra lista que por cada elemento de la original, va a tenerlo a él mismo, más todos los números entre él, y su doble.

Ejemplos de uso:

    • [2] -> [2,3,4]
    • [2, 8] -> [2,3,4,8,9,10,11,12,13,14,15,16]
    • [1] -> [1]
    • [2,4] -> [2,3,4,4,5,6,7,8]

La implementación sería tan simple como:

numeros = [2, 5, 11]
print [x for n in numeros for x in range(n, (n*2) + 1)]

Diccionarios por comprensión

Ya vimos Diccionarios en ésta otra página. Lo que vemos acá es que esta idea de conjuntos por comprensión, también se puede utilizar para generar diccionarios, en lugar de listas.

Solo cambia un poquito la sintaxis. En lugar de corchetes delimitamos por llaves que es la forma de escribir un diccionario. Y claro, que la expresión que define a cada elemento ahora deberá tener dos partes, la clave y el valor:

{ expresionGeneraClave : expresionGeneraValor for variable in lista if condicion }

Veamos un ejemplo:

Dada una lista de Productos queremos generar un diccionario con sus nombres y precios.

productos = [Producto("aceite", 6.8), Producto("pan", 2.5),  Producto("agua", 6.4)]
precios = { p.nombre : p.precio for p in productos }
print "Precios", precios

Como ven la mecánica es exáctamente la misma. Y mismo el poder! :)

Generadores

// TODO