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.

Python Avanzado Rendimiento explicado simplemente (con diagramas y código real)

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.

tl;dr
  • Introducción e Instalación
  • Perfilado del código
  • Comprensiones, iteradores y generadores
  • Multithreading vs Multiprocessing
  • asyncio y corrutinas
~$ cat ./parcours.md # Python Avancé Performance — 10 capítulos
01
Introducción e Instalación
→ Presentación del curso→ Instalar las herramientas de perfilado+ 1 más lecciones
02
Perfilado del código
→ cProfile et pstats→ timeit para mediciones precisas+ 1 más lecciones
03
Comprensiones, iteradores y generadores
→ List, set y dict comprehensions→ El protocolo iterador+ 1 más lecciones
04
Multithreading vs Multiprocessing
→ Threading, el GIL y sus límites→ Multiprocessing, el verdadero paralelismo+ 1 más lecciones
05
asyncio y corrutinas
→ Introducción a asyncio→ async / await en práctica+ 1 más lecciones
06
Cython Numba y vectorización
→ Vectorización con NumPy→ Numba JIT+ 1 más lecciones
07
Gestión de memoria
→ El garbage collector Python→ Referencias débiles con weakref+ 1 más lecciones
08
Caché y memoización
→ functools.lru_cache→ Caché en disco con joblib y diskcache+ 1 más lecciones
🏁
Proyecto final (+ 2 capítulos en el camino)
→ Te vas con un proyecto concreto y demostrable

Presentación del curso

NOTEObjetivo — Comprender qué significa « optimizar código Python », cuándo hacerlo (y sobre todo cuándo no hacerlo), y tener una visión clara del recorrido que te espera en este curso.

Objetivos pedagógicos

TIPAl finalizar este módulo — Podrás explicar por qué Python tiene fama de lento (y por qué es en parte un mito), conocerás los 3 grandes ejes de optimización y comprenderás la regla de oro: medir antes de optimizar.

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

NOTE¿Python es realmente lento? — Python puro (CPython) puede ser entre 50 y 100× más lento que C para bucles numéricos. Pero, la mayoría de las bibliotecas científicas (NumPy, Pandas, scikit-learn) están escritas en C o Fortran. Bien utilizado, Python alcanza entre el 80 y el 95 % del rendimiento de C. El problema casi nunca es « Python es lento » sino « mi código Python está mal escrito ».

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)
WARNINGOrden de ataque — Siempre en este orden: algoritmo > estructura > paralelismo. Paralelizar un mal algoritmo es poner a 8 personas a cavar un túnel con cucharita cuando bastaba una excavadora.

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.

TIPConsecuencia — No tiene sentido optimizar todo tu código. Encontrá las 5 líneas que cuestan el 95 % del tiempo, optimizalas agresivamente y dejá el resto tranquilo.

Donald Knuth, leyenda de la informática, lo resumió así en 1974:

NOTE« Premature optimization is the root of all evil. »
(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.

output
# 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

NOTEObjetivo — Descubrir 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

TIPAl finalizar este módulo — Sabrás escribir un generador, usarlo en un pipeline de procesamiento y elegir inteligentemente entre lista, generador y colección materializada.

Un primer generador

output
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
NOTEMagia — En cuanto una función contiene un 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

returnyield
Termina la funciónSuspende la función
El estado se pierdeEl estado se conserva
Devuelve un valor (una vez)Puede llamarse varias veces
Devuelve todo de una vezDevuelve un elemento a la vez

Caso de uso real: leer un archivo de log grande

output
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), ...]
TIPEl arte del pipeline — Cada generador hace una sola cosa y pasa el resultado al siguiente. Es el modelo de las herramientas Unix: cat file | grep ERROR | awk '{print $NF}' | sort | uniq -c. Legible, modular, memoria constante.

yield from: delegar a otro generador

output
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).

output
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

output
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:

output
data = [i*i for i in range(5)]   # lista, reutilizable

Perfilar y encontrar los bottlenecks

NOTEObjetivo — Aplicar el método del capítulo 1 a nuestro pipeline ETL: usar cProfile para ver dónde se gastó el tiempo, snakeviz para visualizar y line_profiler para hacer zoom en la función crítica.

Objetivos pedagógicos

TIPAl finalizar este módulo — Sabrás perfilar un script de producción completo, leer el informe, identificar las 3-4 líneas que consumen el 90 % del tiempo y escribir un « informe de diagnóstico » claro para tu equipo.

1. cProfile global

output
python -m cProfile -o pipeline.prof pipeline_v0.py

Para explorar interactivamente con pstats:

output
python -m pstats pipeline.prof
% sort cumulative
% stats 15
output
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)
NOTELecturatraiter_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

output
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:

output
@profile
def traiter_transactions(produits):
    ...
output
kernprof -l -v pipeline_v0.py
va-plus-loin

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 Code

FAQ

¿Cuánto tiempo se necesita para aprender Python Avanzado Rendimiento?
Con una progresión estructurada (11 capítulos, 35 lecciones cortas y prácticas), se alcanza un nivel operativo en unas semanas dedicando entre 30 y 60 minutos al día. Lo importante es practicar cada concepto de inmediato.
¿Se necesitan requisitos previos?
Es mejor estar cómodo con los fundamentos del área: este contenido profundiza, con casos reales.
¿Por dónde empezar concretamente?
Reproduce los comandos de este artículo y luego sigue el curso completo Python Avanzado Rendimiento: encadena las 35 lecciones en orden, con ejercicios y proyecto final.

📬 ¿Querés recibir este tipo de guía cada semana? Suscríbete gratis — código real, cero palabrería.