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

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.

Los lambdas, las comprensiones y zip son tres herramientas que condensan patrones comunes en expresiones. No son obligatorias, pero aparecen por todo el código Python y vale la pena reconocerlas y escribirlas con fluidez. El principio que debe guiarte: úsalas cuando hagan más clara la intención, no solo cuando acorten el código.

Todas estas herramientas expresan una transformación como una expresión (un fragmento de código que se evalúa como un valor) en lugar de un bucle imperativo que construye un resultado paso a paso. Los lambdas te dan una pequeña función sin nombre, escrita en línea. Las comprensiones construyen una colección en una sola pasada. Los generadores son perezosos (producen valores a demanda en vez de todos a la vez), así que mantienen en memoria un solo elemento en lugar de la secuencia completa. zip recorre varias secuencias al mismo tiempo, avanzando en paralelo. El beneficio es un código que se lee como "lo que quiero" en vez de "cómo armarlo", siempre que te detengas antes de que la línea deje de ser legible.

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().

Un lambda es una función anónima de una sola expresión. Puede recibir varios argumentos, pero su cuerpo debe ser una sola expresión, no una instrucción. Su uso principal es como argumento key= en línea o como callback, en casos donde un def completo agregaría una indirección innecesaria. Para cualquier cosa más compleja, usa def.

Un lambda es el mismo tipo de objeto función que produce def, con tres límites: no tiene nombre (aparece como <lambda> en un traceback, el reporte de error que Python imprime cuando algo lanza una excepción), su cuerpo es una sola expresión, así que no puede contener instrucciones, y no lleva docstring. La trampa que cuesta tiempo real de depuración es el enlace tardío (late binding) en un closure (una función que captura una variable del entorno que la rodea). Un lambda creado dentro de un bucle captura la variable del bucle por referencia, así que cada lambda ve el valor final de la variable, no el valor que tenía cuando se creó el lambda:

python
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])        # [2, 2, 2], no [0, 1, 2]

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])        # [0, 1, 2]: i=i fija el valor en ese momento

El truco de i=i fija el valor actual como argumento por defecto en el momento de la creación. Recurre a él cada vez que construyas funciones invocables dentro de un bucle.

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

Esto es equivalente a:

python
def double(x):
    return x * 2

Para 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():

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

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

Cuá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.

JunoFunciones lambda Un lambda es una función minúscula de una sola línea sin nombre, escrita con la palabra clave 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.
JunoFunciones lambda Un lambda es una función anónima de una sola expresión, útil como key= en línea o como callback, en casos donde un def con nombre solo agregaría ruido. El cuerpo es una sola expresión, no se permiten instrucciones. Para algo más elaborado, recurre a def.
JunoFunciones lambda Es el mismo objeto función que produce def, sin nombre, sin instrucciones y sin docstring, así que mantenlos como key= de una sola línea. La trampa es el enlace tardío: un lambda creado dentro de un bucle lee el valor final de la variable del bucle, así que escribe lambda i=i: i para fijarlo. A todo el mundo lo agarra esa una vez.

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.

Las comprensiones de listas son un remplazo conciso del patrón de construir con un bucle. Por lo general son más rápidas que el for equivalente con .append(), porque Python no llama a un método en cada vuelta. La estructura es [expression for item in iterable if condition]; la cláusula if es opcional.

Una comprensión es notablemente más rápida que el mismo bucle llamando a .append() en cada vuelta, así que es la forma idiomática de construir una lista a partir de una transformación. Además corre en su propio ámbito (scope, la región donde un nombre es visible), lo que significa que la variable del bucle no se filtra al código que la rodea: después de [n ** 2 for n in numbers], el nombre n no existe fuera de la comprensión. La trampa a vigilar es recurrir a una comprensión cuando el cuerpo hace trabajo real. En cuanto la expresión crece más allá de una transformación limpia, o quieres agregar logging o un bloque try, el bucle explícito es la mejor herramienta, porque una comprensión solo puede contener una expresión, nunca una instrucción.

La forma larga:

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

