Skip to content
This page has been auto-translated and may contain errors.View in English

Lambdas y comprehensions

Estas tres características tienen algo en común: te permiten expresar ideas que de otra forma tomarían varias líneas en una sola expresión legible. Bien usadas, hacen el código más corto y claro. Mal usadas, lo vuelven ilegible. Este capítulo cubre cuándo recurrir a cada una y cuándo detenerse.

Las lambdas, los comprehensions y zip son tres herramientas que comprimen patrones comunes en expresiones. No son obligatorios, pero aparecen en todo el código Python y vale la pena reconocerlos y escribirlos con fluidez. El principio guía: úsalos cuando hagan más clara la intención, no solo más corto el código.

Las expresiones lambda crean objetos función anónimos en tiempo de ejecución. Los comprehensions se compilan a bytecode optimizado que construye colecciones sin un bucle for en el frame externo. Los generadores son perezosos: producen valores bajo demanda sin materializar la secuencia entera. zip devuelve un iterador de tuplas, consumiendo los iterables de entrada de forma perezosa. Los tres comparten la idea de expresar transformaciones como expresiones en vez de bucles imperativos.

Funciones lambda

Una lambda es una función sin nombre, de una sola expresión. La creas con la palabra clave lambda. Su verdadera utilidad es que puedes escribirla en línea, justo donde la necesitas, sin definir primero una función con nombre. Esto es lo que la hace útil con sorted().

Una lambda es una función anónima de una sola expresión. Puede tomar múltiples argumentos pero su cuerpo debe ser una única expresión, no una sentencia. Su uso principal es como argumento key= o callback en línea, donde un def completo añadiría una indirección innecesaria. Para cualquier cosa más compleja, usa def.

lambda args: expression se compila a un objeto código y crea un objeto función, idéntico a def excepto que no tiene nombre (aparece como <lambda> en los tracebacks), no puede contener sentencias, y no admite docstrings ni anotaciones. Las lambdas participan en closures: las variables libres se capturan del ámbito envolvente. El error común: lambda i: i dentro de un bucle captura i por referencia, no por valor; usa lambda i=i: i para fijar el valor en el momento de la creación.

python
double = lambda x: x * 2
double(5)   # 10

Eso es equivalente a:

python
def double(x):
    return x * 2

En la mayoría de los casos, usa def. Las lambdas tienen una ventaja real: puedes escribirlas en línea, justo donde las necesitas, sin nombrarlas. Esto es lo que las hace útiles con sorted(), map() y filter():

python
players = [("Sofía", 87), ("Mateo", 74), ("Camila", 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 una lambda, tendrías que definir una función con nombre solo para el argumento key=. La lambda mantiene la intención local y visible.

Las lambdas pueden tomar múltiples argumentos:

python
add = lambda a, b: a + b
add(3, 4)   # 7

Cuándo usar una lambda: solo cuando sea una expresión simple usada en un solo lugar. Si se vuelve compleja, o necesitas reutilizarla, escribe un def propio. Una lambda que abarca varios operadores o requiere condicionales suele ser una señal para cambiar a def.

Comprehensions de listas

La transformación más común en Python: tomar una secuencia, hacer algo con cada elemento y obtener una nueva lista. Una list comprehension hace esto en una sola línea legible: [expression for item in iterable]. También puedes añadir un filtro con if.

Las list comprehensions son un reemplazo conciso del patrón de construir-con-un-bucle. Se compilan a bytecode optimizado y son generalmente más rápidas que bucles for equivalentes con .append(). La estructura es [expression for item in iterable if condition]; la cláusula if es opcional.

Las list comprehensions se compilan a un bucle LIST_APPEND con bytecode dedicado, más rápido que llamadas repetidas a list.append() en un bucle a nivel de Python. Crean un nuevo ámbito en Python 3 (a diferencia de Python 2), por lo que la variable del bucle no se filtra. Los comprehensions anidados se ejecutan de izquierda a derecha y de arriba hacia abajo: [expr for x in a for y in b] equivale a un bucle for anidado con x como el bucle externo.

La forma larga:

python
numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
    squares.append(n ** 2)

La list comprehension:

python
squares = [n ** 2 for n in numbers]

La estructura siempre es la misma: [expression for item in iterable].

python
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

Filtrar con una condición

Añade una cláusula if para incluir solo los elementos que pasen una prueba. El resultado es una nueva lista con solo los elementos en los que la condición es True.

La cláusula if en un comprehension es un filtro, no un if/else. Se ejecuta una vez por elemento e incluye solo los elementos para los que la condición es verdadera. Para una transformación condicional (mapear un valor a otro según una condición), usa una expresión ternaria dentro de la expresión principal.

El filtro if se distingue de una expresión condicional en la salida. [x for x in data if x > 0] filtra. [x if x > 0 else 0 for x in data] mapea (limita a cero). Puedes combinar ambos: [x * 2 for x in data if x > 0]. Múltiples cláusulas if se encadenan con un and implícito.

python
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]
python
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]

Comprehensions anidados

Puedes anidar comprehensions para aplanar una lista de listas en una sola lista. Léelo de izquierda a derecha: por cada fila, por cada elemento en esa fila, incluye el elemento.

