Python prioriza la legibilidad, la productividad y un ecosistema amplio. Esa elección puede hacerlo más lento que un programa compilado en determinadas tareas, pero no significa que todos los proyectos necesiten optimización. La mayoría de aplicaciones pierde tiempo en algoritmos inadecuados, consultas repetidas, acceso a disco, red o estructuras de datos incorrectas. Antes de cambiar de lenguaje, mide el problema y optimiza la parte que realmente consume recursos.
Para profundizar, consulta la guía de cProfile en Python, el artículo sobre timeit, la introducción a NumPy, la guía de list comprehensions y el tutorial sobre sets.
Las referencias externas principales son la documentación oficial de los profilers de Python y la documentación oficial de timeit.
Python no siempre es el cuello de botella
Un script puede parecer lento porque espera una API, abre miles de archivos pequeños, ejecuta consultas individuales o descarga datos sin reutilizar conexiones. En esos casos, la velocidad del intérprete tiene menos importancia que la arquitectura del proceso.
Medir el tiempo total
from time import perf_counter
inicio = perf_counter()
resultado = ejecutar_proceso()
duracion = perf_counter() - inicio
print(f"Duración: {duracion:.3f} segundos")
perf_counter() es apropiado para medir intervalos cortos. Repite la prueba con datos representativos y evita sacar conclusiones a partir de una única ejecución.
Perfilar con cProfile
python -m cProfile -s cumulative programa.py
El informe muestra cantidad de llamadas y tiempo acumulado por función. Ordenar por tiempo acumulado ayuda a detectar funciones que llaman a otras operaciones costosas.
import cProfile
import pstats
with cProfile.Profile() as perfil:
ejecutar_proceso()
estadisticas = pstats.Stats(perfil)
estadisticas.sort_stats("cumulative").print_stats(20)
No optimizar sin evidencia
Si una función representa menos del uno por ciento del tiempo total, hacerla dos veces más rápida apenas cambia la experiencia. Prioriza las operaciones dominantes y conserva pruebas para asegurar que la optimización no modifica los resultados.
Elegir un algoritmo mejor
def contiene_duplicados(valores):
vistos = set()
for valor in valores:
if valor in vistos:
return True
vistos.add(valor)
return False
Buscar repetidamente en un set suele ser mucho más eficiente que recorrer una lista completa para cada elemento. Cambiar la estructura puede producir una mejora mayor que reescribir el mismo bucle.
Evitar bucles anidados innecesarios
# Enfoque costoso
coincidencias = []
for usuario in usuarios:
for permiso in permisos:
if usuario["permiso_id"] == permiso["id"]:
coincidencias.append((usuario, permiso))
Crea un índice:
permisos_por_id = {p["id"]: p for p in permisos}
coincidencias = []
for usuario in usuarios:
permiso = permisos_por_id.get(usuario["permiso_id"])
if permiso is not None:
coincidencias.append((usuario, permiso))
Utilizar funciones integradas
total = sum(valores)
mayor = max(valores)
ordenados = sorted(valores)
Las funciones integradas suelen estar optimizadas y expresan mejor la intención que una implementación manual. No reemplaces código claro por trucos difíciles de leer para ahorrar una cantidad insignificante.
Comprensiones con moderación
cuadrados_pares = [n * n for n in numeros if n % 2 == 0]
Una comprensión sencilla puede ser más compacta y eficiente que añadir elementos manualmente. Si contiene varias condiciones, llamadas o bucles anidados, una función y un bucle normal serán más mantenibles.
Generadores para reducir memoria
cuadrados = (n * n for n in range(10_000_000))
for valor in cuadrados:
procesar(valor)
El generador produce valores bajo demanda. Esto no siempre reduce el tiempo, pero evita construir una lista enorme y puede permitir que el proceso comience antes.
Procesar archivos por líneas
with open("registro.log", encoding="utf-8") as archivo:
for linea in archivo:
analizar(linea)
Leer todo con read() puede consumir memoria y provocar pausas. Para transformaciones tabulares grandes, utiliza bloques o herramientas diseñadas para streaming.
Reducir operaciones de entrada y salida
# Mejor que escribir una línea abriendo el archivo cada vez
with open("salida.txt", "w", encoding="utf-8") as archivo:
archivo.writelines(lineas)
Agrupa escrituras y reutiliza conexiones. El acceso a disco y red suele ser mucho más lento que operaciones en memoria.
Reutilizar sesiones HTTP
import requests
with requests.Session() as sesion:
for url in urls:
respuesta = sesion.get(url, timeout=20)
respuesta.raise_for_status()
Una sesión puede reutilizar conexiones. Respeta límites del servicio y evita paralelizar solicitudes sin autorización.
Optimizar consultas a bases de datos
Evita una consulta por cada fila. Utiliza parámetros, operaciones por lotes, índices adecuados y selecciona solo las columnas necesarias. Mide el plan de ejecución en la base de datos; modificar Python no arregla una consulta mal diseñada.
Caché para resultados repetidos
from functools import lru_cache
@lru_cache(maxsize=256)
def calcular_configuracion(clave):
return operacion_costosa(clave)
La caché funciona cuando la misma entrada produce el mismo resultado y se repite. No almacenes indefinidamente datos enormes, sensibles o que cambian con frecuencia. Revisa cache_info() para confirmar que existen aciertos.
NumPy para operaciones numéricas
import numpy as np
valores = np.array([1, 2, 3, 4], dtype=np.float64)
resultado = valores * 1.18
NumPy ejecuta operaciones vectorizadas en código compilado y es apropiado para arrays numéricos homogéneos. Convertir listas pequeñas a arrays en cada función puede añadir más coste que beneficio.
Pandas sin apply innecesario
df["total"] = df["cantidad"] * df["precio"]
Las operaciones vectorizadas suelen ser preferibles a aplicar una función Python por fila. Para archivos grandes, limita columnas, define tipos y procesa en bloques.
Concurrencia para tareas de espera
Threads y asyncio pueden mejorar tareas que esperan red, disco o servicios externos. No aceleran automáticamente cálculos intensivos de CPU debido a limitaciones del intérprete tradicional y al coste de coordinación.
Multiprocessing para CPU
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as ejecutor:
resultados = list(ejecutor.map(calculo_intensivo, datos))
Los procesos permiten utilizar varios núcleos, pero deben serializar datos y consumen memoria. Utilízalos para tareas suficientemente grandes, independientes y medibles.
Compilar partes críticas
Cython, Numba, extensiones en C o Rust y otras herramientas pueden acelerar bucles numéricos críticos. Son una etapa posterior: primero mejora algoritmo, estructura y acceso a datos. Añadir una compilación aumenta complejidad de desarrollo y distribución.
Comparar con timeit
from timeit import repeat
mediciones = repeat(
"sum(range(1000))",
repeat=5,
number=10_000,
)
print(min(mediciones))
Compara funciones equivalentes con los mismos datos. No incluyas configuración, impresión o lectura de archivos si solo quieres medir el núcleo del algoritmo.
Vigilar la memoria
Un programa puede volverse lento por paginación de memoria, copias repetidas o acumulación de objetos. Usa generadores, libera referencias innecesarias y evita duplicar DataFrames enormes. La solución no es llamar constantemente al recolector de basura.
Establecer un objetivo
Define qué significa “suficientemente rápido”: tiempo máximo de respuesta, cantidad de archivos por minuto o consumo de memoria. Sin un objetivo, la optimización puede continuar indefinidamente sin valor para el usuario.
Errores frecuentes
Los errores habituales son optimizar antes de perfilar, comparar una sola ejecución, utilizar time.time() para mediciones pequeñas, sustituir claridad por trucos, paralelizar tareas diminutas y creer que una comprensión siempre es más rápida. También es común ignorar base de datos, red y disco.
Conclusión
Python puede ser más lento en ciertos cálculos, pero la mayoría de mejoras comienza con medición, algoritmos y estructuras de datos. Perfila con cProfile, compara microoperaciones con timeit, reduce I/O y utiliza NumPy o concurrencia cuando el tipo de trabajo lo justifique. Optimiza únicamente las secciones que dominan el tiempo total y conserva pruebas para garantizar resultados correctos.






