Python Avancé Performance expliqué simplement (avec schémas et vrai code)

Python Avancé Performance : l'essentiel en un article — vrai code, schémas et étapes concrètes, extraits d'un cours de 35 leçons.

Python Avancé Performance expliqué simplement (avec schémas et vrai code)

Un guide qui va droit au but : Python Avancé Performance décortiqué avec des schémas, des exemples concrets et des commandes testées. Tout vient d'un cours structuré de 11 chapitres — en voici le meilleur.

tl;dr
  • Introduction et Installation
  • Profilage du code
  • Comprehensions iterators generators
  • Multithreading vs Multiprocessing
  • asyncio et coroutines
~$ cat ./parcours.md # Python Avancé Performance — 10 chapitres
01
Introduction et Installation
→ Présentation du cours→ Installer les outils de profilage+ 1 autres leçons
02
Profilage du code
→ cProfile et pstats→ timeit pour des mesures précises+ 1 autres leçons
03
Compréhensions iterators generators
→ List, set et dict comprehensions→ Le protocole itérateur+ 1 autres leçons
04
Multithreading vs Multiprocessing
→ Threading, le GIL et ses limites→ Multiprocessing, le vrai parallélisme+ 1 autres leçons
05
asyncio et coroutines
→ Introduction à asyncio→ async / await en pratique+ 1 autres leçons
06
Cython Numba et vectorisation
→ Vectorisation avec NumPy→ Numba JIT+ 1 autres leçons
07
Gestion mémoire
→ Le garbage collector Python→ Références faibles avec weakref+ 1 autres leçons
08
Caching et mémoïsation
→ functools.lru_cache→ Cache disque avec joblib et diskcache+ 1 autres leçons
🏁
Projet final (+ 2 chapitres en chemin)
→ Tu repars avec un projet concret et démontrable

Présentation du cours

NOTEObjectif — Comprendre ce que veut dire « optimiser du code Python », quand le faire (et surtout quand ne pas le faire), et avoir une vision claire du parcours qui vous attend dans ce cours.

Objectifs pédagogiques

TIPÀ l’issue de ce module — Vous saurez expliquer pourquoi Python est réputé lent (et pourquoi c’est en partie un mythe), vous connaîtrez les 3 grands axes d’optimisation, et vous comprendrez la règle d’or : mesurer avant d’optimiser.

Le problème concret

Vous avez déjà vécu cette scène ? Vous lancez votre script Python à 17h pour calculer un rapport, vous partez vous chercher un café, et quand vous revenez 30 minutes plus tard, le script tourne toujours. Pire : votre collègue qui fait la même chose en R ou en Julia a fini en 3 minutes.

Symptômes typiques d’un programme Python lent

Ce que vous allez apprendre à faire

NOTEPython est-il vraiment lent ? — Python pur (CPython) peut être 50 à 100× plus lent que C pour des boucles numériques. Mais, la plupart des bibliothèques scientifiques (NumPy, Pandas, scikit-learn) sont écrites en C ou Fortran. Bien utilisé, Python atteint 80 à 95 % de la performance du C. Le problème n’est presque jamais « Python est lent » mais « mon code Python est mal écrit ».

Les 3 grands axes d’optimisation

Axe Question posée Gain typique
1. Algorithme Mon code est-il en O(n²) alors qu’il pourrait être en O(n log n) ? 10× à 10 000×
2. Structure de données Est-ce que j’utilise une list alors qu’un set ferait l’affaire ? 10× à 1000×
3. Concurrence / parallélisme Est-ce que je peux faire ces 8 tâches en même temps ? 2× à 16× (selon CPU/IO)
WARNINGOrdre d’attaque — Toujours dans cet ordre : algorithme > structure > parallélisme. Paralléliser un mauvais algorithme, c’est mettre 8 personnes pour creuser un tunnel à la petite cuillère alors qu’une pelleteuse suffisait.