La comprensión de lista:

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
JunoComprensiones de listas[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.
JunoComprensiones de listas[expression for item in iterable] remplaza el patrón de construir con un bucle y .append(), y de paso se lee más limpio. Produce una lista nueva y no muta nada. Mantén la expresión simple, o pierdes la legibilidad que ganaste.
JunoComprensiones de listas Más rápida que el bucle con .append() y corre en su propio ámbito, así que la variable del bucle nunca se filtra hacia afuera. La línea que hay que sostener: una comprensión recibe una expresión, no una instrucción, así que en el momento en que quieras un bloque try o una línea de log, el bucle explícito gana.

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.

La cláusula if en una comprensión es un filtro, no un if/else. Se ejecuta una vez por cada elemento e incluye solo aquellos 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.

La posición del if decide si filtras o transformas, y confundir ambos casos es una causa común de resultados con el largo equivocado. Un if al final filtra: [x for x in data if x > 0] descarta elementos. Una condicional al principio mapea: [x if x > 0 else 0 for x in data] conserva todos los elementos y reescribe los que fallan (aquí, llevando los negativos a cero). Puedes combinar ambos en una sola comprensión, [x * 2 for x in data if x > 0], y encadenar varias cláusulas if al final, que se combinan como si fueran un and.

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]
JunoFiltrar con una condición Agrega 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.
JunoFiltrar con una condición Un if al final filtra, conservando solo los elementos donde la condición es verdadera. Eso es distinto de una condicional dentro de la expresión: [x if x > 0 else 0 for x in data] reescribe valores en lugar de descartarlos. Ten claro cuál quieres antes de escribirlo.
JunoFiltrar con una condición Un if al final filtra y cambia el largo; una condicional al principio mapea y lo conserva. Si confundes los dos, terminas enviando un resultado con el tamaño equivocado. Varias cláusulas if encadenadas al final se combinan con and.

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.

Las comprensiones anidadas se ejecutan de izquierda a derecha. La primera cláusula for es el bucle externo, la segunda es el interno. Producen un solo resultado plano, no una estructura bidimensional. Si la comprensión es difícil de leer de un vistazo, escribe los bucles de forma explícita.

El orden de las cláusulas se lee en el mismo orden que los bucles anidados equivalentes: el primer for es el bucle externo, el segundo es el interno, y una cláusula posterior puede usar un nombre vinculado por una anterior. Donde esto realmente vale la pena es al aplanar; donde perjudica es en una verdadera grilla de combinaciones (cada par posible entre dos secuencias), para lo cual itertools.product se lee mucho más claro que un doble for. La regla que mantiene sano un código: si entender la comprensión te toma más de un segundo, el bucle explícito es mejor documentación, así que escribe eso en su lugar.

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é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.

JunoComprensiones anidadas Dos cláusulas 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.
JunoComprensiones anidadas Apila dos cláusulas for y la primera es el bucle externo, la segunda el interno: [item for row in matrix for item in row] aplana una lista 2D en una sola. El resultado es plano, no una grilla. Cuando deja de ser legible de un vistazo, los bucles explícitos son mejores.
JunoComprensiones anidadas El primer for es el bucle externo, y una cláusula posterior puede usar un nombre de una anterior. Excelente para aplanar, deficiente para combinaciones de todos los pares, donde itertools.product se lee más limpio. Si entenderla toma más de un segundo, el bucle es la mejor documentación.

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.

Las comprensiones de diccionarios crean un dict nuevo 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 que surgen del bucle usan el último valor, sin avisar. .items() sobre un dict existente es la fuente iterable más común para las comprensiones de diccionarios.

Dos cosas suelen morder en código real. Primero, toda clave debe ser hasheable (un valor que Python puede reducir a un número fijo de búsqueda, lo cual implica que nunca cambia, así que strings, números y tuplas califican, pero listas y dicts no). Una expresión de clave que produce una lista lanza TypeError. Segundo, las claves duplicadas no generan error: si el bucle produce la misma clave dos veces, el valor posterior sobrescribe al anterior sin avisar, lo cual es una fuente silenciosa de pérdida de datos cuando construyes un dict a partir de una fuente que en realidad no es única. Cuando solo quieres combinar dos dicts existentes, el operador | (la forma de fusión de dicts, disponible desde Python 3.9) expresa la intención con más claridad que una comprensión.

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

