Apéndice S — Iterables, Mapeo, Filtros y Reducciones.

Objetivo. Revisar elementos avanzados de Python para crear códigos más concisos.

S.1 Iterables

  • En Python existen objetos que contienen secuencias de otros objetos.
  • Estos objetos se pueden recorrer usando ciclos for ... in ....
  • A estos objetos se les conoce también como iterables (objetos iterables, secuencias iterables, contenedores iterables, conjunto iterable, entre otros).
Ejemplo S.1:

Crear una cadena, una lista, una tupla, un diccionario, un conjunto y leer un archivo; posteriormente recorrer cada uno de estos iterables usando un ciclo for:

mi_cadena = "pythonico"
mi_lista = ['p','y','t','h','o','n','i','c','o']
mi_tupla = ('p','y','t','h','o','n','i','c','o')
mi_dict = {'p':1,'y':2,'t':3,'h':4,'o':5,'n':6,'i':7,'c':8,'o':9}
mi_conj = {'p','y','t','h','o','n','i','c','o'}
mi_archivo = open("./gatos.txt")

print('\nCadena:', end=' ')
# Recorremos la cadena e imprimimos cada elemento 
for char in mi_cadena:
    print(char, end=' ')

print('\nLista:', end=' ')
# Recorremos la lista e imprimimos cada elemento 
for element in mi_lista:
    print(element, end=' ')

print("\nTupla: ", end='')
# Recorremos la tupla e imprimimos cada elemento 
for element in mi_tupla:
    print(element, end=' ')

print("\nDiccionario  (claves): ", end='') 
# Recorremos el diccionario e imprimimos cada clave 
for key in mi_dict.keys():
    print(key, end=' ')

print("\nDiccionario (valores): ", end='') 
# Recorremos el diccionario e imprimimos cada valor 
for key in mi_dict.values():
    print(key, end=' ')

print("\nConjunto: ", end='') 
# Recorremos el conjunt e imprimimos cada elemento 
for s in mi_conj:
    print(s, end = ' ')
    
print("\nArchivo: ") 
# Recorremos el archivo e imprimimos cada elemento 
for line in mi_archivo:
    print(line, end = '')

Cadena: p y t h o n i c o 
Lista: p y t h o n i c o 
Tupla: p y t h o n i c o 
Diccionario  (claves): p y t h o n i c 
Diccionario (valores): 1 2 3 4 9 6 7 8 
Conjunto: i t y h p o n c 
Archivo: 
     Persa       2.3
    Sphynx       3.5
   Ragdoll       5.4
    Siamés       2.5

Observaciones.

En los casos del diccionario y del conjunto sucede:

  • Diccionario: cuando hay claves repetidas, se sustituye el último valor que toma la clave ('0':9).
  • Conjunto: los elementos se ordenan y no se admiten elementos repetidos.

S.2 Mapeo.

En análisis matemático, un Mapeo es una regla que asigna a cada elemento de un primer conjunto, un único elemento de un segundo conjunto, simbólicamente se representa como sigue:

\[ \texttt{map} \] \[ \left[ \begin{matrix} s_1 \\ s_2 \\ \vdots \\ s_{n-1} \end{matrix} \right] \begin{matrix} \longrightarrow \\ \longrightarrow \\ \vdots \\ \longrightarrow \end{matrix} \left[ \begin{matrix} t_1 \\ t_2 \\ \vdots \\ t_{n-1} \end{matrix} \right] \]

En Python existe la función para realizar este tipo de mapeo que se llama map(function, sequence) cuyo primer parámetro es una función la cual se va a aplicar a una secuencia, que es el segundo parámetro. El resultado será una nueva secuencia con los elementos obtenidos de aplicar la función a cada elemento de la secuencia de entrada.

Ejemplo S.2:

Crear el siguiente mapeo con una lista, una tupla y un conjunto \[ f(x) = x^2 \] \[ \left[ \begin{matrix} 0 \\ 1 \\ 2 \\ 3 \\ 4 \end{matrix} \right] \begin{matrix} \longrightarrow \\ \longrightarrow \\ \longrightarrow \\ \longrightarrow \\ \longrightarrow \end{matrix} \left[ \begin{matrix} 0 \\ 1 \\ 4 \\ 9 \\ 16 \end{matrix} \right] \]

