Ir al contenido
wannacode

Lambdas

En resumen: Una lambda es una función corta sin nombre, con una sola expresión, que pasas como valor, por ejemplo como key de sorted.

Problema

Una función entera para una regla

Hay que ordenar los pedidos por total.

Order
class Order:
def __init__(self, customer, total):
self.customer = customer
self.total = total
orders = [
Order("María", 75.50),
Order("Jorge", 12.90),
Order("Elena", 40.00),
]
def by_total(order):
return order.total
orders.sort(key=by_total)
for o in orders:
print(f"{o.customer}: {o.total:.2f} €")
Salida
Jorge: 12.90 €
Elena: 40.00 €
María: 75.50 €

sort(key=...) recibe una función que dice, para cada elemento, por qué ordenarlo. Aquí es by_total: para un pedido, devuelve su total.

Funciona. Pero para una regla («por total») escribes una función entera con nombre y líneas en blanco alrededor, lejos del lugar donde la usas. Y solo se llama aquí. Tú quieres pasar solo la regla.

La idea

Una lambda: solo la regla

El mismo orden, en una línea:

orders.sort(key=lambda o: o.total)

lambda o: o.total se llama expresión lambda, o lambda para abreviar. Es una función sin nombre: después de lambda van los parámetros, después de los dos puntos va el cuerpo. No escribes return: el valor del cuerpo es el resultado.

Las formas de escribirla:

  • Un parámetro: lambda o: o.total > 50. Varios: lambda a, b: a.total + b.total. Sin parámetros: lambda: ....
  • El cuerpo es una expresión. Si la regla necesita varias líneas, un if o un bucle, es una función normal con def.
by_total = lambda o: o.total
total_of_two = lambda a, b: a.total + b.total
guest = lambda: Order("Invitado", 0)
orders.sort(key=by_total)
for o in orders:
print(f"{o.customer}: {o.total:.2f} €")
print(total_of_two(orders[0], orders[1]))
print(guest().customer)
Salida
Jorge: 12.90 €
Elena: 40.00 €
María: 75.50 €
52.9
Invitado

Una lambda es un valor: puedes guardarla en una variable y pasarla más tarde. Lo mismo vale para una función escrita con def: by_total sin paréntesis es la propia función, por eso funcionó sort(key=by_total).

Más precisamente, para curiosos

La guía de estilo PEP 8 aconseja: si la regla tiene nombre, escríbela con def, no con nombre = lambda .... Una lambda es para el lugar donde la regla se pasa directamente, como en sort(key=lambda o: o.total). En esta lección algunas reglas están en variables, para que veas que una lambda es un valor como cualquier otro.

La idea

Una función es un valor

¿Cómo sabe sort que lambda o: o.total es la función de clave? No necesita saber nada más. En Python una función es un valor, como un número o una cadena: puedes guardarla en una variable, pasarla a otra función y llamarla con paréntesis.

student = lambda price: price * 0.8
minus_five = lambda price: price - 5
print(student(50))
print(minus_five(50))
Salida
40.0
45

student es un objeto de tipo function. Lo llamas como cualquier función: student(50). Dos reglas de precio, y ni una sola clase.

Aquí Java necesita una interfaz funcional: una interfaz con un método que dice qué tipo de regla es. Python no necesita nada parecido. Cualquier función se puede pasar y llamar.

student = lambda price: price * 0.8
minus_five = lambda price: price - 5
print(student(50))
print(minus_five(50))
Paso a paso, en palabras
  1. La lambda es un objeto de tipo function, y student apunta a él. Su cuerpo todavía no se ejecuta.
  2. Una segunda regla, un segundo objeto. No se escribe ni una sola clase.
  3. student(50) pasa 50 a la lambda: price es 50, y price * 0.8 da 40.0.
  4. minus_five(50): el mismo número, otra regla, 45.
