Fundamentos
lambda, map, filter y sorted con key en Python
Aprende lambda, map, filter y sorted con key en Python: funciones pequeñas como argumento, qué hace cada una y el orden inestable de MicroPython.
Por Prof. Rodrigo Faustino · Actualizado el · 14 min de lectura
Métricas de este artículo
- Palabras
- 2711
- Caracteres
- 15.297
- Tiempo de lectura
- ~14 min
La lambda es una función pequeña, sin nombre, escrita en una sola línea: lambda n: n * 2 recibe n y devuelve el doble. Sirve principalmente para pasarse como argumento a otra función, y las tres que más la reciben son map, que transforma cada elemento de un grupo, filter, que mantiene solo los elementos que pasan una prueba, y sorted, que ordena por el valor que la función de key calcula para cada elemento.
Lo que vuelve esto posible es una idea que apareció de pasada en el artículo sobre def: en Python, una función es un valor, como un número o un texto. Se puede guardar en una variable, pasar como argumento y devolver desde otra función. Este artículo cierra el módulo de funciones mostrando esa idea en acción. Vamos a escribir lambdas, usar map, filter y sorted, ver los errores que aparecen en el camino y conocer un comportamiento del sorted en MicroPython que difiere del Python del computador y puede sorprender a quien ordena datos de sensores.
Las funciones son valores
Hasta aquí, toda función se llamó por su nombre, con paréntesis. Pero el nombre sin paréntesis, como dobro, es solo una referencia a la función, y se puede entregar a otra función:
def aplicar(f, x):
return f(x)
def dobro(n):
return n * 2
print(aplicar(dobro, 4))
print(aplicar(lambda n: n + 1, 4))
print(aplicar(abs, -3))
print(aplicar(str, 42) + "!")
f = dobro
print(f(10))
print(type(f))La salida es 8, 5, 3, 42!, 20 y <class 'function'>. La función aplicar no sabe qué hace f, solo que recibe un valor y devuelve otro. En la primera llamada, f es el dobro. En la segunda, es una lambda escrita en el momento. En la tercera, es la función lista abs, y en la cuarta es str, que convierte a texto. La línea f = dobro muestra que la función puede guardarse en una variable y llamarse por otro nombre, y el type(f) confirma que el valor es una función.
Este patrón, una función que recibe otra, aparece todo el tiempo en Python y es la base de map, filter y sorted.
Del programa al semestre planificado
- Plan de enseñanza y calendario montados solos
- Diario de clases listo para pegar en el SIGA
- Diapositivas y listas de ejercicios con solucionario
Versión Community gratis · Windows
lambda: una función de una expresión
La lambda es la forma corta de escribir una función simple. La figura muestra la misma función dobro escrita de las dos maneras. En la lambda, la palabra sustituye al def, los parámetros quedan sin paréntesis, los dos puntos vienen después de ellos y lo que sigue es una expresión, cuyo valor se devuelve automáticamente, sin escribir return.
def dobro(n):
return n * 2
dobro_l = lambda n: n * 2
print(dobro(4), dobro_l(4))
print((lambda a, b: a + b)(2, 3))
ola = lambda: "hola"
print(ola())
par = lambda n: "par" if n % 2 == 0 else "impar"
print(par(4), par(7))
pot = lambda base, exp=2: base ** exp
print(pot(5), pot(2, 10))La salida es 8 8, 5, hola, par impar y 25 1024. La lambda acepta cero, uno o más parámetros, e incluso valores por defecto, como el exp=2 de la potencia. La línea (lambda a, b: a + b)(2, 3) crea la función y la llama en el mismo punto, lo que raramente se hace fuera de ejemplos. El par muestra la expresión condicional, valor1 if condición else valor2, que es la manera de poner una decisión dentro de una sola expresión.
La restricción que define la lambda es que el cuerpo es una sola expresión. No caben return, asignaciones, if en varias líneas ni bucles:
f = lambda n: return nTraceback (most recent call last):
File "main.py", line 1
SyntaxError: invalid syntaxLo mismo vale para una asignación dentro de la lambda, que da SyntaxError: can't assign to expression. El print dentro de una lambda está permitido, porque es una llamada, y por tanto una expresión.
La lambda no tiene nombre: en el print(dobro) aparece <function <lambda> at 0x3ffb0010>, la dirección varía. Esto vuelve menos claros los mensajes de error. Por eso la regla de buen uso es la siguiente: si la función se va a reutilizar o tiene más de una línea de lógica, usa def y dale un nombre. La lambda es para el trozo corto que se entregará a otra función y se usará allí mismo.
map: transformar cada elemento
El map(función, grupo) aplica la función a cada elemento del grupo y produce los resultados. Devuelve un objeto que genera los valores a demanda, y para ver los resultados como lista hay que convertirlo con list():
numeros = [1, 2, 3, 4]
dobros = map(lambda n: n * 2, numeros)
print(list(dobros))
print(list(map(str, numeros)))
print(list(map(lambda a, b: a + b, [1, 2, 3], [10, 20, 30])))
for d in map(lambda n: n * n, range(1, 5)):
print(d, end=" ")
print()
print(sum(map(lambda n: n * 2, numeros)))La salida es [2, 4, 6, 8], ['1', '2', '3', '4'], [11, 22, 33], 1 4 9 16 y 20. Con dos grupos, la función recibe un elemento de cada uno por vez, como en la suma par a par. En un for o en un sum, el map puede usarse sin convertir. La cantidad de elementos de la salida es igual a la de la entrada, como muestra la figura.
Dos trampas del map merecen registro. La primera es que el resultado no es una lista:
m = map(lambda n: n * 2, [1, 2, 3])
print(m[0])Traceback (most recent call last):
File "main.py", line 2, in <module>
TypeError: 'map' object isn't subscriptableEl Python del computador escribe is not subscriptable. Lo mismo ocurre con len(m), que da object of type 'map' has no len(). La segunda es que el map se agota después de usarse una vez:
m = map(lambda n: n + 1, [1, 2])
print(list(m))
print(list(m))La salida es [2, 3] y después []. La segunda conversión ya no encuentra valores, porque la primera consumió todo. Cuando el resultado se vaya a usar más de una vez, guarda la lista: dobros = list(map(...)).
filter: mantener solo lo que pasa
El filter(función, grupo) mantiene los elementos para los cuales la función devuelve verdadero. La función es una prueba, como las condiciones de los artículos sobre if:
numeros = [1, 2, 3, 4, 5, 6]
pares = filter(lambda n: n % 2 == 0, numeros)
print(list(pares))
print(list(filter(lambda n: n > 3, numeros)))
print(list(filter(None, [0, 1, "", "a", None, 2, [], [3]])))
print(list(filter(lambda s: len(s) > 3, ["pera", "uva", "banana", "kiwi"])))La salida es [2, 4, 6], [4, 5, 6], [1, 'a', 2, [3]] y ['pera', 'banana', 'kiwi']. La figura muestra el primer caso, con los impares apagados. La tercera línea usa None en lugar de la función: el filter entonces mantiene los elementos verdaderos y descarta los "vacíos", como el 0, el texto vacío, el None y la lista vacía. Es un atajo práctico para limpiar un grupo.
El filter tiene el mismo comportamiento de objeto a demanda que el map, y las mismas trampas: necesita list() para volverse lista y se agota después de usarse. Los errores de tipo aparecen cuando el segundo argumento no es un grupo:
print(list(filter(lambda n: n > 1, 5)))Traceback (most recent call last):
File "main.py", line 1, in <module>
TypeError: 'int' object isn't iterableEl Python del computador dice 'int' object is not iterable.
La IA que recuerda tu vida y vive en tu ordenador
- Memoria a largo plazo con la fuente de cada respuesta
- Funciona sin conexión: tus datos se quedan en tu PC
- Lee PDF, diapositivas, fotos y grabaciones de reuniones
Plan Esencial gratis para siempre
sorted: ordenar con key
El sorted(grupo) devuelve una lista nueva con los elementos en orden creciente, sin alterar el original. Con reverse=True, el orden es decreciente. Y con key=función, la ordenación deja de comparar los elementos directamente y compara el valor que la función calcula para cada uno:
nums = [3, 1, 2]
print(sorted(nums))
print(sorted(nums, reverse=True))
print(nums)
palavras = ["pera", "uva", "banana"]
print(sorted(palavras))
print(sorted(palavras, key=len))
print(sorted(palavras, key=len, reverse=True))La salida es [1, 2, 3], [3, 2, 1], [3, 1, 2], ['banana', 'pera', 'uva'], ['uva', 'pera', 'banana'] y ['banana', 'pera', 'uva']. El nums sigue siendo [3, 1, 2], porque el sorted no lo toca. Sin key, los textos se ordenan en orden alfabético. Con key=len, se ordenan por el tamaño: uva (3), pera (4), banana (6), y con reverse=True el orden se invierte. La figura muestra el caso del key=len. Aquí todas las claves son diferentes, y eso importa para el próximo punto.
El método lista.sort() ordena la propia lista, en el lugar, y devuelve None:
lista = [3, 1, 2]
r = lista.sort()
print(r, lista)
nova = sorted([3, 1, 2])
print(nova)La salida es None [1, 2, 3] y [1, 2, 3]. Escribir lista = lista.sort() es un error clásico: la variable pasa a guardar None y la lista se pierde.
El key es muy útil con tuplas, cuando cada elemento tiene varias partes:
sensores = [("sala", 24.5), ("cocina", 27.0), ("cuarto", 22.5)]
mais_quente = max(sensores, key=lambda s: s[1])
print("más caliente:", mais_quente[0], mais_quente[1])
for nome, temp in sorted(sensores, key=lambda s: s[1]):
print(nome, temp)
frias = list(filter(lambda s: s[1] < 25, sensores))
print(frias)
nomes = list(map(lambda s: s[0], sensores))
print(nomes)La salida es más caliente: cocina 27.0, las tres líneas cuarto 22.5, sala 24.5 y cocina 27.0, [('sala', 24.5), ('cuarto', 22.5)] y ['sala', 'cocina', 'cuarto']. El s[1] toma la temperatura de cada tupla. Las funciones max y min también aceptan key, y el max devuelve la tupla entera, lo que permite saber qué sensor tiene el mayor valor, y no solo el valor. El map con s[0] extrae solo los nombres.
Atención: el sorted de MicroPython no es estable
Una ordenación es estable cuando los elementos con la misma clave quedan en el mismo orden en que estaban. El Python del computador lo garantiza. MicroPython no: su código usa un algoritmo de ordenación que puede cambiar el orden de elementos empatados, y el simulador del proyecto reproduce el algoritmo de la placa. El mismo programa da resultados diferentes:
palavras = ["pera", "uva", "banana", "kiwi"]
print(sorted(palavras, key=len))
print(sorted(palavras, key=lambda s: (len(s), s)))En el simulador, la salida es ['uva', 'kiwi', 'pera', 'banana'] en las dos líneas. En el Python del computador, la primera línea da ['uva', 'pera', 'kiwi', 'banana'], con pera antes que kiwi, como en la lista original. Las palabras pera y kiwi tienen el mismo tamaño, 4, y empatan. El computador mantiene el orden de entrada. El simulador lo invierte.
En la práctica, cuando hay empate, el orden entre los empatados no está garantizado en la ESP32. La solución es eliminar el empate con una clave compuesta, una tupla con el criterio principal y uno de desempate:
leituras = [("a", 30), ("b", 12), ("c", 25), ("d", 12), ("e", 12)]
print(sorted(leituras, key=lambda t: (t[1], t[0])))
print(sorted(["b", "A", "c", "B"], key=lambda s: (s.lower(), s)))La salida es [('b', 12), ('d', 12), ('e', 12), ('c', 25), ('a', 30)] y ['A', 'B', 'b', 'c'], iguales en el simulador y en el computador. La clave (t[1], t[0]) ordena primero por el valor y, en los empates, por el nombre, y el resultado queda predecible. Siempre que el orden de los empatados importe, como en una lista de clasificación o en un informe de sensores, desempata con una clave compuesta.
Otro detalle del simulador, que también reproduce la placa: la función key se llama en cada comparación, y no una vez por elemento. Contando las llamadas en una lista de 8 elementos, el simulador hace 19, y el Python del computador hace 8:
chamadas = 0
def chave(x):
global chamadas
chamadas += 1
return x
lista = [5, 3, 8, 1, 9, 2, 7, 4]
r = sorted(lista, key=chave)
print(r, "llamadas de la key:", chamadas, "elementos:", len(lista))La salida es [1, 2, 3, 4, 5, 7, 8, 9] llamadas de la key: 19 elementos: 8. Esto importa en dos casos: una key lenta, que consulta un sensor, por ejemplo, se llamará muchas veces, y una key con efecto colateral, como un print, aparecerá más veces de lo esperado. Mantén la key corta y sin efecto colateral.
Errores comunes con sorted
Ordenar valores de tipos incomparables da TypeError:
print(sorted([3, "a", 1]))Traceback (most recent call last):
File "main.py", line 1, in <module>
TypeError: unsupported types for __lt__: 'int', 'str'El Python del computador escribe '<' not supported between instances of 'str' and 'int'. La causa es una lista que mezcla números y textos, como ocurre cuando una lectura llega como texto. Convierte antes, con int() o float(), o usa una key que normalice. Pasar algo que no es un grupo, como sorted(5), da 'int' object isn't iterable. Y una key con la cantidad equivocada de parámetros también falla:
print(sorted(["a", "b"], key=lambda: 1))Traceback (most recent call last):
File "main.py", line 1, in <module>
TypeError: function takes 0 positional arguments but 1 were givenLa key siempre recibe un argumento, el elemento, así que la lambda necesita un parámetro, como lambda s: ....
Una trampa de la lambda dentro de bucles
Cuando una lambda usa una variable de afuera, guarda el nombre, y no el valor del momento en que fue creada. El valor solo se lee cuando se llama a la lambda:
fs = []
for i in range(3):
fs.append(lambda: i)
print(fs[0](), fs[1](), fs[2]())
gs = []
for i in range(3):
gs.append(lambda i=i: i)
print(gs[0](), gs[1](), gs[2]())La salida es 2 2 2 y 0 1 2. En la primera versión, las tres lambdas miran la misma i, que al final del bucle vale 2. En la segunda, el i=i es un parámetro con valor por defecto, y el valor por defecto se calcula en la creación, de modo que cada lambda guarda el suyo. Es el mismo mecanismo del artículo sobre alcance, y aparece principalmente al crear varios manejadores de evento en un bucle.
En la ESP32
Los tres recursos encajan bien en el tratamiento de lecturas. Retomando el ejemplo del ADC, que lee de 0 a 4095, el filter descarta las lecturas fuera de rango, el max y el min encuentran los extremos y el sorted las ordena:
leituras = [512, -1, 530, 4096, 498]
validas = list(filter(lambda v: 0 <= v <= 4095, leituras))
print(validas)
print("mayor", max(validas), "menor", min(validas))
print(sorted(validas, reverse=True))
print("media", sum(validas) / len(validas))
percentual = list(map(lambda v: v * 100 // 4095, validas))
print(percentual)La salida es [512, 530, 498], mayor 530 menor 498, [530, 512, 498], media 513.3333 y [12, 12, 12]. El mismo conjunto de lecturas apareció en el artículo sobre break y continue, con un bucle y un continue; aquí el filter hace el trabajo del descarte en una línea. El 513.3333 es el float de 32 bits del simulador, y el Python del computador mostraría 513.3333333333334. El artículo sobre ADC explica el rango de lectura.
Las lambdas también son la manera habitual de escribir manejadores de evento cortos. El simulador acepta registrar una en la interrupción de un botón:
from machine import Pin
botao = Pin(4, Pin.IN, Pin.PULL_UP)
botao.irq(handler=lambda p: print("presionó", p), trigger=Pin.IRQ_FALLING)
print("irq configurado")El Serial muestra irq configurado. El manejador solo se llama cuando el evento ocurre, y recibe el pin como argumento, el p de la lambda. El handler= y el trigger= son argumentos con nombre, como en el artículo sobre parámetros. Si el manejador necesita más de una línea o altera una variable global, escribe una función con def. El artículo sobre programar sin colgarse trata de cómo organizar este tipo de código.
En el aula: dónde suelen tropezar estos recursos
Estos casos se repiten en los grupos.
Olvidar el list() después del map o del filter
El alumno imprime map(...) y ve <map object at 0x3ffb0020>, creyendo que el computador "no calculó". El objeto solo genera los valores cuando alguien los pide, y el list() los pide todos.
Usar el resultado dos veces
El primer uso funciona y el segundo viene vacío, porque map y filter se agotan. La solución es guardar la lista.
Lambda con más de una línea
El alumno intenta poner un if con else en líneas separadas, o un return, dentro de la lambda, y recibe SyntaxError. Cuando la lógica necesita más de una expresión, es hora de usar def.
Asignar la lista ordenada al resultado del sort
lista = lista.sort() deja None en la variable. La elección es entre lista.sort(), que ordena en el lugar, y lista = sorted(lista), que crea una nueva.
Esperar estabilidad en el sorted de la ESP32
El programa funciona perfectamente en el computador, y en la placa los empatados aparecen en otro orden. El desempate con clave compuesta lo resuelve, y vale incluirlo siempre que el orden de los iguales haga diferencia.
Comparar tipos mezclados
Una lista con 3, "a" y 1. Ocurre mucho con datos leídos de un archivo o de una entrada, en que todo llega como texto. La conversión con int() antes de ordenar evita el TypeError.
Las lambdas del bucle que dan todas el mismo valor
El caso del 2 2 2. La corrección es el i=i, y el síntoma es siempre el mismo: todas las funciones creadas en el bucle responden con el valor del último paso.
Ejercicio: ranking de alumnos
Dada una lista de alumnos con nota, produce tres cosas: el ranking de la mayor a la menor nota, la lista de los aprobados (nota 7 o más) y la lista de los nombres en mayúsculas.
alunos = [("Ana", 8.5), ("Bia", 6.0), ("Caio", 9.0), ("Duda", 6.0)]
print(sorted(alunos, key=lambda a: a[1], reverse=True))
aprovados = list(filter(lambda a: a[1] >= 7, alunos))
print(aprovados)
print(list(map(lambda a: a[0].upper(), alunos)))La salida es [('Caio', 9.0), ('Ana', 8.5), ('Bia', 6.0), ('Duda', 6.0)], [('Ana', 8.5), ('Caio', 9.0)] y ['ANA', 'BIA', 'CAIO', 'DUDA']. Fíjate en que Bia y Duda empatan con 6.0. En este ejemplo el orden de ellas coincidió con el del computador, pero, como la ordenación de MicroPython no es estable, no hay garantía de que eso se repita con otros datos. Para mejorarlo, cambia la clave por lambda a: (-a[1], a[0]), que ordena por nota decreciente y, en los empates, por nombre, y confirma que el resultado queda predecible. El signo menos invierte la nota sin necesitar reverse=True.
Preguntas frecuentes
¿Qué es una función anónima?
Es una función sin nombre. La lambda crea una, y por eso Python la muestra como <lambda> en los mensajes.
¿Puedo guardar una lambda en una variable?
Puedes, como en dobro = lambda n: n * 2. Funciona, pero si la función merece un nombre, el def es más claro y da mejores mensajes de error.
¿Cuándo usar map y filter, y cuándo usar un bucle?
Cuando la transformación o la prueba cabe en una expresión corta, las dos formas funcionan, y la elección es de lectura. Si el cuerpo necesita varias líneas, el bucle es mejor. El próximo módulo de la serie muestra las comprensiones de lista, que hacen el mismo trabajo de map y filter de una manera que mucha gente encuentra más legible.
¿Por qué existen los dos, sorted y sort?
El sorted acepta cualquier grupo y devuelve una lista nueva. El sort es un método de lista, ordena la propia lista y devuelve None. Usa el sorted cuando necesites el original.
¿Qué cambia en MicroPython?
La principal diferencia es la ordenación no estable: los elementos empatados pueden salir en otro orden, y la key se llama varias veces por elemento. El resto, lambda, map y filter, se comporta como en el computador.
¿Se puede usar key en min y max?
Se puede. El max(sensores, key=lambda s: s[1]) devuelve el elemento con el mayor valor de s[1], y no solo el valor.
Para continuar
Este artículo cierra el módulo de funciones. Si quieres repasar la base, vuelve a los artículos sobre funciones, parámetros y argumentos, return, alcance y recursividad. Los desafíos Función Doble y Mayor de Dos pueden resolverse también con lambda, como ejercicio opcional, y la ruta Lógica con Python reúne todos los desafíos.
El próximo artículo abre el módulo de estructuras de datos con las listas: cómo crear, acceder, alterar y recorrer, que es lo que varios ejemplos de este artículo ya usaron sin presentarlo.