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.
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.
- Introduction et Installation
- Profilage du code
- Comprehensions iterators generators
- Multithreading vs Multiprocessing
- asyncio et coroutines
Présentation du cours
Objectifs pédagogiques
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
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) |
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.
Donald Knuth, légende de l’informatique, l’a résumé ainsi en 1974 :
(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.
# 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
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
Un premier générateur
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
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
| return | yield |
|---|---|
| Termine la fonction | Suspend la fonction |
| L’état est perdu | L’état est conservé |
| Renvoie une valeur (une fois) | Peut être appelé plusieurs fois |
| Renvoie tout d’un coup | Renvoie un élément à la fois |
Cas d’usage réel : lire un gros fichier log
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), ...]
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
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).
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
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 :
data = [i*i for i in range(5)] # liste, réutilisable
Profiler et trouver les bottlenecks
Objectifs pédagogiques
1. cProfile global
python -m cProfile -o pipeline.prof pipeline_v0.py
Pour explorer interactivement avec 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 % 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
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 :
@profile
def traiter_transactions(produits):
...kernprof -l -v pipeline_v0.py
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 CodeFAQ
Combien de temps pour apprendre Python Avancé Performance ?
Faut-il des prérequis ?
Par où commencer concrètement ?
📬 Tu veux recevoir ce type de guide chaque semaine ? Abonne-toi gratuitement — code réel, zéro blabla.