python
passing = {name: score for name, score in score_map.items() if score >= 80}
# {"ana": 87, "carla": 92}
python
words = ["apple", "banana", "cherry"]
word_lens = {word: len(word) for word in words}
# {"apple": 5, "banana": 6, "cherry": 6}
JunoComprensiones de diccionarios{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.
JunoComprensiones de diccionarios{key: value for item in iterable} crea un dict a partir de cualquier fuente de pares. Las dos fuentes más comunes en el día a día son .items() sobre un dict existente y zip() sobre dos listas. Cuidado con las claves duplicadas: gana la última, sin avisar.
JunoComprensiones de diccionarios Las claves deben ser hasheables (nada de listas), y las claves duplicadas no generan error, el valor posterior sobrescribe sin avisar, lo cual se traga datos cuando tu fuente no es única. Así que verifica la unicidad antes de confiar en el conteo. Para fusionar dos dicts, | lo expresa de forma más directa que una comprensión.

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.

Las comprensiones de conjuntos usan {expression for item in iterable} y producen un set. Eliminan duplicados automáticamente. Úsalas cuando necesites una colección única construida a partir de una transformación, en casos donde el orden no importa.

El resultado es un set, así que se derivan dos cosas que la versión de lista no te da. Los valores duplicados de la expresión se colapsan en uno solo automáticamente, que es justamente el objetivo: es la forma más limpia de eliminar duplicados de una transformación en una sola expresión. El costo es que un set no tiene orden, así que no puedes confiar en que los elementos salgan en una secuencia particular. Los miembros también deben ser hasheables, la misma regla que aplica a las claves de un dict: un valor que Python puede reducir a un número fijo de búsqueda, lo cual descarta a las listas.

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

JunoComprensiones de conjuntos{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.
JunoComprensiones de conjuntos{expr for item in iterable} produce un set, eliminando duplicados sobre la marcha. Recurre a ella cuando quieras una colección única a partir de una transformación y el orden no importe. Llaves sin dos puntos, eso es lo único que la separa de una comprensión de diccionario.
JunoComprensiones de conjuntos El set descarta duplicados automáticamente, que es la razón principal para elegirlo sobre una comprensión de lista. A cambio, no hay orden en el que confiar, y los miembros deben ser hasheables, así que nada de listas por dentro. El mejor caso: eliminar duplicados de una transformación en una sola expresión.

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.

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 es consumido de inmediato por una función como sum(), max() o any(), así que no tiene sentido construir la lista completa antes.

Un generador produce valores de forma perezosa (uno a la vez, solo cuando algo pide el siguiente), así que su uso de memoria se mantiene constante sin importar qué tan grande sea la entrada: nunca guarda más que el valor actual, mientras que una comprensión de lista guarda todos los elementos a la vez. Eso lo convierte en la opción correcta para una fuente grande o de streaming que alimenta directamente a sum(), max() o any(). El modo de falla que hay que recordar es que un generador se usa una sola vez: en cuanto algo lo recorre hasta el final, queda agotado y no produce nada más, así que un segundo bucle sobre el mismo generador se ejecuta cero veces y no da ningún error. Si necesitas recorrer los datos dos veces, construye una lista una sola vez y itera sobre esa.

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

Al pasar 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 solo par de paréntesis, no dos

Para 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.

JunoExpresiones generadoras Un generador se parece a una comprensión de lista con paréntesis en vez de corchetes, pero produce los valores de uno en uno en lugar de construir toda la lista de entrada. Para una secuencia gigante eso ahorra un montón de memoria. El caso ideal: pasarlo directo a sum() o max() y no construir la lista en absoluto.
JunoExpresiones generadoras Una expresión generadora devuelve un iterador perezoso: calcula el siguiente valor solo cuando se le pide, así que nada construye la lista completa en memoria. Es mejor cuando el resultado alimenta directamente a sum(), max() o any(). Recuerda que se usa una sola vez, una vez consumido queda vacío.
JunoExpresiones generadoras Perezoso y con un uso de memoria constante sin importar qué tan grande sea la entrada, así que es la herramienta correcta para una fuente grande o de streaming que alimenta a sum() o any(). La trampa: se usa una sola vez, y un segundo bucle sobre un generador agotado se ejecuta cero veces sin dar ningún error. ¿Necesitas recorrerlo dos veces? Construye una lista una sola vez.

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í.

zip() devuelve un iterador perezoso de tuplas, consumiendo sus iterables de entrada en paralelo. Se detiene en la entrada más corta: las secuencias más largas se truncan sin avisar. Para secuencias que pueden tener longitudes distintas, itertools.zip_longest() rellena las más cortas con un valor especificado.

zip() recorre sus entradas en paralelo y se detiene en el momento en que la más corta se agota. Esa truncación es la trampa a tener en cuenta al diseñar: si emparejas una lista de 1000 registros con una de 999, pierdes el último registro sin avisar, sin error ni advertencia. Cuando las longitudes deben coincidir, zip(seq_a, seq_b, strict=True) (disponible desde Python 3.10) lanza una excepción si difieren en vez de perder datos, y itertools.zip_longest rellena los huecos cuando se permite que difieran. El otro truco que vale la pena conocer: zip(*rows) transpone, convirtiendo una lista de filas en una lista de columnas, porque * desempaqueta la lista externa en argumentos separados.

python
names = ["Ana", "Bruno", "Carla"]
scores = [87, 74, 92]

for name, score in zip(names, scores):
    print(f"{name}: {score}")
# Ana: 87
# Bruno: 74
# Carla: 92

zip() 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):

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

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})")
Junozip()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.
Junozip()zip() devuelve un iterador perezoso de tuplas, avanzando por sus entradas en paralelo. Se detiene en la más corta, así que las secuencias más largas se truncan sin hacer ruido. itertools.zip_longest() rellena los huecos cuando las longitudes legítimamente difieren, y zip(*pairs) desempareja.
Junozip() Perezoso, en paralelo, y se detiene en la entrada más corta, lo cual pierde datos en silencio cuando las longitudes debían coincidir. Pasa strict=True para que una discrepancia lance una excepción en su lugar. zip(*rows) es tu transposición, columnas a partir de filas en una sola llamada.

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.