# Primero definimos la función
def square(x):
    """
    Calcula el cuadrado de x.
    """
    return x**2

# Luego definimos las secuencias
l = [0,1,2,3,4]
t = (0,1,2,3,4)
s = {0,1,2,3,4}

# Ahora creamos los mapeos
lmap = map(square, l)
tmap = map(square, t)
smap = map(square, s)

# Checamos el tipo de cada mapeo
print(type(lmap), type(tmap), type(smap))

print('Lista {}'.format(l))
print('Mapeo {}\n'.format(list(lmap))) # Convertimos el mapeo a lista
                                       # para que se pueda imprimir

print('Tupla {}'.format(t))
print('Mapeo {}\n'.format(tuple(tmap)))

print('Conj {}'.format(s))
print('Mapeo {}\n'.format(set(smap)))
<class 'map'> <class 'map'> <class 'map'>
Lista [0, 1, 2, 3, 4]
Mapeo [0, 1, 4, 9, 16]

Tupla (0, 1, 2, 3, 4)
Mapeo (0, 1, 4, 9, 16)

Conj {0, 1, 2, 3, 4}
Mapeo {0, 1, 4, 9, 16}

Observa que el resultado del mapeo es un objeto de tipo <class 'map'> por lo que debemos convertirlo en un tipo que pueda ser desplegado para imprimir. Lo anterior se puede realizar también con una función anónima lambda. Por ejemplo, para la lista haríamos lo siguiente:

print("Lista original", l)
lista_resultado = list(map(lambda x: x**2, l))
print("Resultado", lista_resultado)
Lista original [0, 1, 2, 3, 4]
Resultado [0, 1, 4, 9, 16]

.

Ejemplo S.3:

Crear un mapeo para convertir grados Fahrenheit a Celsius y viceversa:

Primer definimos las funciones de conversión:

def toFahrenheit(T):
    """
    Transforma los elementos de T en grados Farenheit.
    """
    return (9/5)*T + 32

def toCelsius(T):
    """
    Transforma los elementos de T en grados Celsius.
    """
    return (5/9)*(T-32)

Celsius \(\to\) Fahrenheit

# Lista original con los datos
c = [0, 22.5, 40, 100]

# Construimos el mapeo y lo nombramos `fmap`.
fmap = map(toFahrenheit, c)
# Imprimimos a lista original y el mapeo
print("Grados centígrados (original):", c)
print("Grados Farenheit (conversión):", list(fmap))
Grados centígrados (original): [0, 22.5, 40, 100]
Grados Farenheit (conversión): [32.0, 72.5, 104.0, 212.0]
NotaNota.

Solo se puede usar el mapeo una vez, si vuelves a ejecutar la celda anterior el resultado del mapeo estará vacío. Para volverlo a generar debes ejecutar la celda donde se construye el mapeo.

Lo anterior se puede realizar en una sola línea: crear el mapeo, convertir a lista e imprimir.

print("Grados centígrados (original):", c)

# Conversión en una sola línea:
print("Grados Farenheit (conversión):", list(map(toFahrenheit,c)))
Grados centígrados (original): [0, 22.5, 40, 100]
Grados Farenheit (conversión): [32.0, 72.5, 104.0, 212.0]

También es posible utilizar una función lambda:

print("Grados centígrados (original):", c)
# Conversión en una sola línea:
print("Grados Farenheit (conversión):", list(map(lambda T: (9/5)*T + 32, c)))
Grados centígrados (original): [0, 22.5, 40, 100]
Grados Farenheit (conversión): [32.0, 72.5, 104.0, 212.0]

Fahrenheit \(\to\) Celsius

# Lista original con grados Farenheit
f = [32.0, 72.5, 104.0, 212.0]

