Tiza Paso a paso
Practicar →

Pensamiento Computacional · Unidad 4 — Estructuras de datos

map, filter y lambda

map le aplica una función a cada elemento; filter se queda con los elementos para los que la función da True. Hacen lo mismo que un for con append, en una línea.

Practicar este tema →8 ejercicios que te corrigen paso a paso · gratis, sin cuenta

La explicación

Una función también es un dato

En el tema 4.3 ya apareció una función pasada a otra: sorted(palabras, key=len). Ahí len va sin paréntesis: no se la llama, se la entrega para que sorted la use con cada elemento. map y filter funcionan con la misma idea: reciben una función y una secuencia, y se encargan de recorrerla.

map: transformar cada elemento

def doble(x):
    return x * 2

print(list(map(doble, [3, 5, 8])))
[6, 10, 16]

map(funcion, secuencia) le aplica la función a cada elemento y junta los resultados. La lista nueva tiene siempre la misma cantidad de elementos que la original; lo que cambia es cada valor. La original no se toca.

Para qué sirve el list(...). map no devuelve una lista sino un objeto de tipo map, que va entregando los resultados de a uno cuando se lo recorre. Si se lo imprime directo, se ve algo como <map object at 0x7f...>, que no dice nada. Con list(...) se recorre entero y queda una lista. También se lo puede recorrer con un for, como a cualquier secuencia.

filter: quedarse con algunos

def es_par(x):
    return x % 2 == 0

print(list(filter(es_par, [3, 4, 7, 10])))
[4, 10]

filter(funcion, secuencia) le pregunta a la función por cada elemento y se queda con los que dan True. Los elementos que quedan no cambian: salen tal cual estaban, en el mismo orden. Por eso la función de un filter tiene que contestar una pregunta (devolver un booleano), y la de un map tiene que devolver el valor transformado.

Lo que filter mira en realidad es si el resultado cuenta como verdadero, igual que un if. Si la función devuelve un número en vez de un booleano, el 0 cuenta como falso y cualquier otro número como verdadero. Una función que devuelve x % 2 deja pasar los impares (dan resto 1), justo al revés de lo que sugiere su nombre si se llama es_par. Para no tropezar: que la función de un filter termine siempre en una comparación.

Cómo reconocer cuál usar

¿quiero cambiar cada valor?               → map     (mismo largo)
¿quiero quedarme con algunos?            → filter  (igual o más corta)
¿las dos cosas?                          → primero filter, después map

Se pueden encadenar, porque lo que devuelve uno es una secuencia que el otro puede recorrer:

def positivo(x):
    return x > 0

def cuadrado(x):
    return x * x

print(list(map(cuadrado, filter(positivo, [3, -1, 0, 2]))))
[9, 4]

El orden importa. Al revés, filter(positivo, map(cuadrado, ...)), el −1 primero se eleva al cuadrado, pasa a ser 1, y el filtro lo deja pasar: da [9, 1, 4]. Se filtra mirando el dato original, y recién después se transforma.

Las dos sirven con cualquier secuencia, no sólo con listas: list(map(int, "2026")) recorre el texto letra por letra y da [2, 0, 2, 6], y list(map(int, "3 8 5".split())) convierte de una todos los números de un renglón leído con input().

lambda: una función sin nombre

Escribir un def de dos líneas para usarlo una sola vez es mucho. Para eso está lambda, que arma una función de una sola expresión en el mismo lugar donde se la usa:

lambda x: x * 2           es lo mismo que   def doble(x): return x * 2
precios = [1500, 700, 2300]
print(list(map(lambda p: p + 100, precios)))
print(list(filter(lambda p: p < 2000, precios)))
[1600, 800, 2400]
[1500, 700]

A la izquierda de los dos puntos van los parámetros y a la derecha lo que devuelve; no lleva return. Sirve igual para el key de sorted: sorted(alumnos, key=lambda a: a[1]) ordena una lista de tuplas por su segundo dato. Si la cuenta necesita más de una línea, o se va a usar en varios lugares, conviene un def con nombre.

Tres formas de escribir lo mismo

# con un ciclo
dobles = []
for x in L:
    dobles.append(x * 2)

# con map
dobles = list(map(lambda x: x * 2, L))

# con una lista por comprensión
dobles = [x * 2 for x in L]

Y para filtrar, list(filter(lambda x: x > 4, L)) es lo mismo que [x for x in L if x > 4]. La lista por comprensión (que ya apareció en el tema 4.1) hace map y filter a la vez: lo de la izquierda es la transformación, el if del final es el filtro. Cuando un ejercicio pide «sin usar ciclos» o «usando map y filter», es para practicar esta forma de pensar: describir qué se le hace a cada elemento, sin escribir el recorrido a mano.

El map que se gasta

def cuad(x):
    return x ** 2