La règle des 80/20 (Pareto)

Dans 80 % des programmes, 80 % du temps d’exécution est passé dans 20 % du code. Souvent c’est même 90/10 ou 95/5.

TIPConséquence — Inutile d’optimiser tout votre code. Trouvez les 5 lignes qui coûtent 95 % du temps, optimisez-les agressivement, et laissez le reste tranquille.

Donald Knuth, légende de l’informatique, l’a résumé ainsi en 1974 :

NOTE« Premature optimization is the root of all evil. »
(L’optimisation prématurée est la racine de tous les maux.)

Traduction pratique : écrivez d’abord du code clair et correct. Profitez-le. Si c’est trop lent, optimisez seulement les portions chaudes. Sinon, vous perdez du temps à rendre illisible du code qui n’impactait personne.

Un avant/après qui parle

Voici un exemple réel : sommer les carrés des nombres de 0 à 10 millions.

output
# Version naive : boucle Python
total = 0
for i in range(10_000_000):
    total += i * i
# Temps : ~1.2 seconde sur un laptop moderne

# Version vectorisée avec NumPy
import numpy as np
arr = np.arange(10_000_000)
total = (arr * arr).sum()
# Temps : ~0.04 seconde -> 30× plus rapide

# Version compilée avec 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
# Temps : ~0.01 seconde -> 120× plus rapide

Même problème, même langage, même résultat mathématique — mais 120 fois plus rapide. C’est exactement ce que vous allez apprendre à faire, systématiquement, sur votre code à vous.

Ce que vous allez construire

Phase 1 : Mesurer (ch. 0-1)

Installer les outils, faire votre premier profil, savoir lire un rapport cProfile. Vous saurez identifier le goulot d’étranglement en moins de 5 minutes.

Phase 2 : Optimiser (ch. 2-7)

Générateurs, threading, multiprocessing, asyncio, NumPy, Numba, Cython, cache. Tout l’arsenal moderne du développeur Python.

Générateurs avec yield

NOTEObjectif — Découvrir yield, le mot-clé qui transforme une fonction en générateur, et apprendre à construire des pipelines de traitement de données paresseux capables de traiter des fichiers de plusieurs gigaoctets avec quelques mégaoctets de RAM.

Objectifs pédagogiques

TIPÀ l’issue de ce module — Vous saurez écrire un générateur, l’utiliser dans un pipeline de traitement, et choisir intelligemment entre liste, générateur et collection matérialisée.

Un premier générateur

output
def compter(max):
    n = 0
    while n < max:
        yield n     # suspend la fonction et renvoie n
        n += 1

# Appel : ne fait RIEN, on récupère un générateur
g = compter(5)
print(type(g))   # <class 'generator'>

# Consommation
for n in g:
    print(n)     # 0 1 2 3 4
NOTEMagie — Dès qu’une fonction contient un yield, Python en fait une fabrique de générateurs. L’appel ne déclenche pas le code : il renvoie un objet générateur. Le code ne s’exécute qu’à chaque appel à next().

yield vs return

returnyield
Termine la fonctionSuspend la fonction
L’état est perduL’état est conservé
Renvoie une valeur (une fois)Peut être appelé plusieurs fois
Renvoie tout d’un coupRenvoie un élément à la fois

Cas d’usage réel : lire un gros fichier log

output
def lire_log(chemin):
    """Générateur qui yield chaque ligne, sans charger tout."""
    with open(chemin, encoding="utf-8") as f:
        for ligne in f:
            yield ligne.rstrip("\n")

def filtrer_erreurs(lignes):
    """Générateur qui ne garde que les lignes ERROR."""
    for ligne in lignes:
        if "ERROR" in ligne:
            yield ligne

def extraire_codes(lignes):
    """Générateur qui yield le code HTTP de chaque ligne."""
    for ligne in lignes:
        try:
            code = int(ligne.split()[-1])
            yield code
        except (ValueError, IndexError):
            continue