Más precisamente, para curiosos
  • Si quieres dejar escrito qué regla esperas, Python tiene una sugerencia de tipo (type hint): Callable[[float], float] significa «una función que recibe un número y devuelve un número». Python no la comprueba. Lo incorrecto llega hasta el lugar donde se llama, y solo allí se detiene el programa:
from collections.abc import Callable
PriceRule = Callable[[float], float]
def show(rule: PriceRule, price: float):
print(rule(price))
show(lambda price: price * 0.8, 50)
show("estudiante", 50)
Salida
40.0
Traceback (most recent call last):
...
TypeError: 'str' object is not callable
  • callable(x) te dice si x se puede llamar: callable(student) es True, y callable("estudiante") es False.

La idea

Los cuatro tipos de reglas

El tipo de toda regla es function, pero hay cuatro tipos de reglas. Conviene reconocerlos, porque las funciones que reciben reglas quieren un tipo concreto:

  • condición: recibe un valor, devuelve True o False. «¿El pedido es grande?»
  • transformación: recibe una cosa, devuelve otra. Un pedido en una etiqueta.
  • acción: recibe un valor y hace algo con él: lo imprime, lo guarda. Su resultado no se usa.
  • proveedor: no recibe nada, devuelve un objeto nuevo cuando se lo pides.
big = lambda o: o.total >= 50
label = lambda o: o.customer.upper()
show = lambda text: print("-> " + text)
guest = lambda: Order("Invitado", 0)
order = Order("María", 75.50)
print(big(order))
show(label(order))
print(guest().customer)
Salida
True
-> MARÍA
Invitado

En Java las reglas son objetos con métodos que las combinan: las condiciones se unen con and, or y negate («no»), y las funciones se encadenan una tras otra con andThen. Python no tiene esos métodos. La nueva regla es una nueva función que llama a las anteriores: lambda o: big(o) and maria(o). Dos funciones una tras otra también se combinan con una función:

price_with_vat = lambda price: price * 1.2
rounded = lambda price: round(price, 2)
then = lambda f, g: lambda x: g(f(x))
final_price = then(price_with_vat, rounded)
print(final_price(10.555))
Salida
12.67

then recibe dos funciones y devuelve una tercera: llama a la primera y pasa su resultado a la segunda.

Más precisamente, para curiosos
  • Una acción igualmente devuelve algo: None, porque ese es el resultado de print. print(show("café")) imprime «-> café» y luego None.
  • El módulo operator tiene funciones listas para las transformaciones más comunes. attrgetter("total") es lo mismo que lambda o: o.total.

Pruébalo

Condiciones combinadas

¿Qué imprimirá el programa?

big = lambda o: o.total >= 50
maria = lambda o: o.customer == "María"
big_and_maria = lambda o: big(o) and maria(o)
big_or_maria = lambda o: big(o) or maria(o)
not_big = lambda o: not big(o)
small = Order("María", 30)
large = Order("Jorge", 80)
print(big_and_maria(small))
print(big_or_maria(small))
print(not_big(large))
Respuesta

Míralo

Funciones que reciben funciones

Muchas funciones incorporadas reciben una regla en lugar de un bucle:
from collections import defaultdict
cart = ["pan", "", "miel", " ", "azúcar"]
cart = list(filter(lambda item: item.strip() != "", cart))
for item in cart:
print(item)
print(max(cart, key=lambda item: len(item)))
by_city = defaultdict(lambda: [])
by_city["Granada"].append("María")
by_city["Madrid"].append("Jorge")
by_city["Granada"].append("Elena")
for city, names in sorted(by_city.items()):
print(f"{city}: {names}")
Salida
pan
miel
azúcar
azúcar
Granada: ['María', 'Elena']
Madrid: ['Jorge']
  • filter(condición, lista) deja pasar solo los elementos para los que la condición es verdadera. Los da de uno en uno, así que list(...) los reúne en una lista nueva. La anterior no cambia: aquí cart simplemente apunta a la nueva.
  • max(lista, key=función) y min encuentran el elemento más grande y el más pequeño según la regla. key es lo mismo que en sort: aquí es la longitud, así que gana «azúcar».
  • defaultdict(función) es un diccionario que llama a la función cuando falta una clave, pone el resultado bajo la clave y lo devuelve. Así agrupar por ciudad funciona sin if city not in by_city. Aquí la función es lambda: [], pero también puede ser el propio list: también devuelve una lista vacía nueva.
  • Python no tiene forEach. Para una acción con cada elemento se usa for.

