Un script Python lento no siempre necesita una técnica avanzada. Muchas veces el problema está en un bucle innecesario, una estructura de datos incorrecta, demasiadas lecturas de disco, solicitudes de red repetidas o cálculos que se ejecutan una y otra vez. La forma profesional de mejorar el rendimiento consiste en medir, identificar el cuello de botella y cambiar solo lo que realmente importa.
Primero define qué significa lento
Antes de optimizar, establece un escenario reproducible. ¿El script tarda diez segundos, diez minutos o varias horas? ¿La lentitud aparece al iniciar, durante una consulta, al procesar archivos o al guardar resultados? Sin una medición inicial no podrás demostrar que una modificación mejoró el programa.
Medir con perf_counter
Para una medición sencilla:
from time import perf_counter
inicio = perf_counter()
ejecutar_tarea()
duracion = perf_counter() - inicio
print(f"Duración: {duracion:.3f} segundos")Repite varias veces con la misma entrada. Una sola ejecución puede estar afectada por cachés, procesos del sistema o latencia externa.
Localizar cuellos de botella con cProfile
Cuando el programa contiene muchas funciones, utiliza un perfilador:
python -m cProfile -s cumulative mi_script.pyLa salida indica cuántas veces se llamó cada función, el tiempo interno y el tiempo acumulado. La guía de cProfile en Python explica cómo interpretar estos valores. La documentación oficial de los perfiladores describe cProfile, profile y pstats.
Usar timeit para fragmentos pequeños
timeit sirve para comparar alternativas cortas evitando parte del ruido:
from timeit import repeat
resultados = repeat(
"sum(range(1000))",
repeat=5,
number=10_000,
)
print(min(resultados))No utilices un microbenchmark para concluir cómo se comportará una aplicación completa. La documentación oficial de timeit explica parámetros, línea de comandos y buenas prácticas.
Elegir la estructura de datos correcta
Buscar repetidamente en una lista puede ser costoso. Si solo necesitas comprobar pertenencia, un conjunto suele ser más apropiado:
usuarios_validos = {"ana", "luis", "marta"}
if nombre in usuarios_validos:
autorizar()Listas, conjuntos y diccionarios tienen propiedades diferentes. La guía de listas en Python ayuda a elegir una colección adecuada.
Evitar trabajo repetido dentro de bucles
Mueve fuera del bucle los cálculos que no cambian:
# menos eficiente
for producto in productos:
impuesto = obtener_tasa_configurada()
guardar(producto.precio * impuesto)
# mejor
impuesto = obtener_tasa_configurada()
for producto in productos:
guardar(producto.precio * impuesto)La mejora real depende del costo de obtener_tasa_configurada, pero el principio es evitar repetir trabajo constante.
Usar funciones incorporadas
Funciones como sum, min, max, any y all suelen ser claras y eficientes:
total = sum(venta.total for venta in ventas)
existe_error = any(item.error for item in resultados)No reemplaces código legible por trucos difíciles de mantener solo por una diferencia mínima.
Comprensiones y generadores
Una comprensión puede ser más compacta que un bucle que solo construye una lista:
cuadrados = [n * n for n in numeros]Si solo recorrerás los valores una vez, un generador evita almacenar toda la colección:
cuadrados = (n * n for n in numeros)
for valor in cuadrados:
procesar(valor)Esto es especialmente importante cuando existe riesgo de MemoryError.
Reducir operaciones de entrada y salida
Leer y escribir en disco suele ser mucho más lento que una operación en memoria. En lugar de abrir un archivo por cada línea, abre una sola vez:
with open("salida.txt", "w", encoding="utf-8") as archivo:
for linea in lineas:
archivo.write(linea + "\n")Para bases de datos, agrupa inserciones y evita una consulta por elemento. Para APIs, considera paginación, reutilización de conexiones y límites de concurrencia.
No confundir CPU con espera
Una tarea intensiva en CPU necesita mejores algoritmos, bibliotecas optimizadas o procesos paralelos. Una tarea que espera red, disco o base de datos puede beneficiarse de concurrencia o asincronía. Aplicar asyncio a cálculos pesados no los vuelve automáticamente más rápidos.
Complejidad algorítmica
Un algoritmo cuadrático puede funcionar con cien elementos y colapsar con un millón:
# comparación repetida costosa
repetidos = []
for valor in datos:
if datos.count(valor) > 1:
repetidos.append(valor)
# enfoque más apropiado
from collections import Counter
conteos = Counter(datos)
repetidos = [valor for valor, cantidad in conteos.items() if cantidad > 1]La biblioteca estándar ofrece soluciones preparadas para muchos patrones. El artículo sobre itertools muestra cómo construir iteraciones eficientes.
Caché con límites
Si una función pura repite cálculos con los mismos argumentos, lru_cache puede ayudar:
from functools import lru_cache
@lru_cache(maxsize=256)
def calcular_precio(codigo):
return consulta_costosa(codigo)La caché debe tener un límite y una estrategia coherente. Una caché ilimitada puede convertirse en un problema de memoria.
NumPy y bibliotecas compiladas
Para grandes operaciones numéricas, NumPy suele ejecutar bucles en código compilado. Evita aplicar una función Python elemento por elemento cuando exista una operación vectorizada. Sin embargo, medir sigue siendo necesario: crear muchas copias de arrays también tiene costo.
Python compilado y Cython
Cuando el cuello de botella está en cálculos Python que no pueden vectorizarse, Cython puede añadir tipos estáticos y compilar partes críticas. Revisa la guía para convertir Python con Cython. Esta opción añade complejidad de compilación y no debe ser el primer paso.
Funciones pequeñas y medibles
Separar el programa en funciones claras permite perfilar cada etapa, probar cambios y evitar efectos inesperados. Una función gigantesca dificulta saber dónde se consume el tiempo.
Errores frecuentes al optimizar
Los errores más comunes son optimizar sin medir, elegir siempre la ejecución más rápida de una prueba inestable, ignorar la base de datos, sacrificar legibilidad por microsegundos y aumentar concurrencia sin límites. También es un error comparar dos alternativas con entradas diferentes.
Flujo recomendado
Define una carga representativa, registra el tiempo, perfila el programa, selecciona el cuello de botella principal y formula una hipótesis. Haz un cambio pequeño, ejecuta pruebas de corrección y vuelve a medir. Conserva el cambio solo si aporta una mejora relevante sin deteriorar mantenimiento o memoria.
Un script rápido no nace de aplicar todos los trucos disponibles. Nace de eliminar el trabajo innecesario en la parte que realmente domina el tiempo total. Medir antes y después convierte la optimización en ingeniería, no en intuición.