Los comprehensions anidados se ejecutan de izquierda a derecha. La primera cláusula for es el bucle externo, la segunda es el interno. Producen un único resultado plano, no una estructura 2D. Si el comprehension es difícil de leer de un vistazo, escribe los bucles explícitamente.

Los comprehensions anidados se ejecutan como bucles anidados con el primer for como el más externo. El ámbito de cada variable del bucle está disponible para las cláusulas siguientes. Para productos cartesianos, itertools.product suele ser más claro. La regla clave de legibilidad: si analizar el comprehension toma más de un segundo, la forma explícita con bucle es mejor documentación.

python
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éelo de izquierda a derecha: por cada fila en matrix, por cada item en row, incluye item.

Los comprehensions anidados pueden volverse confusos rápidamente. Si toma más de un momento analizarlo, escribe los bucles explícitamente.

Dict comprehensions

Los dict comprehensions construyen un diccionario en una sola expresión, usando la misma idea que las list comprehensions: {key: value for item in iterable}. Añade un filtro con if igual que en las list comprehensions.

Los dict comprehensions crean un nuevo dict a partir de cualquier iterable que produzca pares clave-valor. La sintaxis es {key_expr: val_expr for item in iterable if condition}. Las claves duplicadas del bucle usan el último valor, silenciosamente. .items() en un dict existente es la fuente iterable más común para dict comprehensions.

Los dict comprehensions se compilan a bytecode MAP_ADD dedicado, análogo a LIST_APPEND para list comprehensions. Crean un nuevo ámbito en Python 3. Las expresiones de clave deben producir valores hashables; si una expresión de clave produce un duplicado, el valor posterior gana silenciosamente. Para semántica de fusión ordenada, el operador | (Python 3.9+) es más limpio que un comprehension.

python
names  = ["sofía", "mateo", "camila"]
scores = [87, 74, 92]

score_map = {name: score for name, score in zip(names, scores)}
# {"sofía": 87, "mateo": 74, "camila": 92}

Con un filtro:

python
passing = {name: score for name, score in score_map.items() if score >= 80}
# {"sofía": 87, "camila": 92}
python
words     = ["apple", "banana", "cherry"]
word_lens = {word: len(word) for word in words}
# {"apple": 5, "banana": 6, "cherry": 6}

Set comprehensions

Los set comprehensions construyen un conjunto en una sola expresión, con llaves y sin dos puntos. Como el resultado es un conjunto, los duplicados se eliminan automáticamente.

Los set comprehensions usan {expression for item in iterable} y producen un set. Eliminan duplicados automáticamente. Úsalos cuando necesitas una colección única construida a partir de una transformación, donde el orden no importa.

Los set comprehensions se compilan a bytecode SET_ADD. El resultado es un conjunto desordenado: los valores duplicados de la expresión se fusionan silenciosamente. Los set comprehensions son menos comunes que los de listas o dicts pero son la forma limpia de producir una transformación deduplicada en una sola expresión.

python
words   = ["apple", "banana", "cherry", "apple"]
unique  = {w.lower() for w in words}    # {"apple", "banana", "cherry"}

Usa set comprehensions cuando quieras valores únicos y no te importe el orden.

Expresiones generadoras

Los generadores se parecen a las list comprehensions con paréntesis en lugar de corchetes. La diferencia clave: una list comprehension construye toda la lista en memoria de una vez. Un generador produce valores uno a la vez, solo cuando se necesitan. Para secuencias grandes, esto usa muchísima menos memoria.

Una expresión generadora produce un iterador, no una colección. Calcula los valores de forma perezosa: el siguiente valor solo se produce cuando se solicita. Esto es más valioso cuando el resultado se consume inmediatamente por una función como sum(), max() o any(), así que no tiene sentido construir la lista completa primero.

Las expresiones generadoras se compilan a un objeto código y devuelven un objeto generador. Los valores se producen de forma perezosa vía __next__, haciendo que el uso de memoria sea O(1) sin importar el tamaño de la entrada. Participan en el protocolo de iteradores y pueden encadenarse. Cuando se pasan directamente a una función que acepta un iterable, se pueden omitir los paréntesis externos. Los generadores no pueden reutilizarse una vez agotados; si necesitas iterar varias veces, materialízalos en una lista.

python
squares_gen = (n ** 2 for n in range(1000000))
python
total = sum(n ** 2 for n in range(1000000))   # sum() consume el generador

Cuando pasas un generador directamente a una función como sum(), max(), min() o any(), puedes omitir los paréntesis adicionales:

python
total = sum(n ** 2 for n in range(1000))   # un par de paréntesis, no dos

Para la mayor parte del código cotidiano, las list comprehensions están bien. Usa generadores cuando estés procesando grandes conjuntos de datos o datos en streaming donde mantener todo en memoria sería un desperdicio.

zip()

zip() empareja elementos de dos o más secuencias para que puedas recorrerlas en paralelo. Se detiene en la secuencia más corta. Es la forma limpia de evitar gestionar índices cuando dos listas se corresponden entre sí.

