Python Avanzado Rendimiento explicado simplemente (con diagramas y código real)
Python Avanzado Rendimiento: lo esencial en un artículo — código real, diagramas y pasos concretos, extractos de un curso de 35 lecciones.
Una guía que va al grano: Python Avanzado Rendimiento diseccionado con diagramas, ejemplos concretos y comandos probados. Todo proviene de un curso estructurado de 11 capítulos — aquí lo mejor.
- Introducción e Instalación
- Perfilado del código
- Comprensiones, iteradores y generadores
- Multithreading vs Multiprocessing
- asyncio y corrutinas
Presentación del curso
Objetivos pedagógicos
El problema concreto
¿Ya has vivido esta escena? Lanzás tu script Python a las 17 h para calcular un informe, te vas a buscar un café y cuando volvés 30 minutos después, el script sigue ejecutándose todavía. Peor: tu compañero que hace lo mismo en R o en Julia terminó en 3 minutos.
Síntomas típicos de un programa Python lento
Lo que vas a aprender a hacer
Los 3 grandes ejes de optimización
| Eje | Pregunta planteada | Ganancia típica |
|---|---|---|
| 1. Algoritmo | ¿Mi código está en O(n²) cuando podría estar en O(n log n)? | 10× a 10 000× |
| 2. Estructura de datos | ¿Estoy usando una list cuando un set bastaría? |
10× a 1000× |
| 3. Concurrencia / paralelismo | ¿Puedo ejecutar estas 8 tareas al mismo tiempo? | 2× a 16× (según CPU/IO) |
La regla del 80/20 (Pareto)
En el 80 % de los programas, el 80 % del tiempo de ejecución se pasa en el 20 % del código. A menudo es incluso 90/10 o 95/5.
Donald Knuth, leyenda de la informática, lo resumió así en 1974:
(La optimización prematura es la raíz de todos los males.)
Traducción práctica: escribí primero código claro y correcto. Disfrutalo. Si es demasiado lento, optimizá solo las porciones calientes. De lo contrario, perdés tiempo volviendo ilegible un código que no afectaba a nadie.
Un antes/después que habla
Aquí un ejemplo real: sumar los cuadrados de los números del 0 al 10 millones.
# Versión ingenua: bucle Python total = 0 for i in range(10_000_000): total += i * i # Tiempo: ~1.2 segundos en una laptop moderna # Versión vectorizada con NumPy import numpy as np arr = np.arange(10_000_000) total = (arr * arr).sum() # Tiempo: ~0.04 segundos -> 30× más rápido # Versión compilada con Numba @jit from numba import jit @jit(nopython=True) def somme_carres(n): total = 0 for i in range(n): total += i * i return total # Tiempo: ~0.01 segundos -> 120× más rápido
Mismo problema, mismo lenguaje, mismo resultado matemático — pero 120 veces más rápido. Es exactamente lo que vas a aprender a hacer, de forma sistemática, en tu propio código.
Lo que vas a construir
Fase 1: Medir (cap. 0-1)
Instalar las herramientas, hacer tu primer perfil, saber leer un informe de cProfile. Podrás identificar el cuello de botella en menos de 5 minutos.
Fase 2: Optimizar (cap. 2-7)
Generadores, threading, multiprocessing, asyncio, NumPy, Numba, Cython, caché. Todo el arsenal moderno del desarrollador Python.
Generadores con yield
yield, la palabra clave que transforma una función en generador, y aprender a construir pipelines de procesamiento de datos perezosos capaces de manejar archivos de varios gigabytes con solo unos megabytes de RAM.Objetivos pedagógicos
Un primer generador
def compter(max): n = 0 while n < max: yield n # suspende la función y devuelve n n += 1 # Llamada: no hace NADA, obtenemos un generador g = compter(5) print(type(g)) # <class 'generator'> # Consumo for n in g: print(n) # 0 1 2 3 4
yield, Python la convierte en una fábrica de generadores. La llamada no ejecuta el código: devuelve un objeto generador. El código solo se ejecuta en cada llamada a next().yield vs return
| return | yield |
|---|---|
| Termina la función | Suspende la función |
| El estado se pierde | El estado se conserva |
| Devuelve un valor (una vez) | Puede llamarse varias veces |
| Devuelve todo de una vez | Devuelve un elemento a la vez |
Caso de uso real: leer un archivo de log grande
def lire_log(chemin): """Generador que yield cada línea, sin cargar todo.""" with open(chemin, encoding="utf-8") as f: for ligne in f: yield ligne.rstrip("\n") def filtrer_erreurs(lignes): """Generador que solo conserva las líneas ERROR.""" for ligne in lignes: if "ERROR" in ligne: yield ligne def extraire_codes(lignes): """Generador que yield el código HTTP de cada línea.""" for ligne in lignes: try: code = int(ligne.split()[-1]) yield code except (ValueError, IndexError): continue # Pipeline: ninguna de las etapas consume RAM, incluso para 50 GB lignes = lire_log("acces.log") erreurs = filtrer_erreurs(lignes) codes = extraire_codes(erreurs) # Consumo final from collections import Counter print(Counter(codes).most_common(5)) # [(500, 1284), (502, 412), (503, 309), ...]
cat file | grep ERROR | awk '{print $NF}' | sort | uniq -c. Legible, modular, memoria constante.yield from: delegar a otro generador
def sous_compter(a, b): for i in range(a, b): yield i def compter_tout(): yield from sous_compter(0, 3) # 0,1,2 yield from sous_compter(10, 13) # 10,11,12 yield 99 print(list(compter_tout())) # [0, 1, 2, 10, 11, 12, 99]
yield from evita el bucle for x in autre: yield x y también gestiona correctamente las excepciones y los valores enviados.
send(): generadores bidireccionales
Se pueden enviar valores a un generador (poco usado pero potente).
def echo(): while True: recu = yield print("Recibido:", recu) g = echo() next(g) # iniciar el generador g.send("hello") g.send("world") # Muestra: Recibido: hello / Recibido: world
Este mecanismo es el origen de asyncio antes de Python 3.5. Hoy se prefiere async/await.
Trampa n.º 1: un generador solo se recorre una vez
g = (i*i for i in range(5)) print(list(g)) # [0, 1, 4, 9, 16] print(list(g)) # [] -- ATENCIÓN, g está agotado
Solución: recrear el generador, o materializarlo en lista si lo necesitás varias veces:
data = [i*i for i in range(5)] # lista, reutilizable
Perfilar y encontrar los bottlenecks
Objetivos pedagógicos
1. cProfile global
python -m cProfile -o pipeline.prof pipeline_v0.py
Para explorar interactivamente con pstats:
python -m pstats pipeline.prof % sort cumulative % stats 15
42_847_310 function calls in 1083.42 seconds
ncalls tottime cumtime filename:lineno(function)
1 0.000 1083.42 pipeline_v0.py:1(<module>)
1 0.005 1083.41 pipeline_v0.py:42(main)
1 654.21 750.18 pipeline_v0.py:11(traiter_transactions)
5000001 34.20 34.20 <built-in method strip>
5000001 28.45 28.45 <built-in method upper>
3750000 21.89 45.30 pipeline_v0.py:18(traiter_transactions/dict.get)
5000001 18.95 18.95 <built-in method float>
3750000 180.45 180.45 list.append (resultats)
1 268.32 268.32 pipeline_v0.py:32(agreger)
1 65.10 65.10 pipeline_v0.py:39(sauver)traiter_transactions = 70 % del tiempo. Dentro: strip/upper (60 s), append (180 s), float() (19 s). agreger = 25 %. sauver = 6 %. Por lo tanto, la prioridad n.º 1 = traiter_transactions.2. Visualizar con snakeviz
snakeviz pipeline.prof
Se abre un navegador. Vista « sunburst »: un círculo central que representa el programa entero, dividido en sectores proporcionales al tiempo. Se puede hacer clic para hacer zoom.
En nuestro perfil vemos inmediatamente:
3. Zoom con line_profiler
Decoramos la función crítica:
@profile
def traiter_transactions(produits):
...kernprof -l -v pipeline_v0.py
Este artículo cubre los extractos más útiles — el curso completo Python Avanzado Rendimiento (11 capítulos, 35 lecciones, ejercicios corregidos y proyecto final) te lleva hasta el final.
./acceder-al-curso-completo curso gratuito: Dominar Claude CodeFAQ
¿Cuánto tiempo se necesita para aprender Python Avanzado Rendimiento?
¿Se necesitan requisitos previos?
¿Por dónde empezar concretamente?
📬 ¿Querés recibir este tipo de guía cada semana? Suscríbete gratis — código real, cero palabrería.