map(func, iterable) devuelve un iterador perezoso que aplica func a cada elemento. filter(func, iterable) devuelve un iterador perezoso con los elementos para los que func es verdadera. Ambas existen desde antes de las comprensiones. Prefiere las comprensiones en código nuevo; usa map() cuando ya tengas una función con nombre que hace lo que necesitas.

Ambas devuelven iteradores perezosos, no listas, así que las envuelves en list() cuando quieres los valores de inmediato. Corresponden uno a uno con comprensiones que ya conoces: map(f, it) equivale a (f(x) for x in it), y filter(pred, it) equivale a (x for x in it if pred(x)). Esa equivalencia es la regla de decisión. Con un lambda en línea, la comprensión se lee mejor y es la opción moderna por defecto. Con una función con nombre que ya hace el trabajo, list(map(int, strings)) se lee como "aplicar int sobre strings" y es la opción más limpia, así que la única decisión real es si ya tienes una función a la mano.

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:

python
strings = ["1", "2", "3"]
numbers = list(map(int, strings))   # [1, 2, 3] (más limpio que una comprensión aquí)
Junomap() y filter()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.
Junomap() y filter()map() transforma cada elemento, filter() conserva los verdaderos, ambas perezosas, así que envuélvelas en list() para ver los resultados. Las comprensiones son la opción por defecto para código nuevo. El único caso en que map() gana es cuando ya tienes una función con nombre: map(int, strings) se lee más limpio que la comprensión.
Junomap() y filter()map(f, it) es (f(x) for x in it) y filter(pred, it) es (x for x in it if pred(x)), ambas perezosas. Así que la decisión es puramente de legibilidad: con un lambda en línea, gana la comprensión; con una función ya nombrada, gana map(int, strings).

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:

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

Filtra una lista de usuarios para quedarte con los administradores activos, construye un dict de búsqueda de id a nombre, y recolecta nombres ordenados, todo en una sola pasada cada uno:

python
raw_users = [
    {"id": 1, "name": "Ana", "role": "admin", "active": True},
    {"id": 2, "name": "Bruno", "role": "user", "active": False},
    {"id": 3, "name": "Carla", "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"Administradores activos: {[u['name'] for u in active_admins]}")
print(f"Todos los activos: {names}")

Empareja nombres de características con puntajes de importancia usando zip, construye una comprensión de diccionario, ordénalos con un lambda, y normaliza los valores en una segunda comprensión:

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("Las 2 características principales:")
for name, score in top_feats:
    print(f"  {name}: {score:.2f}")

# Normalizar para que sumen 1.0 (aquí ya suman 1, 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"Normalizado: {normalised}")

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