Listas por comprensión y generadores
En resumen: Una lista por comprensión hace una lista nueva a partir de otra en una sola línea, con una condición y una transformación, y un generador da los valores de uno en uno.
Problema
Un informe con un bucle
Ahora los pedidos también tienen una ciudad:
class Order: def __init__(self, customer, city, total): self.customer = customer self.city = city self.total = totalorders = [ Order("María", "Madrid", 75.50), Order("Jorge", "Valencia", 12.90), Order("Elena", "Madrid", 40.00), Order("Diego", "Granada", 120.00), Order("Ana", "Valencia", 64.00),]Quieres los clientes con pedidos de al menos 50 €, en orden alfabético:
result = []for o in orders: if o.total >= 50: result.append(o.customer)result.sort()print(result)['Ana', 'Diego', 'María']Seis líneas, y la pregunta es una sola. El código dice cómo: crea una lista vacía, recorre, comprueba, añade, ordena. Para entender qué hace, tienes que ejecutarlo en tu cabeza. Añade «solo los tres primeros» o «sin repetidos», y el bucle crecerá.
La idea
Una lista por comprensión: qué, no cómo
El mismo informe, en una línea:
result = sorted([o.customer for o in orders if o.total >= 50])print(result)['Ana', 'Diego', 'María'][o.customer for o in orders if o.total >= 50] se llama lista por comprensión. Se lee casi como una frase: «el cliente de cada pedido o de orders, si su total es al menos 50». Las partes son:
for o in orders: de dónde vienen los elementos;if o.total >= 50: cuáles se quedan. Es una condición, como enfilter. También se puede omitir;o.customeral principio: en qué se convierte cada elemento, como enmap;- los corchetes: el resultado es una lista nueva.
sorted(...) ordena y también devuelve una lista nueva. La lista orders no cambia.
Más precisamente, para curiosos
- Los mismos pasos existen también como funciones con lambdas de la lección anterior:
filterymap. El resultado es el mismo, pero se lee de dentro hacia fuera, y por eso en Python normalmente se escribe una lista por comprensión:
result = sorted(map(lambda o: o.customer, filter(lambda o: o.total >= 50, orders)))print(result)['Ana', 'Diego', 'María']- Si la expresión es el único parámetro de una función, se pueden omitir los corchetes:
sorted(o.customer for o in orders if o.total >= 50)da lo mismo. Entonces la expresión es un generador. De él trata la siguiente tarjeta.
Míralo
Qué pasa el filtro
prices = [12, 75, 40, 130, 58]expensive = (p for p in prices if p >= 50)labels = (f"{p} €" for p in expensive)print(list(labels))Paso a paso, en palabras
- Cinco precios. Los dos generadores solo preparan el camino: todavía no ha pasado nada.
- 12 no pasa if p >= 50 y se queda fuera. Nunca llega a convertirse en texto.
- 75 pasa if, f"{p} €" lo convierte en el texto «75 €», list() lo reúne.
- 40 también se queda fuera. Cada elemento recorre todo el camino antes de que salga el siguiente.
- 130 y 58 pasan. La lista prices no cambia.
- El resultado es una lista nueva con tres cadenas.
Los cinco precios avanzan de uno en uno. El 12 llega a la comprobación if p >= 50 y se queda fuera. El 75 pasa, el segundo generador lo convierte en el texto «75 €» y list() lo recoge. Después el 40 se queda fuera, y el 130 y el 58 pasan.
Cada elemento hace todo el recorrido antes de que empiece el siguiente. Y nada empieza hasta que list() pide los valores.
Más precisamente, para curiosos
Esto se llama evaluación perezosa. El generador solo anota lo que hay que hacer. Míralo con un print en cada paso:
def check(p): print("filter", p) return p >= 50
def double(p): print("map", p) return p * 2
stream = (double(p) for p in [12, 75, 40] if check(p))print("El generador está listo, todavía no pasó nada por él.")print(list(stream))El generador está listo, todavía no pasó nada por él.filter 12filter 75map 75filter 40[150]- La línea «El generador está listo» se imprime antes de que un solo elemento haya pasado por el generador.
map 75llega justo después defilter 75: los elementos no esperan a que todos pasen la comprobación.- Con corchetes, la misma expresión es una lista y se calcula enseguida, en la línea donde está escrita:
def check(p): print("filter", p) return p >= 50
def double(p): print("map", p) return p * 2
now = [double(p) for p in [12, 75, 40] if check(p)]print("La lista está lista.")print(now)filter 12filter 75map 75filter 40La lista está lista.[150]- Por eso puedes tomar solo los primeros valores de un generador sin comprobar el resto.
islicedel móduloitertoolsse detiene en cuanto ha tomado tantos como necesita:
from itertools import islice
def check(p): print("filter", p) return p >= 50
stream = (p for p in [75, 130, 58, 12, 99] if check(p))print(list(islice(stream, 2)))filter 75filter 130[75, 130]La idea
Suma, cantidad, media
El resultado puede ser un número en lugar de una lista. La suma de los números pares:
numbers = [1, 2, 3, 4, 5]total = sum(n for n in numbers if n % 2 == 0)print(f"Suma de los números pares: {total}")Suma de los números pares: 6sum suma los números que recibe. El generador que lleva dentro no tiene paréntesis extra, porque es su único parámetro.Lo mismo para los pedidos:
big = len([o for o in orders if o.total >= 50])revenue = sum(o.total for o in orders)average = revenue / len(orders) if orders else 0max_total = max((o.total for o in orders), default=0)
print(f"Pedidos de desde 50 €: {big}")print(f"Ingresos: {revenue:.2f} €")print(f"Media: {average:.2f} €")print(f"Mayor: {max_total:.2f} €")Pedidos de desde 50 €: 3Ingresos: 312.40 €Media: 62.48 €Mayor: 120.00 €lenda la cantidad. Aquí contamos los elementos de una lista por comprensión.- La media es la suma dividida entre la cantidad. Si no hay pedidos, dividir entre 0 detiene el programa, de ahí el
if orders else 0. maxde una lista vacía se detiene con unValueError.default=0dice qué debe ser si no hay valor. Esa es la siguiente idea.
Más precisamente, para curiosos
Con una lista vacía:
prices = []print(sum(prices))print(max(prices, default=0))try: print(max(prices))except ValueError as e: print("ValueError:", e)try: print(sum(prices) / len(prices))except ZeroDivisionError as e: print("ZeroDivisionError:", e)00ValueError: max() iterable argument is emptyZeroDivisionError: division by zeroLa idea
Ordenar, recortar, sin repetidos
ratings = [5, 3, 5, 4, 2, 4, 5]print(sorted(set(ratings)))
top = [o.customer for o in sorted(orders, key=lambda o: o.total, reverse=True)[:3]]print(top)
for i in range(1, 4): print(f"Mesa {i}")[2, 3, 4, 5]['Diego', 'María', 'Ana']Mesa 1Mesa 2Mesa 3set(...)quita los repetidos: en un conjunto cada valor está una sola vez. El orden en un conjunto no está garantizado, por esosortedva después.sorted(orders, key=..., reverse=True)ordena empezando por el pedido más grande, conkeyde la lección anterior.[:3]toma los tres primeros elementos. Es una rebanada (slice): desde el principio hasta el índice 3, sin incluirlo. Junto consorted, eso es un «top 3».range(1, 4)da los números 1, 2, 3. El final no se incluye.
Más precisamente, para curiosos
Si necesitas quitar los repetidos pero conservar el orden de primera aparición, dict.fromkeys crea un diccionario con las claves en el orden en que se añadieron:
ratings = [5, 3, 5, 4, 2, 4, 5]print(list(dict.fromkeys(ratings)))[5, 3, 4, 2]Pruébalo
¿Qué llega al final?
¿Qué imprimirá el programa?
drinks = ["café", "té", "café", "vino", "té", "jugo"]result = [d.upper() for d in sorted(set(drinks))[:3]]print(result)set quita los repetidos: café, té, vino, jugo. sorted los ordena alfabéticamente: café, jugo, té, vino. [:3] se queda con los tres primeros, y d.upper() los pasa a mayúsculas.La idea
None: un resultado que puede faltar
¿Cuál es el pedido más grande? Si la lista está vacía, no hay respuesta. Entonces el resultado es None: «nada». Pero quien llama tiene que acordarse de comprobarlo. Si se le olvida, el programa se detiene con un AttributeError en algún sitio más adelante.
Java tiene para esto Optional: una caja que o bien contiene un valor, o bien está vacía. Python no tiene una caja así. Un valor que falta es None, y comprobarlo es cosa tuya, en el lugar donde se usa el resultado:
biggest = max(orders, key=lambda o: o.total, default=None)if biggest is not None: print(f"Mayor: {biggest.customer}")
huge = next((o for o in orders if o.total > 500), None)print(huge is not None)print(huge.customer if huge is not None else "no existe ese pedido")Mayor: DiegoFalseno existe ese pedidomax(..., default=None)devuelveNoneen lugar de detenerse si la lista está vacía.next(generador, None)da el primer valor del generador, oNonesi no hay ninguno. Esto es «el primer pedido de más de 500 €», sin comprobar el resto.is not Nonecomprueba si hay un valor. ConNonese compara usandois, no==: es lo que pide PEP 8.x if condición else yda el valor de reserva si no hay valor.
Tus propias funciones también devuelven None cuando no tienen respuesta:
def nickname(user): # Devuelve el apodo, o None si la persona no tiene. return "Mimi" if user == "maria" else Nonemaria = nickname("maria")if maria is not None: print(f"Apodo: {maria}")
jorge = nickname("jorge")print(jorge if jorge is not None else "sin apodo")Apodo: Mimisin apodoMás precisamente, para curiosos
- Si se te olvida la comprobación:
huge = next((o for o in orders if o.total > 500), None)print(huge.customer)Traceback (most recent call last): ...AttributeError: 'NoneType' object has no attribute 'customer'x or ytambién da un valor de reserva, pero trata una cadena vacía y el0como si faltaran. Úsalo solo si una cadena vacía de verdad significa que falta:
print(None or "sin apodo")print("" or "sin apodo")print(0 or 5)sin apodosin apodo5any(...)yall(...)responden conTrueoFalsesi alguno o todos los elementos cumplen la condición:
print(any(o.total > 100 for o in orders))print(all(o.total >= 10 for o in orders))TrueTruePruébalo
Encuentra el error
Elige la línea con el error.
Un generador se recorre una sola vez. list(expensive) en la línea 3 lo consume, y en la línea 4 ya no queda nada en él. No hay ningún error, y por eso un descuido así es difícil de detectar:
Cantidad: 2Precios: []Si necesitas dos resultados, crea una lista en la línea 2, con corchetes: [p for p in prices if p >= 50]. Puedes usarla tantas veces como quieras: len(expensive) y expensive.
La idea
Agrupar y unir
¿Cuántos pedidos hay de cada ciudad, y cuáles son los ingresos?
from collections import Counter, defaultdict
count_by_city = Counter(o.city for o in orders)print(dict(sorted(count_by_city.items())))
revenue_by_city = defaultdict(float)for o in orders: revenue_by_city[o.city] += o.totalfor city, total in sorted(revenue_by_city.items()): print(f"{city}: {total:.2f} €")
customers = ", ".join(sorted(o.customer for o in orders))print(customers){'Granada': 1, 'Madrid': 2, 'Valencia': 2}Granada: 120.00 €Madrid: 115.50 €Valencia: 76.90 €Ana, Diego, Elena, Jorge, MaríaCounterdel módulocollectionscuenta: para cada ciudad, cuántas veces aparece. Es un diccionario.defaultdict(float)empieza cada ciudad nueva desde0.0(eso es lo que devuelvefloat()), y+=suma el total.", ".join(...)une las cadenas en una sola, con una coma entre ellas.
El diccionario mantiene las claves en el orden en que se añadieron. sorted(diccionario.items()) da los pares de clave y valor ordenados por clave, así que el informe sale en orden alfabético.
Más precisamente, para curiosos
- Para cada ciudad, una lista de los nombres:
from collections import defaultdict
names_by_city = defaultdict(list)for o in orders: names_by_city[o.city].append(o.customer)print(dict(sorted(names_by_city.items()))){'Granada': ['Diego'], 'Madrid': ['María', 'Elena'], 'Valencia': ['Jorge', 'Ana']}- Un diccionario por comprensión crea un diccionario a partir de una lista:
{clave: valor for ...}. Si una clave se repite, gana el último valor, sin ningún error (en JavatoMaplanza una excepción):
last_by_city = {o.city: o.customer for o in orders}print(last_by_city){'Madrid': 'Elena', 'Valencia': 'Ana', 'Granada': 'Diego'}Ejercicio
Informes con listas por comprensión
1. Ventas por categoría. Una clase Sale con un producto, una categoría, una cantidad y un precio unitario, y un método revenue() (cantidad por precio). Suma los ingresos por categoría con defaultdict(float), imprime las categorías en orden alfabético y los ingresos totales. Las ventas: café en grano, bebidas, 12 a 18,50 €; té, bebidas, 30 a 4,20 €; chocolate, dulces, 40 a 2,80 €; galletas, dulces, 25 a 3,10 €; miel, comida, 8 a 12 €; otra vez café en grano, 5 a 18,50 €.
Bebidas: 440.50 €Comida: 96.00 €Dulces: 189.50 €Total: 726.00 €2. Los 3 mejores productos. Con las mismas ventas, encuentra los tres productos con más ingresos. El café está en dos ventas, así que primero agrupa por producto y suma, y luego ordena las entradas del mapa por valor. Pista: sorted(by_product.items(), key=lambda item: item[1], reverse=True)[:3]: cada elemento de items() es un par (producto, ingresos).
Café en grano: 314.50 €Té: 126.00 €Chocolate: 112.00 €3. Nota media. Lee una línea de notas separadas por espacios. Imprime la media, la más alta y cuántas son excelentes (5,50 o más). Pista: [float(g) for g in input().split()].
4.50 5.75 6.00 3.00 5.50Media: 4.95Máxima: 6.00Excelentes: 34. Las palabras de un texto. Lee una línea de texto e imprime sus palabras sin repetidos, en orden alfabético, en minúsculas y separadas por comas. Encuentra las secuencias de letras con re.findall(r"[^\W\d_]+", text): \w es una letra, una cifra o _, y [^\W\d_] es «una letra, pero ni una cifra ni _».
Café con leche, café sin azúcar. ¡Té con miel!azúcar, café, con, leche, miel, sin, téMás tareas
- La palabra más larga. Con
max(words, key=len), encuentra la palabra más larga de una frase. ¿Qué pasará con una línea vacía, y cómo ayudadefault? - Media por grupo. Estudiantes con un nombre, un grupo y una nota: reúne las notas de cada grupo en un
defaultdict(list)e imprime la media de cada grupo. - Pedidos grandes y pequeños. Con
any, comprueba si hay un pedido de más de 1000 €, y conallsi todos son de al menos 10 €.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Listas: append, remove y sort · dict y set · Clases anidadas y datos · Lambdas
- Continúa en: dataclass, match y patrones · Tests con unittest · Bases de datos · Reflexión y decoradores
- Ejercicios sobre el tema: Python moderno