# Pipeline : aucune des étapes ne consomme de RAM, même pour 50 Go
lignes = lire_log("acces.log")
erreurs = filtrer_erreurs(lignes)
codes = extraire_codes(erreurs)

# Consommation finale
from collections import Counter
print(Counter(codes).most_common(5))
# [(500, 1284), (502, 412), (503, 309), ...]
TIPL’art du pipeline — Chaque générateur fait une seule chose et passe le résultat au suivant. C’est le modèle des outils Unix : cat file | grep ERROR | awk '{print $NF}' | sort | uniq -c. Lisible, modulaire, mémoire constante.

yield from : déléguer à un autre générateur

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 évite la boucle for x in autre: yield x et gère aussi correctement les exceptions et les valeurs envoyées.

send() : générateurs bidirectionnels

On peut envoyer des valeurs à un générateur (rarement utilisé mais puissant).

output
def echo():
    while True:
        recu = yield
        print("Reçu :", recu)

g = echo()
next(g)        # démarrer le générateur
g.send("hello")
g.send("world")
# Affiche : Reçu : hello / Reçu : world

Ce mécanisme est à l’origine de asyncio avant Python 3.5. Aujourd’hui on préfère async/await.

Piège n°1 : un générateur ne se parcourt qu’une fois

output
g = (i*i for i in range(5))
print(list(g))   # [0, 1, 4, 9, 16]
print(list(g))   # []  -- ATTENTION, g est épuisé

Solution : recréer le générateur, ou matérialiser en liste si vous en avez besoin plusieurs fois :

output
data = [i*i for i in range(5)]   # liste, réutilisable

Profiler et trouver les bottlenecks

NOTEObjectif — Appliquer la méthode du chapitre 1 sur notre pipeline ETL : utiliser cProfile pour voir où le temps est passé, snakeviz pour visualiser, et line_profiler pour zoomer sur la fonction critique.

Objectifs pédagogiques

TIPÀ l’issue de ce module — Vous saurez profiler un script de production complet, lire le rapport, identifier les 3-4 lignes qui consomment 90 % du temps, et écrire un « rapport de diagnostic » clair pour votre équipe.

1. cProfile global

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

Pour explorer interactivement avec 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)
NOTELecturetraiter_transactions = 70 % du temps. Dedans : strip/upper (60 s), append (180 s), float() (19 s). agreger = 25 %. sauver = 6 %. Donc la priorité #1 = traiter_transactions.

2. Visualiser avec snakeviz

output
snakeviz pipeline.prof

Un navigateur s’ouvre. Vue « sunburst » : un cercle central qui représente le programme entier, divisé en quartiers proportionnels au temps. On clique pour zoomer.

Sur notre profil, on voit immédiatement :

3. Zoomer avec line_profiler

Décorons la fonction critique :

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

Cet article couvre les extraits les plus utiles — le cours complet Python Avancé Performance (11 chapitres, 35 leçons, exercices corrigés et projet final) t'emmène jusqu'au bout.

./acceder-au-cours-complet cours gratuit : Maîtriser Claude Code

FAQ

Combien de temps pour apprendre Python Avancé Performance ?
Avec une progression structurée (11 chapitres, 35 leçons courtes et pratiques), on atteint un niveau opérationnel en quelques semaines à raison de 30 à 60 minutes par jour. L'important est de pratiquer chaque notion immédiatement.
Faut-il des prérequis ?
Mieux vaut être à l'aise avec les fondamentaux du domaine : ce contenu va en profondeur, avec des cas réels.
Par où commencer concrètement ?
Reproduis les commandes de cet article, puis suis le cours complet Python Avancé Performance : il enchaîne les 35 leçons dans l'ordre, avec exercices et projet final.

📬 Tu veux recevoir ce type de guide chaque semaine ? Abonne-toi gratuitement — code réel, zéro blabla.