Fundamentos de Rendimiento
11 ejemplos para empezar con Rendimiento: 8 básicos y 3 intermedios.
Prerrequisitos
- Python 3.14.0
- Módulos de la biblioteca estándar:
timeit,cProfile,tracemalloc - Opcional:
uv add numpypara ejemplos de vectorización
Ejemplos Básicos
1. Mide Antes de Optimizar
Nunca optimices sin una medición de referencia.
import time
def slow_sum(n: int) -> int:
return sum(range(n))
start = time.perf_counter()
result = slow_sum(1_000_000)
elapsed = time.perf_counter() - start
print(f"Resultado: {result}, Tiempo: {elapsed:.4f}s")time.perf_counter()es monotónico y de alta resolución- Registra la referencia antes de cualquier intento de optimización
- Compara después de los cambios para verificar la mejora
- Si la medición es ruidosa, ejecuta múltiples iteraciones
Relacionado: timeit y Microbenchmarks - temporización precisa
2. timeit para Microbenchmarks
El módulo timeit ejecuta código en bucles ajustados para una temporización estable.
import timeit
elapsed = timeit.timeit("sum(range(1000))", number=10000)
print(f"Promedio: {elapsed / 10000 * 1e6:.2f} μs")numbercontrola las iteraciones del bucle- Aísla la instrucción de la sobrecarga de configuración
- Úsalo para comparar dos enfoques en pequeños caminos de código
- No es adecuado para operaciones limitadas por E/S
Relacionado: timeit y Microbenchmarks - metodología
3. cProfile para Puntos Calientes de CPU
Encuentra qué funciones consumen más tiempo de CPU.
import cProfile
import pstats
def work():
sum(i * i for i in range(100_000))
cProfile.run("work()", "profile.out")
stats = pstats.Stats("profile.out")
stats.sort_stats("cumulative").print_stats(10)cumulativeordena por tiempo total, incluidas las subllamadas- Las entradas superiores son candidatas a optimización
- Perfila cargas de trabajo reales, no ejemplos triviales
- Guarda las estadísticas en un archivo para compararlas entre ejecuciones
Relacionado: cProfile y Profilers - grafos de llama
4. tracemalloc para Memoria
Rastrea las asignaciones de memoria para encontrar fugas y desbordamientos.
import tracemalloc
tracemalloc.start()
data = [bytearray(1024) for _ in range(10_000)]
snapshot = tracemalloc.take_snapshot()
top = snapshot.statistics("lineno")[:5]
for stat in top:
print(stat)
tracemalloc.stop()tracemalloces de la biblioteca estándar, no necesita instalación- Muestra qué línea asignó la mayor cantidad de memoria
- Compara instantáneas antes/después para encontrar fugas
- Menos pesado que memray para comprobaciones rápidas
Relacionado: Profiling de Memoria - inmersión profunda en memray
5. Comprensión de Listas vs Bucle
Las comprensiones son más rápidas para transformaciones simples.
# Más rápido
squares = [x * x for x in range(100_000)]
# Más lento
squares = []
for x in range(100_000):
squares.append(x * x)- Las comprensiones evitan búsquedas repetidas de
list.append - Para lógica compleja, la legibilidad puede superar la ganancia de velocidad
- Perfila antes de asumir que la comprensión gana
- Las expresiones generadoras ahorran memoria para secuencias grandes
6. functools.lru_cache
Almacena en caché los resultados de funciones puras para evitar cálculos redundantes.
from functools import lru_cache
@lru_cache(maxsize=256)
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # instantáneo con caché- Solo funciona para funciones con argumentos hasheables
maxsizelimita el uso de memoria- Seguro para hilos en CPython
- Usa
.cache_info()para inspeccionar la tasa de aciertos
Relacionado: Caché y Memoización - estrategias de caché
7. Generador vs Lista
Los generadores producen elementos de forma perezosa, ahorrando memoria en grandes conjuntos de datos.
def read_lines(path: str):
with open(path) as f:
for line in f:
yield line.strip()
# Procesa una línea a la vez - memoria constante
for line in read_lines("huge.log"):
if "ERROR" in line:
print(line)- Las listas cargan todo en memoria
- Los generadores procesan un elemento a la vez
- Usa generadores para E/S de archivos y etapas de pipeline
sum(1 for _ in gen)aún consume el generador una vez
8. Evita la Búsqueda Repetida de Atributos
Vincula los atributos accedidos frecuentemente a variables locales en bucles intensivos.
# Más rápido en bucles intensivos
append = results.append
for item in data:
append(transform(item))
# Más lento - búsqueda repetida de atributos
for item in data:
results.append(transform(item))- El acceso a variables locales es más rápido que la búsqueda de atributos
- Solo importa en los bucles internos intensivos
- Compromiso de legibilidad: aplícalo solo donde se haya perfilado
- El Python moderno optimiza muchos casos automáticamente
Ejemplos Intermedios
9. Vectorización con NumPy
Reemplaza los bucles de Python con operaciones de arrays para trabajo numérico.
import numpy as np
data = np.arange(1_000_000, dtype=np.float64)
result = np.sqrt(data) * 2.0 + 1.0 # vectorizado
# vs: [math.sqrt(x) * 2.0 + 1.0 for x in data] # 10-100x más lento- Las operaciones de NumPy se ejecutan en C/Fortran internamente
- Requiere datos numéricos contiguos
- pandas y Polars se basan en una vectorización similar
- La sobrecarga de conversión importa para arrays pequeños
Relacionado: Vectorización con NumPy - guía completa
10. asyncio para Trabajo Limitado por E/S
Paraleliza la espera en red/disco sin hilos.
import asyncio
import httpx
async def fetch_all(urls: list[str]) -> list[int]:
async with httpx.AsyncClient() as client:
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
return [r.status_code for r in responses]
asyncio.run(fetch_all(["https://example.com"] * 10))- asyncio ayuda cuando el código espera por E/S, no por CPU
- No evita el GIL para trabajo limitado por CPU
- Usa
asyncio.gatherpara corrutinas concurrentes - Para paralelismo de CPU, usa
multiprocessingoconcurrent.futures
Relacionado: Async y Concurrencia para Rendimiento - patrones
11. Flujo de Trabajo de Auditoría de Rendimiento
Un pase sistemático antes de afirmar que algo está "optimizado".
# 1. Mide la referencia
# 2. Perfila (cProfile o py-spy)
# 3. Corrige solo el punto caliente principal
# 4. Vuelve a medir
# 5. Documenta los números de antes y después
baseline_ms = 450
optimized_ms = 120
improvement = (baseline_ms - optimized_ms) / baseline_ms * 100
print(f"Mejora: {improvement:.0f}%") # 73%- Optimiza el cuello de botella medido, no una suposición
- Un cambio a la vez para una atribución clara
- Mantén un registro de las decisiones de optimización
- Detente cuando el rendimiento cumpla los requisitos
Relacionado: Lista de Verificación de Auditoría de Rendimiento - lista completa
Versiones de la Pila: Esta página fue escrita para Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.