zip() devuelve un iterador perezoso de tuplas, consumiendo sus iterables de entrada al mismo paso. Se detiene en la entrada más corta: las secuencias más largas se truncan silenciosamente. Para secuencias que pueden diferir en longitud, itertools.zip_longest() rellena las más cortas con un valor especificado.

zip() devuelve un objeto zip, un iterador perezoso que llama a next() en cada iterador de entrada simultáneamente. Se detiene cuando cualquier iterador lanza StopIteration. Todas las entradas se consumen perezosamente: zip() en sí mismo asigna memoria O(1) sin importar el tamaño de la entrada. zip(*iterable) es la operación de transposición estándar; * desempaqueta el iterable externo en argumentos separados.

python
names  = ["Sofía", "Mateo", "Camila"]
scores = [87, 74, 92]

for name, score in zip(names, scores):
    print(f"{name}: {score}")
# Sofía: 87
# Mateo: 74
# Camila: 92

zip() se detiene en la secuencia más corta. Si tus secuencias pueden tener longitudes diferentes, usa itertools.zip_longest() con un valor de relleno.

Para convertir de vuelta una lista zippeada de pares en dos listas separadas, usa zip(*pairs):

python
pairs  = [("Sofía", 87), ("Mateo", 74), ("Camila", 92)]
names, scores = zip(*pairs)
# names = ("Sofía", "Mateo", "Camila")
# scores = (87, 74, 92)

¿Qué hace * aquí?

*pairs desempaqueta la lista en argumentos separados: zip(*pairs) se convierte en zip(("Sofía", 87), ("Mateo", 74), ("Camila", 92)). El operador * se cubre en el capítulo Funciones.

zip() también es la forma limpia de iterar varias secuencias en paralelo sin gestionar índices manualmente:

python
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})")

map() y filter()

map() y filter() son herramientas de estilo funcional más antiguas que hacen lo mismo que los comprehensions. Las verás en código antiguo, así que vale la pena saber qué significan. Prefiere los comprehensions para código nuevo; son más legibles para la mayoría de los desarrolladores de Python.

map(func, iterable) devuelve un iterador perezoso que aplica func a cada elemento. filter(func, iterable) devuelve un iterador perezoso de los elementos para los que func es verdadero. Ambos son anteriores a los comprehensions. Prefiere los comprehensions en código nuevo; usa map() cuando ya tengas una función con nombre que hace lo que necesitas.

map() y filter() devuelven iteradores perezosos (no listas) en Python 3. map(f, it) equivale a (f(x) for x in it). filter(pred, it) equivale a (x for x in it if pred(x)). Para funciones con nombre, list(map(int, strings)) es idiomático porque se lee como "mapear int sobre strings"; el comprehension equivalente [int(s) for s in strings] es igualmente válido.

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 los comprehensions; son más legibles para la mayoría de los desarrolladores de Python. Usa map() cuando ya tengas una función con nombre que existe:

python
strings = ["1", "2", "3"]
numbers = list(map(int, strings))   # [1, 2, 3] (más limpio que un comprehension aquí)

En la práctica

Filtrar una lista de jugadores por puntajes aprobatorios, clasificar por puntaje con sorted y una lambda, luego imprimir con posiciones enumeradas:

python
players = [
    {"name": "Sofía",  "score": 87},
    {"name": "Mateo",  "score": 42},
    {"name": "Camila", "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}")

Filtrar una lista de usuarios por admins activos, construir un diccionario de búsqueda id-a-nombre, y recolectar nombres ordenados en una pasada cada uno:

python
raw_users = [
    {"id": 1, "name": "Sofía",  "role": "admin", "active": True},
    {"id": 2, "name": "Mateo",  "role": "user",  "active": False},
    {"id": 3, "name": "Camila", "role": "admin", "active": True},
    {"id": 4, "name": "Diego",  "role": "user",  "active": True},
]

active_admins = [u for u in raw_users if u["active"] and u["role"] == "admin"]
id_map        = {u["id"]: u["name"] for u in raw_users}
names         = sorted(u["name"] for u in raw_users if u["active"])

print(f"Active admins: {[u['name'] for u in active_admins]}")
print(f"All active: {names}")

Emparejar nombres de características con puntajes de importancia usando zip, construir un dict comprehension, ordenar con una lambda, y normalizar valores en un segundo comprehension:

python
feature_names = ["age", "income", "score", "tenure"]
importances   = [0.12, 0.34, 0.28, 0.26]

feat_dict = {f: i for f, i in zip(feature_names, importances)}
top_feats = sorted(feat_dict.items(), key=lambda x: x[1], reverse=True)[:2]

print("Top 2 features:")
for name, score in top_feats:
    print(f"  {name}: {score:.2f}")

# Normalizar para que sumen 1.0 (los valores ya suman 1 aquí, pero se muestra como patrón)
total      = sum(feat_dict.values())
normalised = {k: round(v / total, 4) for k, v in feat_dict.items()}
print(f"Normalised: {normalised}")

zip empareja las dos listas sin construir tuplas intermedias. El dict comprehension construye el mapeo en una sola expresión. La lambda del sort evita una función key con nombre. El comprehension de normalización transforma valores sin mutar el dict original.