print("Grados Farenheit (original):", f)
print("Grados centígrados (conversión):", list(map(toCelsius, f)))
Grados Farenheit (original): [32.0, 72.5, 104.0, 212.0]
Grados centígrados (conversión): [0.0, 22.5, 40.0, 100.0]

Ahora usando una función lambda:

print("Grados Farenheit (original):", f)
print("Grados centígrados (conversión):", list(map(lambda T: (5/9)*(T-32), f)))
Grados Farenheit (original): [32.0, 72.5, 104.0, 212.0]
Grados centígrados (conversión): [0.0, 22.5, 40.0, 100.0]

.

Ejemplo S.4:

Crear un mapeo para sumar los elementos de tres listas que contienen números enteros.

  • Primero usando una función definida con def.
  • Segundo usando una función lambda.
NotaNota.

La función map() se puede aplicar a más de un conjunto iterable, siempre y cuando los iterables tengan la misma longitud y la función que se aplique tenga los parámetros correspondientes.

# Función normal
def suma(x,y,z):
    """
    Suma los números x, y, z.
    """
    return x+y+z

# Tres listas con enteros
a = [1,2,3,4]
b = [5,6,7,8]
c = [9,10,11,12]

# Aplicación del mapeo
print(list(map(suma, a,b,c)))
[15, 18, 21, 24]

Usando una función lambda:

# Función lambda
sumaL = lambda x,y,z: x + y + z

# Tres listas con enteros
a = [1,2,3,4]
b = [5,6,7,8]
c = [9,10,11,12]

# Aplicación del mapeo
print(list(map(sumaL, a,b,c)))
[15, 18, 21, 24]

Todo en una sola línea:

# Tres listas con enteros
a = [1,2,3,4]
b = [5,6,7,8]
c = [9,10,11,12]

# Aplicación del mapeo
print(list(map(lambda x,y,z: x + y + z, a,b,c)))
[15, 18, 21, 24]

S.3 Filtrado.

  • Filtrar es un procedimiento para seleccionar elementos de un conjunto o para impedir su paso libremente.

  • En matemáticas, un filtro es un subconjunto especial de un conjunto parcialmente ordenado que simbólicamente se ve como sigue:

\[ \texttt{filter} \] \[ \left[ \begin{matrix} s_1 \\ s_2 \\ s_3 \\ s_4 \\ s_{n-1} \end{matrix} \right] \begin{matrix} \\ \xrightarrow{\texttt{True}} \\ \\ \xrightarrow{\texttt{True}} \\ \xrightarrow{\texttt{True}} \end{matrix} \left[ \begin{matrix} - \\ f_1 \\ - \\ f_2 \\ f_{m-1} \end{matrix} \right] \]

En Python existe la función filter(function, sequence) para realizar un filtrado cuyo primer parámetro es una función la cual se va a aplicar a una secuencia, que es el segundo parámetro. La función debe regresar un objeto de tipo Booleano: True o False. El resultado será una nueva secuencia con los elementos obtenidos de aplicar la función a cada elemento de la secuencia de entrada.

Ejemplo S.5:

Usando la función filter(), encontrar los números pares en una lista.

def esPar(n):
    """
    Función que determina si un número es par o impar.
    """
    if n%2 == 0:
        return True
    else:
        return False
# Probamos la función
print(esPar(10))
print(esPar(9))
True
False
# Creamos una lista de números, del 0 al 19
numeros = list(range(20))
print(numeros)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
# Aplicamos el filtro
filtro = filter(esPar, numeros)
print(filtro, type(filtro))
print(list(filtro))
<filter object at 0x0000012DE0773FA0> <class 'filter'>
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# En una sola línea:
print(list(filter(esPar, numeros)))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Usando una función anónima y el operador ternario:

print(list(filter(lambda n: False if n%2 else True, numeros)))

.

Ejemplo S.6:

Encontrar los números pares en una lista que contiene elementos de muchos tipos.

Paso 1. Creamos la lista.

# Creamos la lista
lista = ['Hola', 4, 3.1416, 3, 8, ('a',2), 10, {'x':1.5, 'y':12} ]
print(lista)
['Hola', 4, 3.1416, 3, 8, ('a', 2), 10, {'x': 1.5, 'y': 12}]

