La legibilidad es una de las primeras señales de calidad. Un código legible permite entender qué hace, por qué lo hace y dónde cambiarlo cuando aparece un nuevo requisito. En Python esto es especialmente importante, porque el lenguaje favorece soluciones expresivas y directas.
En este tema trabajaremos con nombres, intención y estructura visual. Haremos mejoras pequeñas sobre el proyecto ventas_demo creado en el tema anterior, ejecutando las pruebas para comprobar que el comportamiento se mantiene.
Un error común es pensar que la legibilidad es un detalle estético. En realidad, afecta directamente el costo de mantenimiento. Cuando un nombre es confuso o una función está mal organizada, cada cambio exige más esfuerzo mental.
Observa esta línea:
t = t + x["precio"] * x["cant"]
La operación es simple, pero la intención no está completa. Sabemos que se suma algo, pero no queda claro qué representa t, qué representa x ni por qué la clave se llama cant.
En el tema anterior dejamos esta función en src/ventas.py:
def proc(items, cliente, pais):
t = 0
for x in items:
if x["cant"] > 0:
t = t + x["precio"] * x["cant"]
if cliente == "vip":
t = t - (t * 0.15)
else:
if cliente == "regular":
t = t - (t * 0.05)
if pais == "AR":
t = t + (t * 0.21)
else:
if pais == "UY":
t = t + (t * 0.22)
else:
t = t + (t * 0.19)
if t < 10000:
t = t + 1500
return round(t, 2)
Antes de mejorarla, ejecuta las pruebas de caracterización:
python -m pytest
Las pruebas deben pasar. Si fallan, corrige primero el proyecto base antes de avanzar.
Los nombres deben decir qué representa algo en el problema, no solo qué tipo de dato tiene. Por ejemplo, items puede ser una lista, pero en nuestro dominio son productos o líneas de venta.
Una primera mejora es cambiar nombres generales por nombres del dominio:
proc puede ser calcular_total_venta.items puede ser productos.x puede ser producto.t puede ser total o subtotal según el momento del cálculo.cant puede ser cantidad.Si cambiamos proc por calcular_total_venta, las pruebas anteriores dejarán de importar el nombre viejo. Durante una transición podemos conservar un alias temporal.
def calcular_total_venta(productos, cliente, pais):
total = 0
for producto in productos:
if producto["cant"] > 0:
total = total + producto["precio"] * producto["cant"]
if cliente == "vip":
total = total - (total * 0.15)
else:
if cliente == "regular":
total = total - (total * 0.05)
if pais == "AR":
total = total + (total * 0.21)
else:
if pais == "UY":
total = total + (total * 0.22)
else:
total = total + (total * 0.19)
if total < 10000:
total = total + 1500
return round(total, 2)
proc = calcular_total_venta
El alias proc permite que las pruebas viejas sigan pasando. Más adelante podemos actualizar las pruebas y eliminar el alias.
Después del cambio anterior, ejecuta:
python -m pytest
La mejora es pequeña, pero conviene comprobarla. Este hábito permite avanzar con seguridad: cambiar un aspecto, verificar, continuar.
Si una prueba falla, el cambio no fue solamente de legibilidad. En ese caso debemos revisar si modificamos el comportamiento por accidente.
Las abreviaturas pueden ahorrar algunos caracteres, pero muchas veces agregan ambigüedad. En nuestro ejemplo, cant se entiende con esfuerzo, pero cantidad es directo.
Podemos cambiar los datos de prueba y el código para usar cantidad. Primero actualiza las pruebas:
items = [
{"precio": 3000, "cantidad": 2},
{"precio": 1500, "cantidad": 1},
]
Luego cambia el acceso en la función:
for producto in productos:
if producto["cantidad"] > 0:
total = total + producto["precio"] * producto["cantidad"]
Cuando los datos pertenecen a nuestro propio proyecto, conviene preferir nombres completos y claros. Si vienen de una API externa, a veces debemos adaptar o traducir esos nombres en una capa separada.
Los números sueltos dificultan entender reglas de negocio. No sabemos si 0.21 es IVA, comisión, recargo o cualquier otra cosa. Para mejorar la legibilidad podemos crear constantes.
DESCUENTO_VIP = 0.15
DESCUENTO_REGULAR = 0.05
IMPUESTO_ARGENTINA = 0.21
IMPUESTO_URUGUAY = 0.22
IMPUESTO_OTRO_PAIS = 0.19
LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500
Con constantes, el lector entiende el significado de cada valor sin buscar documentación externa.
La estructura visual permite distinguir bloques de lógica. En una función con varios pasos, podemos separar mentalmente las etapas con líneas en blanco y nombres coherentes.
def calcular_total_venta(productos, cliente, pais):
subtotal = 0
for producto in productos:
if producto["cantidad"] > 0:
subtotal += producto["precio"] * producto["cantidad"]
total_con_descuento = subtotal
if cliente == "vip":
total_con_descuento -= subtotal * DESCUENTO_VIP
elif cliente == "regular":
total_con_descuento -= subtotal * DESCUENTO_REGULAR
total_con_impuesto = total_con_descuento
if pais == "AR":
total_con_impuesto += total_con_descuento * IMPUESTO_ARGENTINA
elif pais == "UY":
total_con_impuesto += total_con_descuento * IMPUESTO_URUGUAY
else:
total_con_impuesto += total_con_descuento * IMPUESTO_OTRO_PAIS
total_final = total_con_impuesto
if total_final < LIMITE_ENVIO_GRATIS:
total_final += COSTO_ENVIO
return round(total_final, 2)
Todavía hay oportunidades de mejora, pero esta versión ya es más legible que el punto de partida.
El anidamiento profundo hace que el lector mantenga demasiadas condiciones en la cabeza. En Python, muchas veces un elif expresa mejor una serie de alternativas.
Versión más ruidosa:
if pais == "AR":
total = total + (total * 0.21)
else:
if pais == "UY":
total = total + (total * 0.22)
else:
total = total + (total * 0.19)
Versión más clara:
if pais == "AR":
total += total * IMPUESTO_ARGENTINA
elif pais == "UY":
total += total * IMPUESTO_URUGUAY
else:
total += total * IMPUESTO_OTRO_PAIS
Cuando el código ya tiene un nombre más claro, podemos actualizar las pruebas para importar la función definitiva:
from ventas import calcular_total_venta
Y cambiar los llamados:
assert calcular_total_venta(productos, "vip", "AR") == 9213.75
Después ejecuta:
python -m pytest
Si todas las pruebas pasan, puedes eliminar el alias temporal proc = calcular_total_venta.
El archivo src/ventas.py puede quedar así al finalizar este tema:
DESCUENTO_VIP = 0.15
DESCUENTO_REGULAR = 0.05
IMPUESTO_ARGENTINA = 0.21
IMPUESTO_URUGUAY = 0.22
IMPUESTO_OTRO_PAIS = 0.19
LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500
def calcular_total_venta(productos, cliente, pais):
subtotal = 0
for producto in productos:
if producto["cantidad"] > 0:
subtotal += producto["precio"] * producto["cantidad"]
total_con_descuento = subtotal
if cliente == "vip":
total_con_descuento -= subtotal * DESCUENTO_VIP
elif cliente == "regular":
total_con_descuento -= subtotal * DESCUENTO_REGULAR
total_con_impuesto = total_con_descuento
if pais == "AR":
total_con_impuesto += total_con_descuento * IMPUESTO_ARGENTINA
elif pais == "UY":
total_con_impuesto += total_con_descuento * IMPUESTO_URUGUAY
else:
total_con_impuesto += total_con_descuento * IMPUESTO_OTRO_PAIS
total_final = total_con_impuesto
if total_final < LIMITE_ENVIO_GRATIS:
total_final += COSTO_ENVIO
return round(total_final, 2)
El archivo tests/test_ventas.py puede quedar así:
from ventas import calcular_total_venta
def test_calcula_total_para_cliente_vip_en_argentina():
productos = [
{"precio": 3000, "cantidad": 2},
{"precio": 1500, "cantidad": 1},
]
assert calcular_total_venta(productos, "vip", "AR") == 9213.75
def test_agrega_envio_cuando_el_total_es_bajo():
productos = [
{"precio": 1000, "cantidad": 1},
]
assert calcular_total_venta(productos, "regular", "UY") == 2659.0
def test_ignora_items_con_cantidad_cero_o_negativa():
productos = [
{"precio": 3000, "cantidad": 1},
{"precio": 9000, "cantidad": 0},
{"precio": 9000, "cantidad": -2},
]
assert calcular_total_venta(productos, "nuevo", "CL") == 5070.0
Analiza este fragmento y mejora solamente la legibilidad. No cambies la lógica de negocio.
def g(us):
r = []
for u in us:
if u["a"] == 1 and u["e"] != "":
r.append(u["e"])
return r
Realiza estas tareas:
Una solución posible es:
def obtener_emails_de_usuarios_activos(usuarios):
emails = []
for usuario in usuarios:
if usuario["activo"] == 1 and usuario["email"] != "":
emails.append(usuario["email"])
return emails
usuarios = [
{"activo": 1, "email": "ana@example.com"},
{"activo": 0, "email": "luis@example.com"},
{"activo": 1, "email": ""},
]
print(obtener_emails_de_usuarios_activos(usuarios))
Más adelante podríamos mejorar también el uso de 1 como indicador de usuario activo, pero en este ejercicio nos concentramos en nombres e intención.
Antes de continuar, revisa si puedes aplicar estas ideas:
En este tema mejoramos la legibilidad sin cambiar el comportamiento del programa. Usamos nombres más expresivos, constantes para valores importantes, menor anidamiento y una estructura visual más fácil de seguir.
En el próximo tema veremos convenciones de estilo en Python y aplicaremos PEP 8 con ejemplos concretos para reforzar la consistencia del código.