List y Dict Comprehensions#

Es difícil explicar en abstracto que son las List y Dict comprehensions. Por ahora sólo vamos a decir que son una manera muy potente y rápida de generar List y Dict a partir de otros List y Dict. Veamos un par de ejemplos.

List Comprehensions#

Construir una List a partir de otra List u otra estrucutura de datos.

Ejemplo: Transformar los Elementos de una List#

Supongamos que tenemos una lista de RUTs. Como es típico, los RUTs vienen con formatos inconsistentes, supongamos que pueden venir con o sin separador de miles y con o sin guión antes del dígito verificador. Por ejemplo:

  • 12.345.678-9

  • 21543879-9

  • 214537689

Obviamente, antes de utilizar esta lista, queremos homologar los formatos. Para homologar un RUT al formato sin separador de miles y con guión, escribimos la siguiente función:

def estandariza_rut(rut):
    """
    Estandariza un RUT al siguiente formato XXXXXXXX-DV.
    
    Parameters
    ----------
    
    rut: str o int
        Representa un RUT, puede venir con o sin separador de miles, con o sin guión antes del 
        dígito verificador y podría ser un `int` o un `str`.
        
    Returns
    -------
    
    El RUT en el formato estandarizado como un `str`.
    """
    # Antes de comenzar la transformación nos aseguramos que el parámetro rut sea un str.
    temp = str(rut)
    
    # Se eliminan eventuales separadores de miles.
    temp = temp.replace(".", "")
    temp = temp.replace(",", "")
    
    # Se elimina eventual dígito verificador.
    temp = temp.replace("-", "")
    
    # Se agrega el dígito verificador y se retorna.
    return f'{temp[:-1]}-{temp[-1]}' # slicing

Probemos la función:

ruts = ['12.345.678-9', '21543879-9', 214537689]
for rut in ruts:
    print(estandariza_rut(rut))
12345678-9
21543879-9
21453768-9
ruts_ok = []
for rut in ruts:
    ruts_ok.append(estandariza_rut(rut))
ruts_ok
['12345678-9', '21543879-9', '21453768-9']

Aplicamos ahora un List comprehension para transformar la List ruts en una List con RUTs estandarizados.

ruts_ok = [estandariza_rut(rut) for rut in ruts]
ruts_ok
['12345678-9', '21543879-9', '21453768-9']

La mejor manera de pensar y entender esta sintaxis es recordando la notación matemática (del colegio nada complicado) para denotar o definir un conjunto. En este caso el conjunto \(Y\) formado por todos los valores transformados por la función \(f\) de los elementos del conjunto \(X\).

\[Y=\{ f(x):x\in X \}\]

Ejercicio: Capitalizar una Lista de Nombres#

Considerar esta List de nombres: nombres = ['maría', 'Rosa', 'josé', 'horacio', 'Anacleta'].

Transformar nombres en: ['María', 'Rosa', 'José', 'Horacio', 'Anacleta'].

Tip: ir a Google y buscar capitalize string in python.

Solución:

Hide code cell content
# Usando List comprehension. Más elegante y más rápido.
nombres = ['maría', 'Rosa', 'josé', 'horacio', 'Anacleta']
resultado = [x.capitalize() for x in nombres]
print(resultado)

# Forma fea
resultado1 = []
for x in nombres:
    resultado1.append(x.capitalize())
print(resultado1)
['María', 'Rosa', 'José', 'Horacio', 'Anacleta']
['María', 'Rosa', 'José', 'Horacio', 'Anacleta']

Ejemplo: Filtrar los Elementos de una List#

Tenemos ahora una List de Tuple donde cada Tuple tiene el nombre de un producto comestible y un boolque indica si el producto tiene o no sellos (si es True entonces tiene sellos).

productos = [
    ('Super8', True),
    ('Apio', False),
    ('Zucaritas', True),
    ('Té verde', False)
]

