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.
class Order: def __init__(self, customer, total): self.customer = customer self.total = totalorders = [ 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} €")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
ifo un bucle, es una función normal condef.
by_total = lambda o: o.totaltotal_of_two = lambda a, b: a.total + b.totalguest = 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)Jorge: 12.90 €Elena: 40.00 €María: 75.50 €52.9InvitadoUna 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.8minus_five = lambda price: price - 5
print(student(50))print(minus_five(50))40.045student 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.8minus_five = lambda price: price - 5
print(student(50))print(minus_five(50))Paso a paso, en palabras
- La lambda es un objeto de tipo function, y student apunta a él. Su cuerpo todavía no se ejecuta.
- Una segunda regla, un segundo objeto. No se escribe ni una sola clase.
- student(50) pasa 50 a la lambda: price es 50, y price * 0.8 da 40.0.
- 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)40.0Traceback (most recent call last): ...TypeError: 'str' object is not callablecallable(x)te dice sixse puede llamar:callable(student)esTrue, ycallable("estudiante")esFalse.
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
TrueoFalse. «¿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 >= 50label = 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)True-> MARÍAInvitadoEn 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.2rounded = 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))12.67then 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 deprint.print(show("café"))imprime «-> café» y luegoNone. - El módulo
operatortiene funciones listas para las transformaciones más comunes.attrgetter("total")es lo mismo quelambda o: o.total.
Pruébalo
Condiciones combinadas
¿Qué imprimirá el programa?
big = lambda o: o.total >= 50maria = 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))big_and_maria da False, y big_or_maria da True, porque es de María. El pedido de Jorge es de 80 €, es grande, y not invierte la respuesta: False.Míralo
Funciones que reciben funciones
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}")panmielazúcarazúcarGranada: ['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í quelist(...)los reúne en una lista nueva. La anterior no cambia: aquícartsimplemente apunta a la nueva.max(lista, key=función)yminencuentran el elemento más grande y el más pequeño según la regla.keyes lo mismo que ensort: 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 sinif city not in by_city. Aquí la función eslambda: [], pero también puede ser el propiolist: también devuelve una lista vacía nueva.- Python no tiene
forEach. Para una acción con cada elemento se usafor.
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
lambda s: int(s). Entonces pasa la propia función, por su nombre y sin paréntesis:names = ["maría", "jorge"]join = ", ".joinupper = str.upperparse = intnew_list = list
print(join(names))print(upper("elena"))print(parse("42") + 1)items = new_list()items.append("café")print(items)maría, jorgeELENA43['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:
", ".joineslambda items: ", ".join(items); - un método del parámetro:
str.uppereslambda s: s.upper(); - una función:
inteslambda s: int(s); - una clase:
listeslambda: 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))Ana 6.0Juan 5.5Pedro 5.5Bruno 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)Ana 6.0Juan 5.5Pedro 5.5Bruno 4.75- Una función de comparación, como
compareen Java (un número negativo, 0 o un número positivo), se convierte en unakeyconcmp_to_keydel módulofunctools:
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))Bruno 4.75Juan 5.5Pedro 5.5Ana 6.0Pruébalo
¿Qué invierte reverse=True?
(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)reverse=True invierte el orden por toda la clave, y la clave es «por nota, luego por nombre». Así que se invierten las dos: las notas van de la más alta a la más baja, y Pedro va antes que Juan. Si quieres invertir solo las notas, el signo menos va delante de la nota, como en la tarjeta anterior.Pruébalo
Encuentra el error
Elige la línea con el error.
La función add asigna a total:
Traceback (most recent call last): ... total += p ^^^^^UnboundLocalError: cannot access local variable 'total' where it is not associated with a valueEn cuanto una función asigna a un nombre, Python decide que es una variable local de la función. total += p primero quiere leer el total local, y este todavía no tiene valor. El total de fuera ni siquiera se toca.
Leer una variable de fuera está permitido. Asignarle no, a menos que la función diga global total (o nonlocal total, si la variable está en otra función). Rara vez es buena idea. Para una suma, usa un bucle normal: for p in prices: total += p en el nivel superior. En la próxima lección también lo harás en una línea.
En una lambda, += no es posible en absoluto: su cuerpo es una expresión, y una asignación no lo es. lambda p: total += p es un SyntaxError.
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.2discount = lambda price: price * (1 - rate)print(discount(100))
rate = 0.5print(discount(100))80.050.0En 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
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 €.
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 €.
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 €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?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 solofilter, y luego imprime el resto confor. - Estudiantes por clase. Agrupa estudiantes por clase (
"10A","10B") en undefaultdict(list)e imprime las clases en orden consorted. - 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
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Interfaces como clases abstractas · Listas: append, remove y sort · dict y set · Clases anidadas y datos
- Continúa en: Listas por comprensión y generadores · dataclass, match y patrones · Patrones de diseño · Tests con unittest
- Ejercicios sobre el tema: Python moderno