Paso 2. Escribimos una función que verifique si una entrada es de tipo int.

def esEntero(i):
    """
    Función que determina si un número es entero.
    """
    if isinstance(i, int): # Checamos si la entrada es de tipo int
        return True
    else:
        return False
print(esEntero("Hola"))
print(esEntero(1))
print(esEntero(1.))
False
True
False

Una forma alternativa, más Pythonica, de construir la función esEntero() es la siguiente:

def esEntero(i):
    return True if isinstance(i, int) else False

Paso 3. Probamos la función esEntero() para filtrar solo los número enteros de lista.

list(filter(esEntero,lista))
[4, 3, 8, 10]

Paso 4. Usamos la función esPar() para encontrar los pares de la lista de enteros.

lista_enteros = list(filter(esEntero, lista))

print(list(filter(esPar, lista_enteros)))
[4, 8, 10]
# Todo en una sola línea
print(list(filter(esPar, list(filter(esEntero,lista)))))
[4, 8, 10]

Observa que se aplicó dos veces la función filter(), la primera para determinar si el elemento de la lista es entero usando la función esEntero(), la segunda para determinar si el número es par.

Se pueden usar funcione anónimas y tener el código completo en una sola línea:

print(list(filter(lambda n: False if n%2 else True, list(filter(lambda i: True if isinstance(i, int) else False,lista)))))
[4, 8, 10]

Sin embargo, la solución anterior es un poco complicada de leer y entender.

.

Ejemplo S.7:

Encontrar los números primos en el conjunto \(\{2, \dots, 50\}\).

def noPrimo():
    """
    Determina la lista de números que no son primos en el 
    rango [2, 50]
    """
    np_list = []
    for i in range(2,8):
        for j in range(i*2, 50, i):
            np_list.append(j)
    return np_list

lista_no_primo = noPrimo()

print("Números NO primos en el rango [2, 50] \n{}".format(lista_no_primo))
Números NO primos en el rango [2, 50] 
[4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 10, 15, 20, 25, 30, 35, 40, 45, 12, 18, 24, 30, 36, 42, 48, 14, 21, 28, 35, 42, 49]
def esPrimo(number):
    """
    Determina si un número es primo o no.
    """
    np_list = noPrimo()
    
    if(number not in np_list):
        return True
    else:
        return False

# Creación de la lista de números enteros de 2 a 50
numeros = list(range(2,50))

# Calculamos los primos usando filter(), con 
# la función esPrimo() y la lista números.
lista_primos = list(filter(esPrimo, numeros))

print("\nNúmeros primos en el rango [2, 50] \n {}".format(lista_primos))