Vamos a filtrar los productos sin sellos y almacenarlos en una nueva List.

productos_con_sellos = [p for p in productos if p[1]]

La expresión if p[1] es lo mismo que escribir if p[1] == True, pero es más elegante y conciso. Veamos qué obtuvimos.

productos_con_sellos
[('Super8', True), ('Zucaritas', True)]

También usando la notación matemática para conjuntos, esta sintaxis se puede pensar como:

\[Y=\{(x_0, x_1): (x_0, x_1) \in X \land x_1 = True \}\]

Aquí, \(\land\) es el símbolo matemático para la condición lógica and.

Ejercicio: Filtrar Números#

Considerando la siguiente List rand_nums de números enteros generados aleatoriamente usando una List comprehension:

  • filtrar todos los elementos superiores a 50

  • generar la List con las raíces cuadradas de los elementos de rand_nums.

import random as rnd

# En esta librería está la función sqrt para calcular raíces cuadradas
import math 

rand_nums = [rnd.randint(1, 101) for i in range(100)] # Es primera vez que usamos range

Solución:

Hide code cell content
gt_50 = [number for number in rand_nums if number > 50]
sqr = [math.sqrt(number) for number in rand_nums]

print(rand_nums)
print()
print(gt_50)
print()
print(sqr)
[96, 33, 51, 15, 27, 24, 97, 53, 48, 10, 15, 93, 72, 36, 3, 17, 78, 1, 29, 21, 17, 20, 89, 49, 42, 96, 28, 38, 95, 63, 39, 46, 28, 14, 75, 50, 57, 37, 51, 57, 6, 41, 96, 11, 42, 94, 79, 64, 65, 61, 24, 30, 71, 69, 68, 11, 38, 94, 48, 91, 50, 36, 98, 69, 89, 69, 24, 42, 48, 36, 11, 20, 36, 100, 54, 79, 76, 90, 90, 57, 22, 90, 6, 42, 1, 97, 78, 14, 1, 47, 70, 4, 93, 74, 52, 70, 6, 2, 88, 85]

[96, 51, 97, 53, 93, 72, 78, 89, 96, 95, 63, 75, 57, 51, 57, 96, 94, 79, 64, 65, 61, 71, 69, 68, 94, 91, 98, 69, 89, 69, 100, 54, 79, 76, 90, 90, 57, 90, 97, 78, 70, 93, 74, 52, 70, 88, 85]

[9.797958971132712, 5.744562646538029, 7.14142842854285, 3.872983346207417, 5.196152422706632, 4.898979485566356, 9.848857801796104, 7.280109889280518, 6.928203230275509, 3.1622776601683795, 3.872983346207417, 9.643650760992955, 8.48528137423857, 6.0, 1.7320508075688772, 4.123105625617661, 8.831760866327848, 1.0, 5.385164807134504, 4.58257569495584, 4.123105625617661, 4.47213595499958, 9.433981132056603, 7.0, 6.48074069840786, 9.797958971132712, 5.291502622129181, 6.164414002968976, 9.746794344808963, 7.937253933193772, 6.244997998398398, 6.782329983125268, 5.291502622129181, 3.7416573867739413, 8.660254037844387, 7.0710678118654755, 7.54983443527075, 6.082762530298219, 7.14142842854285, 7.54983443527075, 2.449489742783178, 6.4031242374328485, 9.797958971132712, 3.3166247903554, 6.48074069840786, 9.695359714832659, 8.888194417315589, 8.0, 8.06225774829855, 7.810249675906654, 4.898979485566356, 5.477225575051661, 8.426149773176359, 8.306623862918075, 8.246211251235321, 3.3166247903554, 6.164414002968976, 9.695359714832659, 6.928203230275509, 9.539392014169456, 7.0710678118654755, 6.0, 9.899494936611665, 8.306623862918075, 9.433981132056603, 8.306623862918075, 4.898979485566356, 6.48074069840786, 6.928203230275509, 6.0, 3.3166247903554, 4.47213595499958, 6.0, 10.0, 7.3484692283495345, 8.888194417315589, 8.717797887081348, 9.486832980505138, 9.486832980505138, 7.54983443527075, 4.69041575982343, 9.486832980505138, 2.449489742783178, 6.48074069840786, 1.0, 9.848857801796104, 8.831760866327848, 3.7416573867739413, 1.0, 6.855654600401044, 8.366600265340756, 2.0, 9.643650760992955, 8.602325267042627, 7.211102550927978, 8.366600265340756, 2.449489742783178, 1.4142135623730951, 9.38083151964686, 9.219544457292887]
Ejemplos de range#