El diccionario mantiene las claves en el orden en que se añadieron. sorted(by_city.items()) las ordena, por eso Granada va antes que Madrid.

La idea

Una función por su nombre, sin paréntesis

A menudo una lambda solo pasa su parámetro a una función que ya existe: lambda s: int(s). Entonces pasa la propia función, por su nombre y sin paréntesis:
names = ["maría", "jorge"]
join = ", ".join
upper = str.upper
parse = int
new_list = list
print(join(names))
print(upper("elena"))
print(parse("42") + 1)
items = new_list()
items.append("café")
print(items)
Salida
maría, jorge
ELENA
43
['café']

Java tiene una notación aparte para esto, la referencia a método con ::. En Python basta con el nombre. Los tipos son los mismos cuatro:

  • un método de un objeto concreto: ", ".join es lambda items: ", ".join(items);
  • un método del parámetro: str.upper es lambda s: s.upper();
  • una función: int es lambda s: int(s);
  • una clase: list es lambda: list(). Una clase se llama como una función y devuelve un objeto nuevo.

Si la lambda hace algo más, por ejemplo lambda s: s.strip().upper(), déjala como lambda.

Míralo

Ordenar por varios campos

key puede devolver una tupla: varios valores entre paréntesis. Las tuplas se comparan de izquierda a derecha: primero los primeros valores, y si son iguales, los siguientes.

Los estudiantes se ordenan por nota, de la más alta a la más baja, y por nombre cuando las notas son iguales:

students = [
Student("Juan", 5.50),
Student("Ana", 6.00),
Student("Pedro", 5.50),
Student("Bruno", 4.75),
]
students.sort(key=lambda s: (-s.grade, s.name))
Salida
Ana 6.0
Juan 5.5
Pedro 5.5
Bruno 4.75

-s.grade invierte la dirección solo de la nota: una nota más alta da un número más pequeño y va primero. El nombre sigue en orden alfabético. Ni un solo if.

Más precisamente, para curiosos
  • El texto no se puede invertir con un signo menos. Pero ordenar en Python es estable: los elementos iguales se quedan en el orden en que estaban. Así que «por nota al revés, luego por nombre» también funciona con dos ordenaciones. Primero por la segunda regla, luego por la primera:
students.sort(key=lambda s: s.name)
students.sort(key=lambda s: s.grade, reverse=True)
Salida
Ana 6.0
Juan 5.5
Pedro 5.5
Bruno 4.75
  • Una función de comparación, como compare en Java (un número negativo, 0 o un número positivo), se convierte en una key con cmp_to_key del módulo functools:
from functools import cmp_to_key
def compare(a, b):
return (a.grade > b.grade) - (a.grade < b.grade)
students.sort(key=cmp_to_key(compare))
Salida
Bruno 4.75
Juan 5.5
Pedro 5.5
Ana 6.0

Pruébalo

¿Qué invierte reverse=True?

Aquí la clave es (s.grade, s.name), sin signo menos, y reverse=True invierte la dirección.

¿Qué imprimirá el programa?

students = [
Student("Juan", 5.50),
Student("Ana", 6.00),
Student("Pedro", 5.50),
Student("Bruno", 4.75),
]
students.sort(key=lambda s: (s.grade, s.name), reverse=True)
for s in students:
print(s.name, s.grade)
Respuesta

Pruébalo

Encuentra el error