c = map(cuad, [1, 2, 3])
print(list(c))
print(list(c))
[1, 4, 9]
[]
Un objeto map o filter se puede recorrer una sola vez: entrega los resultados a medida que se los piden y no los guarda. El primer list(c) lo vacía, y el segundo encuentra que no queda nada. Si se lo va a usar más de una vez, se lo convierte en lista una vez y se guarda esa lista: c = list(map(cuad, [1, 2, 3])).
Un ejercicio resuelto, paso a paso

Dada notas = [3, 7, 10, 5, 9], armar con filter la lista de las notas aprobadas (4 o más) y después, con map, subirle un punto a cada una de ésas, sin pasar de 10.

Dos pasos, uno por herramienta. Primero se decide cuáles quedan (una pregunta: filter) y después se cambia cada una (una transformación: map).

notas = [3, 7, 10, 5, 9]

def aprobo(n):
    return n >= 4

def subir(n):
    return min(n + 1, 10)

aprobadas = list(filter(aprobo, notas))
print(aprobadas)
print(list(map(subir, aprobadas)))
[7, 10, 5, 9]
[8, 10, 6, 10]

El 3 se queda afuera porque aprobo(3) da False. Las otras cuatro pasan, en el mismo orden y sin cambiar.

Por qué el min(n + 1, 10). Con n + 1 a secas, el 10 pasaría a 11, que no es una nota. min elige el menor entre el número subido y el tope: si n + 1 se pasa de 10, gana el 10.

Lo mismo con lambda y con una lista por comprensión, para comparar:

list(map(lambda n: min(n + 1, 10), filter(lambda n: n >= 4, notas)))
[min(n + 1, 10) for n in notas if n >= 4]

Control: la lista final no puede ser más larga que la de aprobadas (map no agrega ni saca elementos), y ningún valor puede pasar de 10. Las dos se cumplen: cuatro notas, la más alta 10.

Y uno del nivel del parcial

Un almacén guarda sus productos como tuplas (nombre, precio, stock): productos = [("yerba", 2500, 0), ("galletitas", 900, 12), ("queso", 4100, 0), ("pan", 1300, 0), ("leche", 1100, 5)] Usando filter, sorted y map, armar la lista de los nombres en mayúscula de los productos sin stock, ordenados del más barato al más caro.

Tres operaciones, y el orden en que se hacen es la mitad del ejercicio: filtrar los que no tienen stock, ordenarlos por precio, y recién al final quedarse con el nombre en mayúscula.

productos = [("yerba", 2500, 0), ("galletitas", 900, 12), ("queso", 4100, 0),
             ("pan", 1300, 0), ("leche", 1100, 5)]

def sin_stock(p):
    return p[2] == 0

def precio(p):
    return p[1]

def nombre_mayus(p):
    return p[0].upper()

faltan = sorted(filter(sin_stock, productos), key=precio)
print(faltan)
print(list(map(nombre_mayus, faltan)))
[('pan', 1300, 0), ('yerba', 2500, 0), ('queso', 4100, 0)]
['PAN', 'YERBA', 'QUESO']

Paso 1, filter. sin_stock mira el tercer dato de cada tupla (la posición 2). Quedan yerba, queso y pan, con las tuplas enteras: filter no las cambia.

Paso 2, sorted con key. sorted acepta lo que devuelve filter sin pasarlo por list(), porque sólo necesita poder recorrerlo, y devuelve una lista. El key=precio hace que compare por el segundo dato: 1300, 2500, 4100.

Paso 3, map. Cada tupla se reemplaza por su nombre en mayúscula. Va al final porque después de este paso ya no hay precio para ordenar ni stock para filtrar: sólo quedan textos.

La versión con lambda, sin funciones con nombre:

faltan = sorted(filter(lambda p: p[2] == 0, productos), key=lambda p: p[1])
nombres = list(map(lambda p: p[0].upper(), faltan))

El control: son tres productos sin stock (los que tienen un 0 al final), así que la respuesta tiene que tener tres nombres; y leyendo los precios a mano, 1300 < 2500 < 4100, el orden es pan, yerba, queso ✓

Dónde se pierde el punto: hacer el map antes de ordenar. Con sorted(map(nombre_mayus, filter(sin_stock, productos))) lo que se ordena son los nombres, alfabéticamente, y da ['PAN', 'QUESO', 'YERBA']: los mismos tres, en el orden equivocado. El otro error típico es filtrar con p[1] == 0, que pregunta por el precio y no deja pasar a ninguno.

Dónde se cae la mayoría

Hasta acá la explicación. La práctica es la otra mitad: 8 ejercicios de este tema, en cuatro niveles, que no te dicen sólo si está bien al final sino que te corrigen en cada paso y te explican por qué. Y uno para escribir el programa vos, que se prueba ejecutándolo en Python ahí mismo, en el navegador. Además trae una animación que muestra la idea en movimiento.

Practicar map, filter y lambda →

Los otros temas de la unidad 4

Estructuras de datos

Ver todos los temas de Pensamiento Computacional