Números tales que: \(0 \leq n \lt 5\)

for i in range(5):
    print(i)
0
1
2
3
4

Números tales que: \(-1\leq n \lt 20\) contando de 2 en 2.

for i in range(-1, 20, 2):
    print(i)
-1
1
3
5
7
9
11
13
15
17
19

Documentación de range

print(range.__doc__) # dunder doc
range(stop) -> range object
range(start, stop[, step]) -> range object

Return an object that produces a sequence of integers from start (inclusive)
to stop (exclusive) by step.  range(i, j) produces i, i+1, i+2, ..., j-1.
start defaults to 0, and stop is omitted!  range(4) produces 0, 1, 2, 3.
These are exactly the valid indices for a list of 4 elements.
When step is given, it specifies the increment (or decrement).
datos = ['a', 'b', 'c', 'd', 'e']
for i in range(len(datos)):
    print(datos[i])
a
b
c
d
e

Aunque la manera pythonica de hacer lo anterior es:

for d in datos:
    print(d)
a
b
c
d
e

Dict Comprehensions#

Construir un Dict a partir de otro Dict, una List u otra estrucutura de datos.

Reorganizar una List#

Consideremos la siguiente List de Tuples. Cada Tuple contiene el nombre, edad (años), peso (kilos) y estatura (cm.) de un paciente. Data con esta estructura es la que usualmente se obtiene de la consulta a una base de datos. Sin embargo, si queremos rápidamente acceder a las cifras de un paciente en particular, tener la data almacenada de esta forma, no es lo más conveniente. Si vamos a buscar por nombre, lo más conveniente es usar un Dict cuyos keys sea el nombre del paciente y cuyos values sea la data del paciente.

data = [
    ('Pedro', 25, 70, 170),
    ('Juan', 43, 67, 165),
    ('Diego', 18, 90, 180),
    ('María', 50, 55, 160),
]
data[0][1:]
(25, 70, 170)
data_dict = {d[0]: d[1:] for d in data}
data_dict
{'Pedro': (25, 70, 170),
 'Juan': (43, 67, 165),
 'Diego': (18, 90, 180),
 'María': (50, 55, 160)}

Ahora, si queremos acceder a los datos de María sólo tenemos que:

data_dict['María']
(50, 55, 160)

Asignar Nombres a los Datos Numéricos#

La estructura anterior es sin duda una mejora. Sin embargo, podríamos confundirnos entre la edad y el peso de un paciente. Por ejemplo, María tiene 50 años y pesa 55 kilos. Para que no exista esa confusión, también la data se almacenará en un Dict.

data_dict_2 = {d[0]: {'edad': d[1], 'peso': d[2], 'estatura': d[3]} for d in data}
data_dict_2
{'Pedro': {'edad': 25, 'peso': 70, 'estatura': 170},
 'Juan': {'edad': 43, 'peso': 67, 'estatura': 165},
 'Diego': {'edad': 18, 'peso': 90, 'estatura': 180},
 'María': {'edad': 50, 'peso': 55, 'estatura': 160}}

Ahora, si queremos la edad de María hacemos:

data_dict_2['María']['edad']
50

Y su peso …

data_dict_2['María']['peso']
55