List, dict, set comprehension en Python: escribe conciso y claro
1. Resumen general
Comprehension es una sintaxis característica de Python. Te permite crear list, dict o set a partir de un iterable en solo una línea. Escrito correctamente, el código es más conciso y claro que un bucle for. Abusado, el código se convierte en una línea larga y confusa.
Este artículo cubre list, dict, set comprehension y generator expression con ejemplos concretos. Después de leerlo, sabrás cuándo usar comprehension, cuándo volver a un bucle for, y cómo escribir tests para la lógica de transformación de datos. Todos los ejemplos se ejecutan en Python 3.12.
2. List comprehension
2.1. Sintaxis básica
Supongamos que tienes una lista de pedidos y necesitas obtener el total de cada uno. La forma tradicional usando un bucle for es así:
from dataclasses import dataclass
@dataclass
class Order:
id: int
customer: str
total: float
status: str
orders = [
Order(1, "An", 250_000, "PAID"),
Order(2, "Bình", 90_000, "CANCELLED"),
Order(3, "An", 1_200_000, "PAID"),
Order(4, "Chi", 450_000, "PENDING"),
]
totals = []
for order in orders:
totals.append(order.total)
print(totals)
Reescribiendo con list comprehension:
totals = [order.total for order in orders]
print(totals)
[250000, 90000, 1200000, 450000]
La estructura es [expresión for elemento in iterable]. Léelo de izquierda a derecha: “obtén order.total para cada order en orders”. No necesitas declarar una lista vacía ni llamar a append. La intención “estoy creando una lista nueva” se ve claramente desde el [.
2.2. Filtrar elementos con if
Si solo quieres obtener los pedidos pagados, añades una cláusula if al final:
paid_ids = [order.id for order in orders if order.status == "PAID"]
print(paid_ids)
[1, 3]
El if al final actúa como un filtro. Los elementos que no cumplen la condición se descartan, por lo que la lista resultante puede ser más corta que la original.
2.3. Expresión if-else al principio
Cuando quieres transformar cada elemento según una condición en lugar de filtrar, colocas if-else en la parte de la expresión:
labels = ["lớn" if order.total >= 500_000 else "nhỏ" for order in orders]
print(labels)
['nhỏ', 'nhỏ', 'lớn', 'nhỏ']
Aquí es donde muchas personas se confunden. Solo necesitas recordar dos reglas:
ifdespués deforse usa para filtrar. Esta cláusula no tieneelse.a if cond else bantes defores una expresión condicional. Esta cláusula requiere obligatoriamente unelsey mantiene el número de elementos.
Escribir [x for x in data if cond else y] causará un SyntaxError.
3. Dict comprehension
Dict comprehension usa llaves {} con pares key: value. Por ejemplo, necesitas buscar rápidamente un pedido por id:
orders_by_id = {order.id: order for order in orders}
print(orders_by_id[3])
Order(id=3, customer='An', total=1200000, status='PAID')
También puedes transformar un dict existente. Por ejemplo, aplicar un impuesto del 10% a una tabla de precios:
prices = {"cafe": 30_000, "tra_sua": 45_000, "banh_mi": 20_000}
prices_with_vat = {name: round(price * 1.1) for name, price in prices.items()}
print(prices_with_vat)
{'cafe': 33000, 'tra_sua': 49500, 'banh_mi': 22000}
prices.items() devuelve pares (key, value), y los desempaquetas directamente en el bucle for.
Un truco útil es invertir un dict:
status_codes = {"PAID": 1, "PENDING": 2, "CANCELLED": 3}
code_to_status = {code: status for status, code in status_codes.items()}
print(code_to_status[2])
PENDING
Nota: Si dos elementos producen la misma key, el elemento posterior sobrescribe silenciosamente el anterior. Por ejemplo,
{order.customer: order.total for order in orders}solo mantiene el último pedido de “An” (1.200.000), y el pedido de 250.000 desaparece sin ninguna advertencia. Cuando necesites agrupar, usadefaultdictcon un buclefor(ver sección 7).
4. Set comprehension
Set comprehension también usa {} pero solo tiene una expresión, sin dos puntos :. El resultado elimina duplicados automáticamente. Por ejemplo, quieres saber qué clientes han hecho pedidos:
customers = {order.customer for order in orders}
print(sorted(customers))
['An', 'Bình', 'Chi']
Cada cliente aparece solo una vez aunque “An” tenga dos pedidos. El orden en un set no está garantizado, así que el ejemplo anterior usa sorted al imprimir.
Set comprehension es muy útil cuando necesitas verificar la membresía rápidamente:
banned_emails = {"spam@x.com", "Fake@Y.com"}
normalized = {email.strip().lower() for email in banned_emails}
print("fake@y.com" in normalized)
True
Nota:
{}vacío es un dict, no un set. Para crear un set vacío, debes escribirset().
5. Múltiples bucles for y comprensiones anidadas
Puedes escribir múltiples for en una comprensión. El orden de lectura es exactamente el mismo cuando escribes bucles for anidados de arriba hacia abajo:
carts = {
"An": ["cafe", "banh_mi"],
"Chi": ["tra_sua"],
}
items = [(customer, item) for customer, products in carts.items() for item in products]
print(items)
[('An', 'cafe'), ('An', 'banh_mi'), ('Chi', 'tra_sua')]
La comprensión anterior es equivalente al siguiente código:
items = []
for customer, products in carts.items():
for item in products:
items.append((customer, item))
El caso más común es aplanar (flatten) una lista anidada:
matrix = [[1, 2, 3], [4, 5, 6]]
flat = [value for row in matrix for value in row]
print(flat)
[1, 2, 3, 4, 5, 6]
Para recordar: el bucle for más externo va primero. Si tienes que detenerte a leer una segunda vez para entender el orden, esa es una señal de que deberías separarlo en un bucle for regular.
6. Generator expression: cuando no necesitas toda la lista
Si reemplazas [] por (), obtienes una generator expression. Python no crea una lista en memoria. Los valores se generan uno por uno cuando el código los consume.
revenue = sum(order.total for order in orders if order.status == "PAID")
print(revenue)
1450000
Cuando el generator es el único argumento de una función, puedes omitir un par de paréntesis. Esto funciona bien con sum, any, all, max, min:
has_big_order = any(order.total > 1_000_000 for order in orders)
all_positive = all(order.total > 0 for order in orders)
print(has_big_order, all_positive)
True True
any se detiene tan pronto como encuentra el primer elemento que cumple la condición. Por lo tanto, con datos grandes, el generator ahorra memoria y es más rápido que any([...]).
Nota: Un generator solo se puede iterar una vez. La segunda iteración no devuelve nada:
paid = (o for o in orders if o.status == "PAID")
print(len(list(paid)))
print(len(list(paid)))
2
0
Si necesitas usar el resultado múltiples veces, usa list comprehension.
7. Mantener las comprensiones legibles
7.1. Nombres de función para lógica compleja
Las condiciones largas hacen que la comprensión sea difícil de leer:
vip = [o.customer for o in orders if o.status == "PAID" and o.total >= 1_000_000 and not o.customer.startswith("test")]
Puedes extraer la condición en una función con un nombre descriptivo:
def is_vip_order(order: Order) -> bool:
return (
order.status == "PAID"
and order.total >= 1_000_000
and not order.customer.startswith("test")
)
vip = [order.customer for order in orders if is_vip_order(order)]
print(vip)
['An']
Ahora la comprensión se lee como una oración, y la lógica empresarial tiene su propio lugar para testearse.
7.2. Saltos de línea razonables
Una comprensión no tiene que estar en una sola línea. Dentro de los paréntesis, puedes hacer saltos de línea libremente:
summary = {
order.id: order.total
for order in orders
if order.status != "CANCELLED"
}
print(summary)
{1: 250000, 3: 1200000, 4: 450000}
Cada línea cumple un rol: expresión, fuente, filtro.
7.3. Walrus operator para evitar calcular dos veces
A veces necesitas filtrar y también usar el resultado del mismo cálculo. El operador := (Python 3.8+) te ayuda a calcularlo solo una vez:
def discount_for(order: Order) -> float:
return order.total * 0.05 if order.total >= 400_000 else 0
discounts = {
order.id: d
for order in orders
if (d := discount_for(order)) > 0
}
print(discounts)
{3: 60000.0, 4: 22500.0}
Usa el operador walrus con moderación. La variable d “se filtrará” hacia el scope fuera de la comprensión. Además, muchos lectores aún no están familiarizados con esta sintaxis.
8. Testeando la lógica de transformación de datos
Las comprensiones suelen estar en funciones que transforman datos, así que deberías testearlas como cualquier otra lógica. Primero, instala pytest:
pip install pytest
Supongamos que tienes un módulo reports.py:
# reports.py
from dataclasses import dataclass
@dataclass
class Order:
id: int
customer: str
total: float
status: str
def revenue_by_customer(orders: list[Order]) -> dict[str, float]:
customers = {o.customer for o in orders if o.status == "PAID"}
return {
c: sum(o.total for o in orders if o.customer == c and o.status == "PAID")
for c in customers
}
Y el archivo de test correspondiente:
# test_reports.py
from reports import Order, revenue_by_customer
def test_revenue_only_counts_paid_orders():
orders = [
Order(1, "An", 250_000, "PAID"),
Order(2, "An", 1_200_000, "PAID"),
Order(3, "Bình", 90_000, "CANCELLED"),
Order(4, "Chi", 450_000, "PAID"),
]
result = revenue_by_customer(orders)
assert result == {"An": 1_450_000, "Chi": 450_000}
assert "Bình" not in result
def test_empty_orders_returns_empty_dict():
assert revenue_by_customer([]) == {}
pytest -q
.. [100%]
2 passed in 0.01s
El primer test afirma dos cosas: los pedidos cancelados no se cuentan, y los clientes que solo tienen pedidos cancelados no aparecen en el resultado. El segundo test verifica el caso límite con una lista vacía.
La función anterior es correcta pero tiene complejidad O(n × número de clientes) porque por cada cliente se recorre todo orders. Con datos grandes, un bucle for con defaultdict es tanto más rápido como más legible:
from collections import defaultdict
def revenue_by_customer(orders: list[Order]) -> dict[str, float]:
totals: dict[str, float] = defaultdict(float)
for order in orders:
if order.status == "PAID":
totals[order.customer] += order.total
return dict(totals)
Los dos tests anteriores siguen pasando sin necesidad de cambios. Este es el beneficio de tener tests: puedes refactorizar con confianza de comprehension a bucle for o viceversa.
9. Cuándo NO usar comprehensions
- Cuando necesitas side effects. No escribas
[print(o) for o in orders]o[db.save(o) for o in orders]. Esto crea una lista deNonesin sentido e engaña al lector haciéndole creer que necesitas el resultado. Usa un buclefor. - Cuando se anida más de dos niveles o hay varios
ifintercalados. Tres buclesforen una línea casi siempre es más difícil de leer que la versión con saltos de línea. - Cuando necesitas agrupar o acumular como sumatorias o conteos. Usa
defaultdict,Countero un buclefor. - Cuando necesitas manejar excepciones para cada elemento. Las comprensiones no pueden contener
try/except. Deberías extraerlo en una función separada y llamarla en la comprensión, o usar un buclefor. - Cuando los datos son muy grandes y solo se itera una vez. Entonces usa una generator expression en lugar de una lista.
Nota: En Python 3, la variable del bucle de una comprensión no se filtra hacia afuera. Después de
[x for x in range(3)], el nombrexno existe en el scope externo, a diferencia de un bucleforregular. La única excepción es una variable asignada con:=como se mencionó en la sección 7.3.
10. Conclusión
Las comprensiones te ayudan a expresar la idea “crear una nueva colección a partir de una colección existente” de forma concisa:
[...]para lista,{k: v ...}para dict,{...}para set, y(...)para generator.ifdespués deforse usa para filtrar,a if cond else bantes deforse usa para transformar.- Extrae condiciones complejas en funciones con nombres descriptivos, y salta de línea cuando la expresión es larga.
- Usa generator expressions con
sum,any,allpara ahorrar memoria. - Vuelve a un bucle
forcuando haya side effects, cuando necesites agrupar, manejar excepciones, o cuando la comprensión sea difícil de leer.
La regla más simple: si tus colegas tienen que leer tu comprensión dos veces para entenderla, reescríbela con un bucle for. El código conciso solo tiene valor si sigue siendo fácil de leer.