lección 8
Los atajos de Python que vas a leer todo el rato
Formas cortas de escribir lo que ya sabes hacer con un bucle: comprensiones de diccionario, generadores, el condicional en una línea, lambda con key=, map, filter, set() y defaultdict. Y cuándo estorban.
⏱ 50 min
Un compañero del equipo te manda por chat una sola línea y un «prueba con esto»: sorted(gastos.items(), key=lambda x: x[1], reverse=True)[:3]. Tú le habías pedido los tres clientes que más han gastado. La línea funciona, te da exactamente lo que necesitabas, y no entiendes ni la mitad de lo que hace. Reconoces sorted, reconoces los corchetes del final, y en medio hay algo llamado lambda con una x que sale de la nada. Esa sensación —el código funciona y no lo sé leer— es la que vamos a quitar de encima en esta lección.
La buena noticia es que ahí no hay ningún concepto nuevo. Sabes hacer un bucle for. Sabes recorrer un diccionario con .items(). Sabes escribir un if/elif/else, definir una función y filtrar una lista con una comprensión. Los seis atajos de esta lección son formas cortas de escribir exactamente eso: cosas que ya sabes hacer en cuatro o cinco líneas, escritas en una. No son magia y no son otro lenguaje. Son abreviaturas.
¿Y por qué te importan a ti, que vas a analizar datos y no a construir software? Por dos motivos muy concretos. El primero: los vas a leer constantemente, en código de compañeros, en respuestas de foros y en los ejemplos de casi cualquier documentación. Un atajo que no sabes leer es una línea que tienes que copiar a ciegas, y copiar a ciegas es la forma más rápida de dar un número mal sin enterarte. El segundo: en pandas, la librería con la que vas a tratar tablas de datos de verdad, estos atajos no son opcionales ni decorativos. Ordenar por una columna, aplicar un cálculo a toda una columna, clasificar valores por tramos: las tres cosas se escriben con lo que vas a ver hoy.
### Comprensión de diccionario: lo mismo que ya haces, pero con claves
Cuando escribes [v["importe"] for v in ventas] estás construyendo una lista: un valor por cada registro, sin nombre, en orden. Pero muchas veces lo que necesitas no es una lista de valores, sino un diccionario que asocie cada cliente con su cifra, cada ciudad con su facturación, cada producto con su margen. Eso es lo que hace una comprensión de diccionario: lo mismo, con llaves en vez de corchetes, y con dos partes separadas por dos puntos — la clave y el valor.
El problema que resuelve es el de siempre: preparar un diccionario a partir de una lista de registros. Antes de que existiera la forma corta, eso se escribía con un diccionario vacío y un bucle que lo va rellenando; es la versión que ya sabes escribir y sigue siendo perfectamente válida. La forma corta ahorra tres líneas y, sobre todo, deja claro de un vistazo que el resultado es un diccionario nuevo y no un diccionario que se va modificando por el camino.
1ventas = [2 {"cliente": "Ana García", "importe": 320.00},3 {"cliente": "Luis Pérez", "importe": 890.50},4 {"cliente": "Ana García", "importe": 150.00},5]67# La versión larga: la que ya sabes escribir8saldo = {}9for v in ventas:10 saldo[v["cliente"]] = 0.011print(saldo) # {'Ana García': 0.0, 'Luis Pérez': 0.0}1213# La versión corta: comprensión de diccionario14saldo = {v["cliente"]: 0.0 for v in ventas}15print(saldo) # {'Ana García': 0.0, 'Luis Pérez': 0.0}1617# Y las dos partes pueden calcularse: clave a la izquierda, valor a la derecha18con_iva = {v["cliente"]: round(v["importe"] * 1.21, 2) for v in ventas}19print(con_iva) # {'Ana García': 181.5, 'Luis Pérez': 1077.5}
Las dos primeras líneas de resultado son idénticas. La forma corta no cambia lo que obtienes, cambia lo que se tarda en leerlo.
Una comprensión de diccionario no agrupa: machaca. Si la lista tiene dos registros del mismo cliente, el segundo sobreescribe al primero y te quedas con uno solo, sin ningún aviso. Un total por cliente con esta forma corta sale mal y sale creíble, que es la peor combinación posible. Para sumar por grupo hace falta acumular, y eso es el último atajo de esta lección.
### El generador: los mismos valores, sin construir la lista
Has escrito sumas así: sum([v["importe"] for v in ventas]). Los corchetes de dentro construyen primero una lista completa con todos los importes, y sum() la recorre después. Funciona. Pero hay una versión sin corchetes —sum(v["importe"] for v in ventas)— que da exactamente el mismo número y que vas a ver más a menudo que la otra. La diferencia no está en el resultado, está en lo que ocurre en medio.
Sin los corchetes, Python no construye ninguna lista: le va entregando los valores a sum() de uno en uno, y cada uno se descarta en cuanto se ha sumado. Eso se llama un generador, y la palabra describe bien lo que hace — genera valores a demanda en vez de guardarlos todos. Con tres importes da igual. Con un fichero de dos millones de filas, la versión con corchetes construye en memoria una lista de dos millones de números para sumarla y tirarla, y la versión sin corchetes no guarda ni uno. Es la misma diferencia que hay entre apuntar todos los tiques de la caja en una hoja para sumarlos al final, y llevar la cuenta en la cabeza mientras pasan.
1ventas = [2 {"cliente": "Ana García", "importe": 320.00},3 {"cliente": "Luis Pérez", "importe": 890.50},4 {"cliente": "Ana García", "importe": 150.00},5]67# Con corchetes: construye la lista [320.0, 890.5, 150.0] y luego la suma8print(sum([v["importe"] for v in ventas])) # 1360.5910# Sin corchetes: entrega los valores de uno en uno. Mismo resultado.11print(sum(v["importe"] for v in ventas)) # 1360.51213# Funciona con cualquier función que recorra valores14print(max(v["importe"] for v in ventas)) # 890.515print(any(v["importe"] > 500 for v in ventas)) # True
Los dos primeros print dan 1360.5. La versión sin corchetes es la que verás escrita en la mayoría del código ajeno.
Consejo de senior: cuando veas un paréntesis pegado a sum, max o any y dentro un for sin corchetes, léelo mentalmente como «para cada… dame…». sum(v["importe"] for v in ventas) es «suma, para cada venta, su importe». Con esa traducción en la cabeza, dejas de ver sintaxis raro y empiezas a ver una frase.
### El condicional en una línea: elegir un valor, no bifurcar un programa
Sabes clasificar con if/elif/else en varias líneas. Pero hay un caso muy concreto y muy frecuente en el que ese bloque se queda grande: cuando lo único que quieres es elegir entre dos valores para asignarlos a una variable. No quieres ejecutar cosas distintas según la condición; quieres un valor u otro. Para eso existe el condicional en una línea, que en Python se escribe en un orden que descoloca a todo el mundo la primera vez: primero el valor, después la condición, y al final la alternativa.
Se lee de corrido, como una frase en español: «VIP si el gasto pasa de 5.000, y si no, resto». Ese orden es raro comparado con casi cualquier otro lenguaje, pero tiene una ventaja: lo primero que ves es el valor normal, el que va a salir la mayoría de las veces, y la condición queda como una salvedad. Y donde de verdad se paga su ausencia es en las protecciones de una línea, el clásico «divide entre el número de pedidos, salvo que sean cero».
1gasto = 5390.02pedidos = 034# La versión larga5if gasto > 5000:6 etiqueta = "VIP"7else:8 etiqueta = "resto"910# La versión corta: valor · si condicion · si no, otro valor11etiqueta = "VIP" if gasto > 5000 else "resto"12print(etiqueta) # VIP1314# El uso más frecuente en análisis: evitar dividir entre cero15ticket_medio = gasto / pedidos if pedidos else 0.016print(ticket_medio) # 0.01718# Tres tramos, anidando una vez. El paréntesis no es obligatorio,19# pero sin él nadie sabe dónde empieza la segunda pregunta.20for g in [7420.0, 3120.5, 760.25]:21 segmento = "VIP" if g > 5000 else ("recurrente" if g > 1000 else "nuevo")22 print(g, segmento) # 7420.0 VIP / 3120.5 recurrente / 760.25 nuevo
El mismo if/else de siempre, cuando lo único que está en juego es qué valor se asigna.
El error más común no es de sintaxis, es de límite: 5000 exacto NO es «mayor que 5000». Un cliente que ha gastado exactamente 5.000 € cae en la rama del else, así que con > 5000 no será VIP. Cuando clasifiques por tramos, lee dos veces si el enunciado dice «más de» (>) o «al menos» (>=). Un informe entero puede cambiar de conclusión por ese signo, y ninguna herramienta te va a avisar porque el código es correcto.
### lambda: una función de usar y tirar
Aquí llegamos a la línea del chat. Un lambda es una función sin nombre, escrita en la misma línea donde se usa. Nada más. Recibe algo, devuelve algo, y se acaba ahí: no se guarda, no se reutiliza y no se llama desde otro sitio. El nombre viene del cálculo lambda, un sistema para razonar sobre funciones que el matemático Alonzo Church publicó en los años treinta, mucho antes de que existiera un ordenador donde probarlo. Es la palabra más intimidante de esta lección y lo que hay detrás es lo más simple.
¿Y para qué se quiere una función que no se guarda? Para pasársela a otra función. Eso es lo que hace ese key= que aparece dentro de sorted, de max y de min: es el sitio donde le dices con qué criterio comparar. Por defecto, sorted compara los elementos tal cual, y si son textos los ordena alfabéticamente; con key= le entregas una función que, dado un elemento, devuelve el trozo por el que quieres comparar. La función se aplica a cada elemento, se ordena por lo que devuelve, y lo que sale son los elementos enteros, no el criterio.
1gasto_por_cliente = {2 "Alba Requena": 2415.80,3 "Bruno Sáez": 5390.00,4 "Carla Vidal": 760.25,5}67# Con una función normal: recibe un par (cliente, gasto) y devuelve el gasto8def solo_el_gasto(par):9 return par[1]1011print(sorted(gasto_por_cliente.items(), key=solo_el_gasto, reverse=True))1213# Lo MISMO con lambda, sin darle nombre a una función que se usa una vez14print(sorted(gasto_por_cliente.items(), key=lambda par: par[1], reverse=True))15# [('Bruno Sáez', 5390.0), ('Alba Requena', 2415.8), ('Carla Vidal', 760.25)]1617# Sin key= no falla: ordena por el primer elemento del par, el nombre18print(sorted(gasto_por_cliente.items()))19# [('Alba Requena', 2415.8), ('Bruno Sáez', 5390.0), ('Carla Vidal', 760.25)]2021# Y el mismo key= sirve para quedarte con uno solo22print(max(gasto_por_cliente, key=gasto_por_cliente.get)) # Bruno Sáez
Las dos primeras líneas dan el mismo resultado. Compara la tercera: quien más gasta es Bruno Sáez, y sin key= la lista empieza por Alba Requena.
Consejo de senior: un lambda con más de una operación dentro es una función con vergüenza de dar su nombre. Si lo que tienes que calcular para comparar ocupa más de un trocito —un cálculo con dos condiciones, una limpieza de texto, un redondeo sobre una división— escríbela con def, dale un nombre que diga qué criterio es, y pásala igual a key=. El código queda más largo y la reunión donde te preguntan «¿y esto por qué ordena así?» queda más corta.
### map y filter: aplicar y quedarse
Ahora que sabes qué es un lambda, puedes leer las otras dos funciones que lo usan a diario: map y filter. Las dos reciben una función y una colección; la diferencia es qué hacen con ella. map aplica la función a cada elemento y te devuelve los resultados. filter aplica la función a cada elemento y te devuelve solo los que dieron True.
1importes = [150.0, 320.0, 85.5, 210.0, 45.0]23# map: aplicar una operación a cada elemento4con_iva = list(map(lambda x: round(x * 1.21, 2), importes))5print("Con IVA:", con_iva)6# → [181.5, 387.2, 103.45, 254.1, 54.45]78# filter: quedarse solo con los que cumplen una condición9grandes = list(filter(lambda x: x > 100, importes))10print("Grandes:", grandes)11# → [150.0, 320.0, 210.0]1213# Las dos juntas: IVA solo de los grandes14grandes_con_iva = list(map(lambda x: round(x * 1.21, 2), filter(lambda x: x > 100, importes)))15print("Grandes con IVA:", grandes_con_iva)16# → [181.5, 387.2, 254.1]
map transforma, filter selecciona. Ambas devuelven un iterador: hay que envolverlas en list() para ver el resultado.
La equivalencia con comprensiones es directa: list(map(f, xs)) es lo mismo que [f(x) for x in xs], y list(filter(f, xs)) es lo mismo que [x for x in xs if f(x)]. Las comprensiones suelen ser más legibles en Python, pero map y filter son el vocabulario universal de la programación funcional — las verás en cualquier lenguaje.
### set(): contar sin repetir
Esta es la operación más analista de las seis: cuántos clientes distintos han comprado. No cuántos pedidos, no cuántas líneas: cuántas personas diferentes. Con lo que ya sabes, se resuelve con una lista y un bucle que comprueba «si no está ya, lo añado». Python tiene una estructura que hace justo eso, y solo eso: un conjunto, o set, que es una colección que no admite repetidos. Cada valor entra una vez; el segundo intento se descarta en silencio.
La analogía física es la lista de invitados de una boda. Da igual cuántas veces te digan que viene tu prima Ana: en la lista figura una vez, porque lo que se cuenta son personas, no menciones. Y una lista de invitados tampoco necesita orden para hacer su trabajo, y esa es la otra propiedad del conjunto: no guarda el orden en que metiste las cosas. Si necesitas mostrarlo ordenado, lo pasas por sorted() y ya.
Si has trabajado con SQL, esto es su COUNT(DISTINCT) — la misma pregunta con otra herramienta. Y hay un detalle que casi nadie cuenta: comprobar si algo está en un conjunto es muchísimo más rápido que comprobarlo en una lista larga, porque un conjunto no recorre sus elementos uno a uno para buscar. Con cien valores da igual; con cien mil, la diferencia se mide con el reloj.
1pedidos = [2 {"cliente": "Ana García", "ciudad": "Sevilla"},3 {"cliente": "Luis Pérez", "ciudad": "Valencia"},4 {"cliente": "Ana García", "ciudad": "Sevilla"},5 {"cliente": "Sara Torres", "ciudad": "Valencia"},6]78# La versión larga: la que ya sabes escribir9vistos = []10for p in pedidos:11 if p["cliente"] not in vistos:12 vistos.append(p["cliente"])13print(len(vistos)) # 31415# La versión corta, con un generador dentro (sin corchetes)16print(len(set(p["cliente"] for p in pedidos))) # 31718# Un conjunto no tiene orden: para mostrarlo, se ordena19print(sorted(set(p["ciudad"] for p in pedidos))) # ['Sevilla', 'Valencia']2021# Comparar dos conjuntos: quién repite y quién se ha ido22marzo = {"Ana García", "Luis Pérez", "Sara Torres"}23abril = {"Ana García", "Jorge Díaz"}24print(sorted(marzo & abril)) # ['Ana García'] compraron los dos meses25print(sorted(marzo - abril)) # ['Luis Pérez', 'Sara Torres'] dejaron de comprar
Cuatro clientes en la lista de pedidos, tres personas distintas. La cifra que va al informe es la segunda.
Un conjunto no guarda valores repetidos ni orden, así que hay dos cosas que no le puedes pedir: cuántas veces apareció cada valor y en qué posición estaba. set(["Ana", "Ana", "Luis"]) te dice que hay dos personas distintas y ya no sabe que Ana compró dos veces. Si la pregunta es «cuántas veces», el conjunto no es la herramienta: es contar, y eso viene ahora.
### defaultdict: el diccionario que no se queja
Agrupar y sumar por grupo es, probablemente, la operación que más veces vas a hacer en tu vida profesional: facturación por ciudad, pedidos por cliente, incidencias por categoría. Ya sabes montarlo con un diccionario normal, y también sabes cuál es la parte molesta: antes de sumar tienes que comprobar si la clave existe, porque si no existe y le pides sumar, el programa se detiene con un KeyError. Esas dos líneas de comprobación aparecen en cada agrupación que escribas.
defaultdict es un diccionario que ya viene con esa comprobación puesta. Al crearlo le dices con qué valor debe estrenar una clave nueva —un cero, una lista vacía, un diccionario— y a partir de ahí puedes sumar o añadir directamente sin preguntar si la clave existía. Vive en un módulo de la librería estándar llamado collections, así que hay que importarlo. Es un diccionario a todos los efectos: se recorre igual, se imprime igual, y .items() funciona igual.
1from collections import defaultdict23ventas = [4 {"cliente": "Ana García", "importe": 320.00},5 {"cliente": "Luis Pérez", "importe": 890.50},6 {"cliente": "Ana García", "importe": 150.00},7]89# La versión larga: hay que crear la clave antes de poder sumar10total = {}11for v in ventas:12 if v["cliente"] not in total:13 total[v["cliente"]] = 0.014 total[v["cliente"]] += v["importe"]15print(total) # {'Ana García': 470.0, 'Luis Pérez': 890.5}1617# La versión corta: float() da 0.0 al estrenar cada clave18total = defaultdict(float)19for v in ventas:20 total[v["cliente"]] += v["importe"]21print(dict(total)) # {'Ana García': 470.0, 'Luis Pérez': 890.5}2223# Con list() en vez de float(): guardar todas las líneas de cada cliente24lineas = defaultdict(list)25for v in ventas:26 lineas[v["cliente"]].append(v["importe"])27print(dict(lineas)) # {'Ana García': [320.0, 150.0], 'Luis Pérez': [890.5]}
Ana García suma 470.0 en las dos versiones: 320 + 150. Compara esto con lo que daba la comprensión de diccionario, que se quedaba con 150.
Cuidado con el efecto que nadie espera: consultar una clave que no existe en un defaultdict la CREA. Si tienes facturación por ciudad y escribes print(facturacion["Cuenca"]) para una ciudad sin ventas, verás 0.0 y, a partir de ese momento, Cuenca es una clave del diccionario. El recuento de ciudades ha subido en uno por haber mirado. Un diccionario normal habría lanzado KeyError, que es molesto pero honrado. Cuando solo quieras mirar sin tocar, usa .get(clave, 0).
### Cuándo NO usarlos
Ahora la otra mitad de la lección, la que casi nunca se cuenta. Estos atajos no son «la forma correcta» de escribir Python: son la forma corta, y corto y claro no son sinónimos. El bucle largo no es código de principiante que se abandona al aprender el atajo. Es la herramienta que se usa cuando el atajo deja de leerse de un vistazo.
La regla, en una frase: si la línea no se entiende leyéndola una vez, es peor que el bucle de cinco líneas. Y no es una cuestión de estilo ni de elegancia. El código que escribes hoy lo va a leer alguien —tu compañera, o tú mismo en noviembre— cuando el número que produce no cuadre y haya que averiguar por qué. En ese momento, una línea que hay que descifrar cuesta veinte minutos y una que se lee cuesta veinte segundos. Es el único criterio que aguanta.
1clientes = [2 {"nombre": "Ana García", "gasto": 7420.0, "pedidos": 14, "pais": "ES"},3]45# ILEGIBLE: tres condiciones, dos cálculos y un filtro en la misma línea6resumen = {c["nombre"]: ("VIP" if c["gasto"] / c["pedidos"] > 400 and c["pais"] == "ES" else ("revisar" if c["pedidos"] > 10 else "normal")) for c in clientes if c["pedidos"]}78# LEGIBLE: el mismo resultado, con el cálculo con nombre y las ramas separadas9resumen = {}10for c in clientes:11 if not c["pedidos"]:12 continue13 ticket_medio = c["gasto"] / c["pedidos"]14 if ticket_medio > 400 and c["pais"] == "ES":15 resumen[c["nombre"]] = "VIP"16 elif c["pedidos"] > 10:17 resumen[c["nombre"]] = "revisar"18 else:19 resumen[c["nombre"]] = "normal"2021print(resumen) # {'Ana García': 'VIP'}
Las dos versiones dan el mismo resultado. La segunda es la que quieres tener delante el día que el número no cuadre.
- Usa el atajo cuando la operación es UNA: filtrar, transformar, o construir un diccionario a partir de una lista.
- Usa el bucle cuando hay dos o más operaciones distintas, o cuando cada elemento necesita varios pasos.
- Usa el bucle si necesitas un try/except por elemento: dentro de una comprensión no cabe, y un registro sucio te tira el cálculo entero.
- Usa el bucle si tienes que imprimir o registrar algo por el camino. Un atajo produce un resultado; no cuenta lo que ha hecho.
- Dale nombre a los cálculos intermedios en cuanto sean dos. Una variable con un nombre que dice qué es vale más que una línea ahorrada.
- Y no reescribas un bucle que funciona solo para hacerlo más corto. Corto no es un objetivo.
Consejo de senior: la prueba que uso desde hace años es leer la línea en voz alta. Si sale una frase —«suma, para cada venta, su importe»— está bien. Si tengo que pararme a mirar dónde cierra un paréntesis, la parto sin pensarlo más. No he visto nunca a nadie quejarse de que un cálculo fuese demasiado fácil de leer.
### Resumen: el bucle largo y su atajo, lado a lado
1LO QUE QUIERES HACER LA FORMA LARGA EL ATAJO2───────────────────────── ────────────────────────── ─────────────────────────────3Un diccionario a partir dic = {} {v["cli"]: 0.0 for v in ventas}4de una lista for v in ventas: ...56Sumar valores de una suma = 0 sum(v["imp"] for v in ventas)7lista de registros for v in ventas: suma += ...89Elegir entre dos valores if x > 5000: "VIP" if x > 5000 else "resto"10 e = "VIP"11 else:12 e = "resto"1314Ordenar por un criterio def crit(p): return p[1] key=lambda p: p[1]15que no es el elemento sorted(pares, key=crit)1617Aplicar una función resultado = [] list(map(lambda x: x*1.21, xs))18a cada elemento for x in xs:19 resultado.append(x*1.21)2021Quedarse con los que resultado = [] list(filter(lambda x: x>100, xs))22cumplen una condición for x in xs:23 if x > 100: ...2425Contar cuántos distintos vistos = [] len(set(p["cli"] for p in ped))26 for p in ped:27 if ... not in vistos: ...2829Sumar por grupo if k not in dic: total = defaultdict(float)30 dic[k] = 0.0 total[k] += imp31 dic[k] += imp
Las dos columnas hacen lo mismo. Ninguna es más correcta que la otra: la de la derecha es más corta, y solo es mejor cuando además se lee.
Un último aviso sobre estos seis atajos, y es el que más te va a servir: te los vas a encontrar antes de necesitar escribirlos. Cuando trabajes con pandas, ordenar una tabla por una columna, aplicar un cálculo a toda una columna de golpe o clasificar valores en tramos se escriben con lambda y con el condicional en una línea, y los ejemplos que leas por ahí darán las seis construcciones por sabidas. Eso ya no es un problema: ahora, cuando veas una línea así, sabes que dentro no hay nada que no hayas escrito ya en cinco líneas. Y también sabes cuándo esa línea sobra.
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...