El programa debería imprimir la suma de los precios. Se detiene con un error en el primer precio. ¿En qué línea está el error?

Elige la línea con el error.

Más precisamente, para curiosos

Una lambda ve la propia variable, no una copia. Si la variable cambia después de la lambda, la siguiente llamada ve el valor nuevo:

rate = 0.2
discount = lambda price: price * (1 - rate)
print(discount(100))
rate = 0.5
print(discount(100))
Salida
80.0
50.0

En Java esto está prohibido precisamente por sorpresas como esta. Si la regla tiene que recordar un valor, pásalo como parámetro.

Ejercicio

Reglas como valores

Resuelve las tareas en tu editor.

1. Ordenar por varios campos. Una clase Product con nombre, categoría y precio. Ordena los productos por categoría, dentro de una categoría del más caro al más barato, y por nombre cuando el precio es igual, con un solo sort y una tupla en key. Los productos son: ratón, accesorios, 25 €; portátil, computadoras, 1299 €; teclado, accesorios, 49,90 €; tableta, computadoras, 399 €; auriculares, accesorios, 49,90 €.

Salida
Accesorios | Auriculares | 49.90 €
Accesorios | Teclado | 49.90 €
Accesorios | Ratón | 25.00 €
Computadoras | Portátil | 1299.00 €
Computadoras | Tableta | 399.00 €

2. Filtrar pedidos. Añade una ciudad a Order. Escribe una función select(orders, rule) que devuelva una lista nueva solo con los pedidos para los que la regla es verdadera. Haz dos reglas, madrid y big (al menos 50 €), e imprime los pedidos para los que se cumplen las dos, los de fuera de Madrid y los «de Madrid o de al menos 100 €». Cada una de las tres reglas es una lambda que llama a madrid y big. Los pedidos: María, Madrid, 75,50 €; Jorge, Valencia, 12,90 €; Elena, Madrid, 40 €; Diego, Granada, 120 €.

Salida
Madrid, desde 50 €:
María, Madrid: 75.50 €
Fuera de Madrid:
Jorge, Valencia: 12.90 €
Diego, Granada: 120.00 €
Envío gratis (Madrid o desde 100 €):
María, Madrid: 75.50 €
Elena, Madrid: 40.00 €
Diego, Granada: 120.00 €
3. Descuentos uno tras otro. Haz tres descuentos como lambdas: un descuento de estudiante (−10 %), un descuento de Black Friday (−20 €) y «no menos de cero». Escribe then(f, g), que devuelve una función: primero f, luego g. Úsala para encadenar los descuentos en dos órdenes: primero el de estudiante y primero el del viernes. Pruébalo con 120 € y con 15 €. ¿Importa el orden?
Salida
120.00 € -> estudiante y luego viernes: 88.00 €; viernes y luego estudiante: 90.00 €
15.00 € -> estudiante y luego viernes: 0.00 €; viernes y luego estudiante: 0.00 €

Más tareas

  • Limpiar una lista. De una lista de correos, elimina los vacíos y los que no tienen @ con un solo filter, y luego imprime el resto con for.
  • Estudiantes por clase. Agrupa estudiantes por clase ("10A", "10B") en un defaultdict(list) e imprime las clases en orden con sorted.
  • Validadores. Haz un diccionario con tres validadores como lambdas: texto no vacío, solo dígitos (str.isdigit), longitud de hasta 10. Prueba cada uno con algunas cadenas.

Test

Ponte a prueba

1. ¿Qué puede contener el cuerpo de una lambda?
2. ¿Cuál de estas es una condición: recibe un pedido y devuelve True o False?
3. ¿Qué puede reemplazar a lambda s: s.upper()?
4. students.sort(key=lambda s: (s.grade, s.name), reverse=True). ¿Qué invierte reverse=True?
5. ¿Por qué total += p dentro de una función que se llama en un bucle se detiene con UnboundLocalError?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·