Ir al contenido
wannacode

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:

Order
class Order:
def __init__(self, customer, city, total):
self.customer = customer
self.city = city
self.total = total
orders = [
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)
Salida
['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)
Salida
['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 en filter. También se puede omitir;
  • o.customer al principio: en qué se convierte cada elemento, como en map;
  • 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: filter y map. 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)
Salida
['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

Aquí los pasos son dos generadores: expresiones entre paréntesis en lugar de corchetes. Un generador no crea una lista. Da los valores de uno en uno, cuando alguien los pide, y solo entonces los calcula.

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
  1. Cinco precios. Los dos generadores solo preparan el camino: todavía no ha pasado nada.
  2. 12 no pasa if p >= 50 y se queda fuera. Nunca llega a convertirse en texto.
  3. 75 pasa if, f"{p} €" lo convierte en el texto «75 €», list() lo reúne.
  4. 40 también se queda fuera. Cada elemento recorre todo el camino antes de que salga el siguiente.
  5. 130 y 58 pasan. La lista prices no cambia.
  6. 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))
Salida
El generador está listo, todavía no pasó nada por él.
filter 12
filter 75
map 75
filter 40
[150]
  • La línea «El generador está listo» se imprime antes de que un solo elemento haya pasado por el generador.
  • map 75 llega justo después de filter 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)
Salida
filter 12
filter 75
map 75
filter 40
La lista está lista.
[150]
  • Por eso puedes tomar solo los primeros valores de un generador sin comprobar el resto. islice del módulo itertools se 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)))
Salida
filter 75
filter 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}")
Salida
Suma de los números pares: 6
sum 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 0
max_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} €")
Salida
Pedidos de desde 50 €: 3
Ingresos: 312.40 €
Media: 62.48 €
Mayor: 120.00 €
  • len da 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.
  • max de una lista vacía se detiene con un ValueError. default=0 dice 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)
Salida
0
0
ValueError: max() iterable argument is empty
ZeroDivisionError: division by zero

La 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}")
Salida
[2, 3, 4, 5]
['Diego', 'María', 'Ana']
Mesa 1
Mesa 2
Mesa 3
  • set(...) quita los repetidos: en un conjunto cada valor está una sola vez. El orden en un conjunto no está garantizado, por eso sorted va después.
  • sorted(orders, key=..., reverse=True) ordena empezando por el pedido más grande, con key de la lección anterior.
  • [:3] toma los tres primeros elementos. Es una rebanada (slice): desde el principio hasta el índice 3, sin incluirlo. Junto con sorted, 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)))
Salida
[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)
Respuesta

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")
Salida
Mayor: Diego
False
no existe ese pedido
  • max(..., default=None) devuelve None en lugar de detenerse si la lista está vacía.
  • next(generador, None) da el primer valor del generador, o None si no hay ninguno. Esto es «el primer pedido de más de 500 €», sin comprobar el resto.
  • is not None comprueba si hay un valor. Con None se compara usando is, no ==: es lo que pide PEP 8.
  • x if condición else y da 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 None
maria = nickname("maria")
if maria is not None:
print(f"Apodo: {maria}")
jorge = nickname("jorge")
print(jorge if jorge is not None else "sin apodo")
Salida
Apodo: Mimi
sin apodo
Má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)
Error
Traceback (most recent call last):
...
AttributeError: 'NoneType' object has no attribute 'customer'
  • x or y también da un valor de reserva, pero trata una cadena vacía y el 0 como 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)
Salida
sin apodo
sin apodo
5
  • any(...) y all(...) responden con True o False si 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))
Salida
True
True

Pruébalo

Encuentra el error

El programa debería imprimir cuántos precios son caros, y los propios precios. La cantidad sale bien, pero los precios son una lista vacía. ¿En qué línea está el error?

Elige la línea con el error.

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.total
for city, total in sorted(revenue_by_city.items()):
print(f"{city}: {total:.2f} €")
customers = ", ".join(sorted(o.customer for o in orders))
print(customers)
Salida
{'Granada': 1, 'Madrid': 2, 'Valencia': 2}
Granada: 120.00 €
Madrid: 115.50 €
Valencia: 76.90 €
Ana, Diego, Elena, Jorge, María
  • Counter del módulo collections cuenta: para cada ciudad, cuántas veces aparece. Es un diccionario.
  • defaultdict(float) empieza cada ciudad nueva desde 0.0 (eso es lo que devuelve float()), 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())))
Salida
{'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 Java toMap lanza una excepción):
last_by_city = {o.city: o.customer for o in orders}
print(last_by_city)
Salida
{'Madrid': 'Elena', 'Valencia': 'Ana', 'Granada': 'Diego'}

Ejercicio

Informes con listas por comprensión

Resuelve las tareas en tu editor.

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

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

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

Entrada
4.50 5.75 6.00 3.00 5.50
Salida
Media: 4.95
Máxima: 6.00
Excelentes: 3

4. 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 _».

Entrada
Café con leche, café sin azúcar. ¡Té con miel!
Salida
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 ayuda default?
  • 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 con all si todos son de al menos 10 €.

Test

Ponte a prueba

1. ¿Qué le pasa a la lista orders después de [o for o in orders if o.total > 50]?
2. ¿Qué diferencia hay entre [p * 2 for p in prices] y (p * 2 for p in prices)?
3. ¿Qué hace max(orders, key=lambda o: o.total) con una lista vacía?
4. ¿Por qué el informe por ciudad usa sorted(revenue_by_city.items())?
5. expensive = (p for p in prices if p >= 50); print(len(list(expensive))); print(list(expensive)). ¿Qué imprime la segunda línea?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·