Enroll to access all the lessons in this course.
Lesson 8 of 29· La rúbrica de revisión: cuatro ejes
Hasta aquí has visto los cuatro ejes por separado. Ahora toca lo difícil: aplicarlos todos a la vez, a una función que ya está escrita, que parece bien y que se ejecuta sin lanzar ni un error.
Esa es la situación real. Nadie te entrega código que explote: te entregan cuarenta líneas plausibles —de un compañero, de un repositorio heredado, de un asistente de IA— y una pregunta implícita: «¿lo apruebo?».
La función de esta lección resume las líneas de pedido de una tienda por categoría. Tiene exactamente tres defectos plantados, uno por eje:
Antes de seguir, un recordatorio de la rúbrica en una tabla:
| Eje | Pregunta guía | Cómo se detecta |
|---|---|---|
| 1. Corrección | ¿Hace lo que promete su contrato con la entrada típica? | Predecir la salida y comparar con la real |
| 2. Casos borde | ¿Y con lo vacío, lo nulo, el cero, el límite? | Buscar la fila rara del dataset y seguirla a mano |
| 3. Claridad | ¿Se entiende sin ejecutarlo ni preguntar al autor? | Leerlo en voz alta; si necesitas un diagrama mental, falla |
| 4. Complejidad accidental | ¿Cuánto sobra para resolver el problema? | Quitar cosas mentalmente y ver si algo se pierde |
El orden importa: un defecto de corrección invalida cualquier discusión de estilo, así que se revisa de arriba hacia abajo.
El ciclo que vas a practicar es este, y es el mismo que usarás el resto del curso:
El equipo de operaciones pidió una función que resuma las líneas de pedido por categoría. Este es el contrato acordado —tu única fuente de verdad para decidir si el código está bien:
resumen_por_categoria(lineas)devuelve un diccionariocategoría -> {lineas, unidades, ingresos, ticket_promedio}aplicando estas reglas:
- R1. Las líneas con
estado == "cancelado"no cuentan para nada.- R2. Las líneas sin cantidad válida (vacía o
None) no se pueden valorar: tampoco cuentan.- R3.
ingresos= suma deprecio_unitario * cantidad, redondeado a 2 decimales.- R4.
ticket_promedio=ingresosdividido entre las líneas contabilizadas de esa misma categoría, redondeado a 2 decimales.
Un contrato explícito es la mitad de una revisión. Sin él, «revisar» degenera en opinar sobre nombres de variables: no puedes declarar incorrecto un resultado si nadie escribió cuál era el correcto.
Y estos son los datos: ocho líneas de pedido de la tienda, tal cual salen del CSV que exporta el sistema de ventas (nota que cantidad viene como texto, que una fila la trae vacía y que un estado es None).
Léelo entero antes de hacer nada más. No busques el bug todavía: primero entiende qué cree hacer.
PEDIDOS = [
{"order_id": "ORD-1001", "cliente": "Marcela Ríos", "producto": "Audífonos Aura X2",
"categoria": "Audio", "precio_unitario": 189.9, "cantidad": "1",
"fecha": "2025-03-02", "estado": "entregado"},
{"order_id": "ORD-1002", "cliente": "Julián Prada", "producto": "Teclado Nimbus K1",
"categoria": "Accesorios", "precio_unitario": 149.0, "cantidad": "2",
"fecha": "2025-03-02", "estado": "entregado"},
{"order_id": "ORD-1003", "cliente": "Marcela Ríos", "producto": "Portátil Andes 14",
"categoria": "Computo", "precio_unitario": 3890.0, "cantidad": "1",
"fecha": "2025-03-05", "estado": "en_transito"},
{"order_id": "ORD-1004", "cliente": "Sofía Cardona", "producto": "Mouse Nimbus M3",
"categoria": "Accesorios", "precio_unitario": 79.5, "cantidad": "3",
"fecha": "2025-03-07", "estado": "cancelado"},
{"order_id": "ORD-1005", "cliente": "Andrés Bermúdez", "producto": "Audífonos Aura X2",
"categoria": "Audio", "precio_unitario": 189.9, "cantidad": "",
"fecha": "2025-03-09", "estado": None},
{"order_id": "ORD-1006", "cliente": "Sofía Cardona", "producto": "Monitor Cima 27",
"categoria": "Computo", "precio_unitario": 1290.0, "cantidad": "1",
"fecha": "2025-03-11", "estado": "entregado"},
{"order_id": "ORD-1007", "cliente": "Julián Prada", "producto": "Cable USB-C Pro",
"categoria": "Accesorios", "precio_unitario": 39.0, "cantidad": "4",
"fecha": "2025-03-12", "estado": "entregado"},
{"order_id": "ORD-1008", "cliente": "Andrés Bermúdez", "producto": "Portátil Andes 14",
"categoria": "Computo", "precio_unitario": 3890.0, "cantidad": "1",
"fecha": "2025-03-15", "estado": "en_transito"},
]
def resumen_por_categoria(lineas):
"""Resume las líneas de pedido por categoría.
Contrato:
- Ignora las líneas con estado 'cancelado'.
- Ignora las líneas sin cantidad válida (no se pueden valorar).
- ingresos = suma de precio_unitario * cantidad (2 decimales).
- ticket_promedio = ingresos / líneas contabilizadas de esa categoría.
"""
categorias = []
for linea in lineas:
if linea["categoria"] not in categorias:
categorias.append(linea["categoria"])
resumen = {}
for categoria in categorias:
unidades = 0
ingresos = 0.0
contadas = 0
for linea in lineas:
if linea["categoria"] != categoria:
continue
if linea["estado"] == "cancelado":
continue
cantidad = int(linea["cantidad"] or 0)
unidades += cantidad
ingresos += linea["precio_unitario"] * cantidad
contadas += 1
resumen[categoria] = {
"lineas": contadas,
"unidades": unidades,
"ingresos": round(ingresos, 2),
"ticket_promedio": round(ingresos / len(lineas), 2),
}
return resumenEsta es la parte que casi todo el mundo se salta y la que más rinde. Si ejecutas primero, tu cerebro acepta el resultado que ve y deja de comparar; si predices primero, cualquier diferencia te salta a la cara.
La salida se imprimirá con este formato, una línea por categoría y ordenadas alfabéticamente:
Categoria: lineas=N unidades=N ingresos=N.NN ticket=N.NNEscribe tu predicción (en papel, en un comentario, donde sea) para estas tres preguntas antes de tocar la celda siguiente:
ingresos de Computo?ticket_promedio de Computo? Pista de sentido común: los tres pedidos de Computo valen 3890, 1290 y 3890, así que su promedio tiene que caer entre 1290 y 3890.La tercera pregunta es una técnica de revisión en sí misma: antes de calcular nada, define el rango en el que el resultado puede caer. Un promedio nunca puede quedar fuera del mínimo y el máximo de los valores promediados. Si el número sale del rango, hay un bug, y lo sabes sin haber leído una sola línea de la implementación.
Ahora sí: completa la llamada y ejecuta.
Antes de ejecutar, ten escrita tu predicción de las tres preguntas. Luego completa la última asignación para que resumen contenga el resultado de la función sobre PEDIDOS, ejecuta y compara la salida real con lo que predijiste. No modifiques la función ni imprimir: aquí solo confrontamos tu modelo mental con la realidad.
PEDIDOS = [
{"order_id": "ORD-1001", "cliente": "Marcela Ríos", "producto": "Audífonos Aura X2",
"categoria": "Audio", "precio_unitario": 189.9, "cantidad": "1",
"fecha": "2025-03-02", "estado": "entregado"},
{"order_id": "ORD-1002", "cliente": "Julián Prada", "producto": "Teclado Nimbus K1",
"categoria": "Accesorios", "precio_unitario": 149.0, "cantidad": "2",
"fecha": "2025-03-02", "estado": "entregado"},
{"order_id": "ORD-1003", "cliente": "Marcela Ríos", "producto": "Portátil Andes 14",
"categoria": "Computo", "precio_unitario": 3890.0, "cantidad": "1",
"fecha": "2025-03-05", "estado": "en_transito"},
{"order_id": "ORD-1004", "cliente": "Sofía Cardona", "producto": "Mouse Nimbus M3",
"categoria": "Accesorios", "precio_unitario": 79.5, "cantidad": "3",
"fecha": "2025-03-07", "estado": "cancelado"},
{"order_id": "ORD-1005", "cliente": "Andrés Bermúdez", "producto": "Audífonos Aura X2",
"categoria": "Audio", "precio_unitario": 189.9, "cantidad": "",
"fecha": "2025-03-09", "estado": None},
{"order_id": "ORD-1006", "cliente": "Sofía Cardona", "producto": "Monitor Cima 27",
"categoria": "Computo", "precio_unitario": 1290.0, "cantidad": "1",
"fecha": "2025-03-11", "estado": "entregado"},
{"order_id": "ORD-1007", "cliente": "Julián Prada", "producto": "Cable USB-C Pro",
"categoria": "Accesorios", "precio_unitario": 39.0, "cantidad": "4",
"fecha": "2025-03-12", "estado": "entregado"},
{"order_id": "ORD-1008", "cliente": "Andrés Bermúdez", "producto": "Portátil Andes 14",
"categoria": "Computo", "precio_unitario": 3890.0, "cantidad": "1",
"fecha": "2025-03-15", "estado": "en_transito"},
]
def resumen_por_categoria(lineas):
"""Resume las líneas de pedido por categoría (versión a revisar)."""
categorias = []
for linea in lineas:
if linea["categoria"] not in categorias:
categorias.append(linea["categoria"])
resumen = {}
for categoria in categorias:
unidades = 0
ingresos = 0.0
contadas = 0
for linea in lineas:
if linea["categoria"] != categoria:
continue
if linea["estado"] == "cancelado":
continue
cantidad = int(linea["cantidad"] or 0)
unidades += cantidad
ingresos += linea["precio_unitario"] * cantidad
contadas += 1
resumen[categoria] = {
"lineas": contadas,
"unidades": unidades,
"ingresos": round(ingresos, 2),
"ticket_promedio": round(ingresos / len(lineas), 2),
}
return resumen
def imprimir(resumen):
for categoria in sorted(resumen):
d = resumen[categoria]
print(f"{categoria}: lineas={d['lineas']} unidades={d['unidades']} "
f"ingresos={d['ingresos']:.2f} ticket={d['ticket_promedio']:.2f}")
# TODO: guarda en `resumen` lo que devuelve resumen_por_categoria para PEDIDOS
resumen = None
imprimir(resumen)Esto es lo que realmente imprime:
Accesorios: lineas=2 unidades=6 ingresos=454.00 ticket=56.75
Audio: lineas=2 unidades=1 ingresos=189.90 ticket=23.74
Computo: lineas=3 unidades=3 ingresos=9070.00 ticket=1133.75Antes de leer el diagnóstico, contesta estas tres preguntas mirando solo la salida y el contrato. Son la revisión de verdad; lo que viene después es la confirmación.
lineas=2 pero unidades=1. ¿Cómo puede una categoría tener dos líneas contabilizadas y una sola unidad vendida?La magnitud del desvío se ve mejor así:
"ticket_promedio": round(ingresos / len(lineas), 2),lineas es el parámetro de la función: las ocho líneas del archivo completo, incluida la cancelada y las de las otras categorías. El contrato (R4) exige dividir entre las líneas contabilizadas de esa categoría, que la función ya tiene guardadas en contadas, dos líneas más arriba.
Por eso los tres tickets están divididos entre 8 en vez de entre 2, 1 y 3. Es el defecto más caro de los tres —contamina un número que alguien va a llevar a una reunión— y es invisible a simple vista porque len(lineas) es sintácticamente impecable y semánticamente plausible: hay una variable llamada lineas justo ahí.
Regla portátil: cuando revises un promedio, un porcentaje o una tasa, no leas la fórmula: pregunta en voz alta «¿el numerador y el denominador hablan del mismo conjunto?». Es el chequeo que más bugs de corrección atrapa por minuto invertido.
cantidad = int(linea["cantidad"] or 0)ORD-1005 llegó con cantidad vacía: el sistema de ventas no sabe cuántas unidades se pidieron. Como "" es falsy, la expresión "" or 0 evalúa a 0, y la línea entra al resumen como una venta de cero unidades.
No hay excepción, no hay aviso, no hay None en la salida. Simplemente Audio pasa a tener lineas=2 y, cuando arregles el divisor, ese cero fantasma seguiría hundiendo el ticket promedio a la mitad. El contrato (R2) es explícito: una línea sin cantidad válida no se puede valorar y no cuenta.
Este patrón —or 0, or "", or [] sobre un dato de entrada— es uno de los más frecuentes en código generado automáticamente, porque hace que el programa «no falle». Pero no fallar y estar bien son cosas distintas: aquí el código convierte «no sé» en «cero», que es una afirmación que nadie hizo.
Regla portátil: desconfía de todo valor por defecto aplicado a datos de entrada. Pregunta siempre: ¿el ausente y el cero significan lo mismo en este dominio? Casi nunca.
La función recorre los datos una vez para descubrir las categorías y luego una vez completa por cada categoría para filtrar las que no son de esa categoría. Con tres categorías, eso son cuatro pasadas sobre los mismos datos y un continue cuyo único trabajo es descartar el 66 % de las filas.
El resultado no cambia: podrías dejarlo así y la salida sería idéntica. Pero mira lo que costó:
categorias, la variable categoria del bucle externo y un filtro por categoría que no aportan nada al problema;contadas de donde se usa, y ese alejamiento es exactamente lo que hizo verosímil escribir len(lineas).Aquí está la lección menos obvia de todo el módulo: la complejidad accidental no es un problema estético, es el escondite de los otros defectos. Cuanto más código sobra, más superficie hay donde un error plausible pasa por normal.
Un solo recorrido acumulando en un diccionario categoria -> acumulados hace lo mismo con la mitad de líneas, y tiene un efecto colateral bonito: si una categoría no tiene ninguna línea contabilizada, sencillamente no se crea, y la división del ticket nunca puede dividir entre cero. La simplificación elimina un caso borde en lugar de obligarte a defenderlo.
| Eje | Defecto | Síntoma en la salida | Severidad |
|---|---|---|---|
| 1. Corrección | ticket_promedio divide entre len(lineas) (todo el archivo) | Los 3 tickets, fuera del rango posible | Bloqueante |
| 2. Casos borde | int(cantidad or 0) cuenta la línea sin cantidad | Audio: lineas=2 con unidades=1 | Bloqueante |
| 4. Complejidad accidental | Recorre los datos una vez por categoría | Ninguno (la salida es idéntica) | Sugerencia (pero explica los otros dos) |
Con el formato de la lección anterior —qué, por qué importa, qué sugieres— el comentario del defecto de complejidad quedaría así:
Sugerencia (no bloqueante) —
resumen_por_categoria, bucle externo. La función hace una pasada para descubrir categorías y otra pasada completa por categoría, filtrando las filas ajenas. Un solo recorrido acumulando en un dict por categoría produce el mismo resultado con menos estado intermedio, y evita que el contador de líneas quede lejos de donde se usa (que es, de hecho, donde se coló el divisor equivocado del comentario anterior). Como bonus, si una categoría se queda sin líneas válidas simplemente no aparece, en vez de dividir entre cero.
Fíjate en dos cosas: está marcado como no bloqueante (no confundas «yo lo escribiría distinto» con «esto está mal») y conecta la sugerencia con una consecuencia concreta, no con un gusto personal. Un comentario de estilo sin consecuencia se ignora; uno con consecuencia se aplica.
En un minuto vas a corregir la función y un verificador comparará su salida con la esperada. Ese verificador atrapa los defectos 1 y 2, y es estructuralmente incapaz de ver el 3: el doble recorrido produce exactamente la misma salida.
Por eso existen las revisiones humanas. Los tests defienden la corrección y los bordes; la claridad y la complejidad accidental solo las defiende alguien leyendo. Guarda esta idea: la retomamos en el módulo de tests, donde verás que un test verde tampoco demuestra que la corrección esté bien.
or 0) casi siempre esconde un caso borde mal resuelto.Ahora te toca a ti. La celda trae la función tal cual, con sus tres defectos. Tu trabajo es dejarla cumpliendo el contrato R1–R4 para que imprima exactamente esto:
Accesorios: lineas=2 unidades=6 ingresos=454.00 ticket=227.00
Audio: lineas=1 unidades=1 ingresos=189.90 ticket=189.90
Computo: lineas=3 unidades=3 ingresos=9070.00 ticket=3023.33Dos reglas del ejercicio:
PEDIDOS ni imprimir. Corregir los datos de entrada o el formato de salida para que «cuadre» es hacer trampa —y, en un equipo real, es cómo se rompen tres cosas más abajo.Cuando la salida coincida, pulsa Verificar.
Corrige resumen_por_categoria para que cumpla el contrato R1–R4 e imprima exactamente el resumen esperado. No modifiques PEDIDOS ni imprimir. Aunque la verificación solo comprueba la salida, reescribe además el cuerpo con un único recorrido de los datos: es el tercer defecto de la revisión.
PEDIDOS = [
{"order_id": "ORD-1001", "cliente": "Marcela Ríos", "producto": "Audífonos Aura X2",
"categoria": "Audio", "precio_unitario": 189.9, "cantidad": "1",
"fecha": "2025-03-02", "estado": "entregado"},
{"order_id": "ORD-1002", "cliente": "Julián Prada", "producto": "Teclado Nimbus K1",
"categoria": "Accesorios", "precio_unitario": 149.0, "cantidad": "2",
"fecha": "2025-03-02", "estado": "entregado"},
{"order_id": "ORD-1003", "cliente": "Marcela Ríos", "producto": "Portátil Andes 14",
"categoria": "Computo", "precio_unitario": 3890.0, "cantidad": "1",
"fecha": "2025-03-05", "estado": "en_transito"},
{"order_id": "ORD-1004", "cliente": "Sofía Cardona", "producto": "Mouse Nimbus M3",
"categoria": "Accesorios", "precio_unitario": 79.5, "cantidad": "3",
"fecha": "2025-03-07", "estado": "cancelado"},
{"order_id": "ORD-1005", "cliente": "Andrés Bermúdez", "producto": "Audífonos Aura X2",
"categoria": "Audio", "precio_unitario": 189.9, "cantidad": "",
"fecha": "2025-03-09", "estado": None},
{"order_id": "ORD-1006", "cliente": "Sofía Cardona", "producto": "Monitor Cima 27",
"categoria": "Computo", "precio_unitario": 1290.0, "cantidad": "1",
"fecha": "2025-03-11", "estado": "entregado"},
{"order_id": "ORD-1007", "cliente": "Julián Prada", "producto": "Cable USB-C Pro",
"categoria": "Accesorios", "precio_unitario": 39.0, "cantidad": "4",
"fecha": "2025-03-12", "estado": "entregado"},
{"order_id": "ORD-1008", "cliente": "Andrés Bermúdez", "producto": "Portátil Andes 14",
"categoria": "Computo", "precio_unitario": 3890.0, "cantidad": "1",
"fecha": "2025-03-15", "estado": "en_transito"},
]
def resumen_por_categoria(lineas):
"""Resume las líneas de pedido por categoría.
Contrato:
R1. Ignora las líneas con estado 'cancelado'.
R2. Ignora las líneas sin cantidad válida (vacía o None).
R3. ingresos = suma de precio_unitario * cantidad (2 decimales).
R4. ticket_promedio = ingresos / líneas contabilizadas DE ESA categoría.
TODO: corrige los tres defectos (corrección, caso borde y complejidad
accidental) para que la función cumpla el contrato.
"""
categorias = []
for linea in lineas:
if linea["categoria"] not in categorias:
categorias.append(linea["categoria"])
resumen = {}
for categoria in categorias:
unidades = 0
ingresos = 0.0
contadas = 0
for linea in lineas:
if linea["categoria"] != categoria:
continue
if linea["estado"] == "cancelado":
continue
cantidad = int(linea["cantidad"] or 0)
unidades += cantidad
ingresos += linea["precio_unitario"] * cantidad
contadas += 1
resumen[categoria] = {
"lineas": contadas,
"unidades": unidades,
"ingresos": round(ingresos, 2),
"ticket_promedio": round(ingresos / len(lineas), 2),
}
return resumen
def imprimir(resumen):
for categoria in sorted(resumen):
d = resumen[categoria]
print(f"{categoria}: lineas={d['lineas']} unidades={d['unidades']} "
f"ingresos={d['ingresos']:.2f} ticket={d['ticket_promedio']:.2f}")
imprimir(resumen_por_categoria(PEDIDOS))Free