Lambdas y comprensiones
Estas tres características tienen algo en común: te permiten expresar ideas que de otra forma ocuparían varias líneas en una sola expresión legible. Bien usadas, hacen que el código sea más corto y más claro. Mal usadas, lo vuelven ilegible. Este capítulo explica cuándo recurrir a cada una y cuándo parar.
Funciones lambda
Un lambda es una función sin nombre, formada por una sola expresión. Se crea con la palabra clave lambda. Su verdadera utilidad es que puedes escribirla en línea, justo donde la necesitas, sin definir antes una función con nombre. Por eso resulta tan útil junto con sorted().
double = lambda x: x * 2
double(5) # 10Esto es equivalente a:
def double(x):
return x * 2Para la mayoría de los casos, usa def. Los lambdas tienen una ventaja real: puedes escribirlos en línea, justo donde los necesitas, sin darles nombre. Por eso son útiles junto con sorted(), map() y filter():
players = [("Ana", 87), ("Bruno", 74), ("Carla", 92)]
sorted(players, key=lambda p: p[1]) # ordenar por puntaje (ascendente)
sorted(players, key=lambda p: p[1], reverse=True) # ordenar por puntaje (descendente)Sin un lambda, tendrías que definir una función con nombre solo para el argumento key=. El lambda mantiene la intención local y visible.
Los lambdas pueden recibir varios argumentos:
add = lambda a, b: a + b
add(3, 4) # 7Cuándo usar un lambda: solo cuando se trate de una expresión pequeña usada en un solo lugar. Si empieza a crecer, o necesitas reutilizarla, escribe un def propiamente. Un lambda que abarca varios operadores o necesita una condicional suele ser señal de que conviene cambiar a def.
lambda. Su razón de existir es ir en línea como key= para sorted(), así no defines una función aparte solo para eso. En el momento en que se vuelve más larga que una expresión ordenada, escribo un def de verdad y me siento mejor por eso. Comprensiones de listas
La transformación más común en Python: tomar una secuencia, hacerle algo a cada elemento y obtener de vuelta una nueva lista. Una comprensión de lista hace esto en una sola línea legible: [expression for item in iterable]. También puedes agregar un filtro con if.
La forma larga:
numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
squares.append(n ** 2)La comprensión de lista:
squares = [n ** 2 for n in numbers]La estructura siempre es la misma: [expression for item in iterable].
scores = [87, 42, 96, 55, 71]
scaled = [s * 1.1 for s in scores] # aplicar un bono del 10%
as_grades = [f"{s}/100" for s in scores] # formatear cada uno[expression for item in iterable] toma una secuencia, le hace algo a cada elemento y te devuelve una lista nueva. Léela de izquierda a derecha y dice exactamente lo que hace. Esta fue la primera característica de Python que me hizo sentir que estaba escribiendo Python en vez de traducir desde otro lenguaje. Filtrar con una condición
Agrega una cláusula if para incluir solo los elementos que pasan una prueba. El resultado es una lista nueva con únicamente los elementos donde la condición es True.
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
evens = [n for n in numbers if n % 2 == 0] # [2, 4, 6, 8]
odds = [n for n in numbers if n % 2 != 0] # [1, 3, 5, 7]scores = [87, 42, 96, 55, 71, 38]
passing = [s for s in scores if s >= 60] # [87, 96, 71]
failing = [s for s in scores if s < 60] # [42, 55, 38]if condition al final para conservar solo los elementos que pasan la prueba: [x for x in data if x > 0]. Todo lo que resulte falso se queda fuera de la lista nueva. Es la misma comprensión que ya conoces, pero con un portero en la puerta. Comprensiones anidadas
Puedes anidar comprensiones para aplanar una lista de listas en una sola lista. Léela de izquierda a derecha: por cada fila, por cada elemento de esa fila, incluye el elemento.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [item for row in matrix for item in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]Léela de izquierda a derecha: por cada fila en matrix, por cada elemento en row, incluye item.
Las comprensiones anidadas pueden volverse confusas rápidamente. Si te toma más de un instante entenderlas, escribe los bucles de forma explícita.
for en una sola comprensión aplanan una lista de listas en una única lista plana: [item for row in matrix for item in row]. Léela de izquierda a derecha, primero el bucle externo, exactamente en el orden en que escribirías los bucles. Si tus ojos se traban al leerla, esa es tu señal para escribir los bucles reales en su lugar. Comprensiones de diccionarios
Las comprensiones de diccionarios construyen un diccionario en una sola expresión, con la misma idea que las comprensiones de listas: {key: value for item in iterable}. Agrega un filtro con if, igual que con las comprensiones de listas.
names = ["ana", "bruno", "carla"]
scores = [87, 74, 92]
score_map = {name: score for name, score in zip(names, scores)}
# {"ana": 87, "bruno": 74, "carla": 92}Con un filtro:
passing = {name: score for name, score in score_map.items() if score >= 80}
# {"ana": 87, "carla": 92}words = ["apple", "banana", "cherry"]
word_lens = {word: len(word) for word in words}
# {"apple": 5, "banana": 6, "cherry": 6}{key: value for item in iterable} construye un diccionario en una línea, con la misma forma que una comprensión de lista pero con dos puntos entre la clave y el valor. Combínala con .items() para remodelar un dict que ya tienes, o con zip() para unir dos listas en un solo mapeo. Agrega un if al final para conservar solo los pares que quieres. Comprensiones de conjuntos
Las comprensiones de conjuntos construyen un set en una sola expresión, con llaves y sin dos puntos. Como el resultado es un set, los duplicados se eliminan automáticamente.
words = ["apple", "banana", "cherry", "apple"]
unique = {w.lower() for w in words} # {"apple", "banana", "cherry"}Usa comprensiones de conjuntos cuando quieras valores únicos y no te importe el orden.
{expr for item in iterable} con llaves y sin dos puntos construye un set, y un set descarta duplicados gratis. Así que si tu tarea es "dame los únicos", esto lo hace en una línea. Eso sí, no cuentes con que vuelvan en un orden particular. Expresiones generadoras
Los generadores se parecen a las comprensiones de listas, pero con paréntesis en vez de corchetes. La diferencia clave: una comprensión de lista construye la lista completa en memoria de una sola vez. Un generador produce valores uno a la vez, solo cuando se necesitan. Para secuencias grandes, esto usa mucha menos memoria.
squares_gen = (n ** 2 for n in range(1000000))total = sum(n ** 2 for n in range(1000000)) # sum() consume el generadorAl pasar un generador directamente a una función como sum(), max(), min() o any(), puedes omitir los paréntesis adicionales:
total = sum(n ** 2 for n in range(1000)) # un solo par de paréntesis, no dosPara la mayoría del código cotidiano, las comprensiones de listas funcionan bien. Usa generadores cuando estés procesando conjuntos de datos grandes o datos en streaming, donde guardarlo todo en memoria sería un desperdicio.
sum() o max() y no construir la lista en absoluto. zip()
zip() empareja elementos de dos o más secuencias para que puedas recorrerlas en paralelo con un bucle. Se detiene en la secuencia más corta. Es la forma limpia de evitar manejar índices cuando dos listas se corresponden entre sí.
names = ["Ana", "Bruno", "Carla"]
scores = [87, 74, 92]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# Ana: 87
# Bruno: 74
# Carla: 92zip() se detiene en la secuencia más corta. Si tus secuencias pueden tener longitudes distintas, usa itertools.zip_longest() con un valor de relleno.
Para convertir de vuelta una lista de pares emparejados en dos listas separadas, usa zip(*pairs):
pairs = [("Ana", 87), ("Bruno", 74), ("Carla", 92)]
names, scores = zip(*pairs)
# names = ("Ana", "Bruno", "Carla")
# scores = (87, 74, 92)*pairs desempaqueta la lista en argumentos separados, así que zip(*pairs) se convierte en zip(("Ana", 87), ("Bruno", 74), ("Carla", 92)). El operador * se explica en el capítulo de Funciones.
zip() también es la forma limpia de iterar varias secuencias en paralelo sin manejar índices manualmente:
before = [10, 20, 30]
after = [15, 18, 35]
for b, a in zip(before, after):
change = a - b
print(f"{b} -> {a} ({'+' if change >= 0 else ''}{change})")zip() empareja dos o más secuencias para que puedas recorrerlas juntas, sin hacer malabares con índices. Se detiene en la más corta, así que las longitudes distintas pierden los sobrantes en silencio. Y zip(*pairs) lo hace en reversa, separando una lista de tuplas en listas independientes. map() y filter()
map() y filter() son herramientas más antiguas de estilo funcional que hacen lo mismo que las comprensiones. Las vas a encontrar en código antiguo, así que vale la pena saber qué significan. Prefiere las comprensiones para código nuevo; son más legibles para la mayoría de los desarrolladores de Python.
numbers = [1, 2, 3, 4, 5]
list(map(lambda x: x ** 2, numbers)) # [1, 4, 9, 16, 25]
list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]Prefiere las comprensiones; son más legibles para la mayoría de los desarrolladores de Python. Usa map() cuando tengas una función con nombre que ya existe:
strings = ["1", "2", "3"]
numbers = list(map(int, strings)) # [1, 2, 3] (más limpio que una comprensión aquí)map(func, iterable) ejecuta una función sobre cada elemento; filter(func, iterable) conserva solo los elementos donde la función devuelve verdadero. Son la forma más antigua de hacer lo que hacen las comprensiones, así que te las vas a encontrar en código ajeno. Para el tuyo, una comprensión se lee más clara para la mayoría de la gente. En la práctica
Filtra una lista de jugadores para quedarte con los puntajes aprobatorios, ordénalos por puntaje con sorted y un lambda, y luego imprímelos con posiciones enumeradas:
players = [
{"name": "Ana", "score": 87},
{"name": "Bruno", "score": 42},
{"name": "Carla", "score": 96},
{"name": "Diego", "score": 55},
]
passing = [p for p in players if p["score"] >= 60]
ranked = sorted(passing, key=lambda p: p["score"], reverse=True)
score_map = {p["name"]: p["score"] for p in ranked}
for i, (name, score) in enumerate(score_map.items(), start=1):
print(f"{i}. {name}: {score}")