Números primos en el rango [2, 50] 
 [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

¿Podrías sustituir la declaración if ... else ... en la función esPrimo() usando el operador ternario?

S.4 Reducción.

  • Reducción : Disminuir algo en tamaño, cantidad, grado, importancia, ..

  • La operación de reducción es útil en muchos ámbitos, el objetivo es reducir un conjunto de objetos en un objeto más simple.

Una reducción se hace como sigue:

Dada la función \(f()\) y la secuencia \([s_1, s_2, s_3, s_4]\) se tiene que

\[ [\underbrace{\underbrace{\underbrace{s_1, s_2}_{a = f(s_1,s_2)}, s_3}_{b = f(a,s_3)}, s_4}_{c = f(b,s_4)}] \qquad \Longrightarrow \qquad \underbrace{f(\underbrace{f(\underbrace{f(s_1,s_2)}_{a}, s_3)}_{b}, s_4)}_{c} \]

En Python existe la función reduce(function, sequence) cuyo primer parámetro es una función la cual se va a aplicar a una secuencia, que es el segundo parámetro. La función debe regresar un objeto que es el resultado de la reducción.

Ejemplo S.8:

Calcular la siguiente serie:

\(1 + 2 + \dots + n = \sum\limits_{i=1}^{n} i = \dfrac{n(n+1)}{2}\)

Si \(n = 4\) entonces 1+2+3+4 = 10

# La función reduce() debe importarse del módulo functools
from functools import reduce 
# Creamos la lista
nums = [1,2,3,4]
print(nums)

# Calculamos la serie usando reduce y una función lambda
suma = reduce(lambda x, y: x + y, nums)
print(suma)
[1, 2, 3, 4]
10
# Se pueden usar arreglos de numpy
import numpy as np

# Construimos un arreglo de 1's
a = np.ones(20)
print(a)

suma = reduce(lambda x, y: x + y, a)
print(suma)
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
20.0

.

Ejemplo S.9:

Calcular la siguiente serie:

$1 + + + = _{i=1}^{n} = $

numeros = [1,2,3,4,5,6,7,8,9,10]
result = reduce(lambda x, y: 1/x + 1/y, numeros)
print(result)
1.1

.

Ejemplo S.10:

Calcular el máximo de una lista de números.

numeros = [23,5,23,56,87,98,23]
maximo = reduce(lambda a,b: a if (a > b) else b, numeros)
print(maximo)
98

S.5 Ejemplos adicionales.

Ejemplo S.11: Recursión.

Calcular el factorial de un número.

El factorial de un número \(n\) se define como el producto de todos los números enteros positivos desde n hasta 1. Matemáticamente, se puede expresar como:

\[ n! = n \times (n−1)! \]

Observa que para calcular el factorial de \(n\), primero debes calcular el factorial de \(n-1\). El factorial de \(0! = 1\).

  • La recursión es un concepto en programación donde una función se llama a sí misma para resolver un problema.

  • Una función recursiva divide un problema en subproblemas más pequeños y sencillos, resolviéndolos de manera repetida hasta llegar a una condición base: un caso simple que la función puede resolver sin más llamadas recursivas.

  • La clave en la recursión es que siempre se requiere de lo siguiente:

    • Un caso base: Es la condición que detiene la recursión y evita que la función se llame indefinidamente.
    • Una llamada recursiva: La función se llama a sí misma con un problema más pequeño o una versión simplificada del problema original.

En el caso del factorial de un número \(n\), el caso base es: \(0! = 1\).

def factorial1(n):
    """
    Función recursiva que calcula el factorial de n.
    """
    if n == 0:    # Caso base
        return 1  
    return n * factorial1(n-1) # Se llama a factorial() nuevamente
factorial1(5)
120

Usando reduce y una función lambda es posible calcular el factorial:

reduce(lambda x, y: x * y, range(1,6))
120

Podemos entonces modificar la función factorial combinándola con reduce como sigue:

def factorial2(n):
    """
    Función recursiva que calcula el factorial de n.
    """
    if n == 0:    # Caso base
        return 1   
    return reduce(lambda x, y: x * y, range(1, n + 1))
factorial2(5)
120

Si usamos ahora el operador ternario, podemos escribir la función factorial aún más compacta:

def factorial3(n):
    return 1 if n == 0 else reduce(lambda x, y: x * y, range(1, n + 1))
factorial3(5)
120

Consideraciones sobre Recursión:

  • La recursión puede ser elegante y fácil de entender para problemas que se pueden dividir de manera similar (como el factorial).

  • Sin embargo, no siempre es la solución más eficiente, ya que una recursión profunda consume más memoria por las llamadas acumuladas en la pila de ejecución. Esto puede llevar a un desbordamiento de la memoria si no se maneja adecuadamente o si no hay un caso base claro.

Ejemplo S.12:

Contar el número de caracteres de un texto sin tomar en cuenta los espacios en blanco, combinando reduce(), map( ) y lambda.

Paso 1. Definimos un texto.

texto = 'Hola Mundo Pythónico!'

Contar los caracteres, sin tomar en cuenta los espacios en blanco, es fácil con len() y str.count():

print(len(texto))
print(texto.count(" "))
21
2
print("Número de caracteres: {}".format(len(texto)-texto.count(" ")))
Número de caracteres: 19

Con este valor podemos comprobar si hicimos correctamente el ejercicio.

Paso 2. Separamos el texto por palabras.

palabras = texto.split()
print(palabras)
['Hola', 'Mundo', 'Pythónico!']

Paso 3. Creamos una función lambda para contar los caracteres de una palabra.

num_car_pal = lambda p: len(p)
num_car_pal(palabras[0])
4

Paso 4. Usando un mapeo y la función lambda, podemos contar los caracteres de cada palabra:

num_caracteres = list(map(num_car_pal, palabras))
print(num_caracteres)
[4, 5, 10]

Paso 5. Con la lista que contiene el número de caracteres podemos obtener el total usando reduce:

reduce(lambda x,y: x+y, num_caracteres)
19

Paso 6. Todo en una línea:

print(reduce(lambda x,y: x+y, list(map(lambda p: len(p), texto.split()))))
19

Paso 7. Generalización.

Contar los caracteres de un texto de un archivo, sin tomar en cuenta los espacios.

archivo = open('./QueLesQuedaALosJovenes.txt','r')

suma = 0
for linea in archivo:
    palabras = linea.split()
    suma += reduce(lambda x,y: x+y, list(map(lambda p: len(p), palabras)))
print(suma)
archivo.close()
854

Incluso, podemos construir una función:

def countChar(palabras):
    return reduce(lambda x,y: x+y, list(map(lambda p: len(p), palabras)))
# Utilizamos un gestor de contexto
with open('./QueLesQuedaALosJovenes.txt','r') as archivo:
    suma = 0
    for linea in archivo:
        palabras = linea.split()
        suma += countChar(palabras)
    print(suma)
854

.

Ejemplo S.13:

Encontrar todos los números pares de una lista combinando filter() con lambda.

# Lista de números
nums = [0, 2, 5, 8, 10, 23, 31, 35, 36, 47, 50, 77, 93]

# Aplicación de filter y lambda
result = filter(lambda x : x % 2 == 0, nums)

print(list(result))
[0, 2, 8, 10, 36, 50]

.

Ejemplo S.14:

Encontrar todos los números primos en el conjunto \(\{2, \dots, 50\}\) combinando combinando filter() con lambda.

# Lista de números de 2 a 50
nums = list(range(2, 51)) 

# Cálculo de los números primos usando filter y lambda
for i in range(2, 8):
    nums = list(filter(lambda x: x == i or x % i, nums))

print(nums)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

.

Ejemplo S.15:

La siguiente función es impura porque modifica la lista:

def cuadradosImpuros(lista):
    for i, v in enumerate(lista):
        lista[i] = v ** 2
    return lista

numeros_originales = list(range(5))
print(numeros_originales)
print(cuadradosImpuros(numeros_originales))
print(numeros_originales)

La salida del código anterior es el siguiente:

[0, 1, 2, 3, 4]
[0, 1, 4, 9, 16]
[0, 1, 4, 9, 16]

Escribe una versión pura de la función cuadradosImpuros(lista) usando map() y lambda.

numeros_originales = list(range(5))
print(numeros_originales)
print(list(map(lambda x: x ** 2, numeros_originales)))
print(numeros_originales)
[0, 1, 2, 3, 4]
[0, 1, 4, 9, 16]
[0, 1, 2, 3, 4]

.

Ejemplo S.16:

Convertir el siguiente diccionario que contiene valores de grados Fahrenheit {'t1':32.0, 't2':72.5, 't3':104.0, 't4':212.0} a otro diccionario que contenga los correspondientes valores en grados Celsius.

# Diccionario con grados Farenheit
fahrenheit_dict = {'t1':32.0, 't2':72.5, 't3':104.0, 't4':212.0}

# Usando map, lambda y diccionarios
celsius = list(map(lambda f: (5/9)*(f-32), fahrenheit_dict.values()))
celsius_dict = dict(zip(fahrenheit_dict.keys(), celsius))

print(fahrenheit_dict)
print(celsius_dict)
{'t1': 32.0, 't2': 72.5, 't3': 104.0, 't4': 212.0}
{'t1': 0.0, 't2': 22.5, 't3': 40.0, 't4': 100.0}