21. Performance Python
cProfile,py-spy,timeit, optimisation,__slots__,numba,cython, memory profiling, vectorisation.
21.1 Principe : mesurer avant d’optimiser
“Premature optimization is the root of all evil.” — Knuth
- Profiler → trouver les goulots
- Optimiser → le strict nécessaire
- Mesurer → vérifier le gain
21.2 timeit — mesurer des fragments
import timeit
# Depuis Python
temps = timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
# Depuis IPython/REPL
# %timeit [x**2 for x in range(1000)]# Mesure précise d'une fonction
def à_tester():
return sum(range(1_000_000))
t = timeit.timeit(à_tester, number=100)
print(f"Moyenne: {t/100*1000:.2f}ms")21.3 cProfile — profilage global
import cProfile, pstats
def lent():
total = 0
for i in range(1_000_000):
total += i ** 2
return total
cProfile.run("lent()", "profil_stats")
# Analyse
p = pstats.Stats("profil_stats")
p.sort_stats("cumtime").print_stats(10) # top 10 par temps cumulé
p.sort_stats("time").print_stats(10) # top 10 par temps propreDepuis la ligne de commande :
python3 -m cProfile -o profil_stats mon_script.py
python3 -m pstats profil_stats
# > sort cumtime
# > stats 20SnakeViz — visualisation
pip install snakeviz
snakeviz profil_stats
# Ouvre un graphique interactif dans le navigateur21.4 py-spy — profilage sans modification
pip install py-spy
# Profiler un script en cours d'exécution
py-spy record -o profil.svg -- python3 mon_script.py
# S'attacher à un PID
py-spy top --pid 12345Utile pour : scripts en production, sans modification du code.
21.5 Memory profiling
pip install memory-profilerfrom memory_profiler import profile
@profile
def fonction_lourde():
a = [i ** 2 for i in range(100_000)]
b = [i ** 3 for i in range(100_000)]
return sum(a) + sum(b)
fonction_lourde()python3 -m memory_profiler mon_script.pyTracemalloc (stdlib)
import tracemalloc
tracemalloc.start()
# ... code à surveiller ...
snapshot = tracemalloc.take_snapshot()
stats = snapshot.statistics("lineno")
for stat in stats[:10]:
print(stat.size, stat.count, stat.traceback)21.6 __slots__ — réduire la mémoire
Vue en leçons 10 et 10b. Impact mesuré :
# Sans __slots__
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# Avec __slots__
class PointSlots:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
import sys
p1 = Point(1, 2)
p2 = PointSlots(1, 2)
print(sys.getsizeof(p1)) # ~56 (sans compter __dict__)
print(sys.getsizeof(p2)) # ~40 (pas de __dict__)Économie : 30-50% de mémoire par instance, plus rapide à l’accès.
21.7 numba — compilation JIT
from numba import jit
import numpy as np
@jit(nopython=True) # compilation en machine code
def somme_carrés(arr):
total = 0
for i in range(len(arr)):
total += arr[i] ** 2
return total
arr = np.random.rand(1_000_000)
résultat = somme_carrés(arr) # premier appel: compilation; second: natifGain : 10-100× pour des boucles Python pures.
Limites nopython=True : pas de listes d’objets, pas de try, pas de dict arbitraire. Support limité de NumPy.
@njit = @jit(nopython=True)
from numba import njit
@njit
def mandelbrot(c, max_iter):
z = 0j
for n in range(max_iter):
if abs(z) > 2:
return n
z = z * z + c
return max_iter@vectorize — UFuncs NumPy
from numba import vectorize
@vectorize
def sigmoid(x):
return 1 / (1 + np.exp(-x))
x = np.random.randn(1_000_000)
y = sigmoid(x) # parallélisé automatiquement21.8 Vectorisation avec NumPy
Toujours préférer les opérations vectorisées aux boucles :
import numpy as np
# LENT (boucle Python)
def lente(a, b):
résultat = np.empty_like(a)
for i in range(len(a)):
résultat[i] = a[i] * b[i] + a[i] ** 2
return résultat
# RAPIDE (vectorisé)
def rapide(a, b):
return a * b + a ** 2
a = np.random.rand(100_000)
b = np.random.rand(100_000)
%timeit lente(a, b) # ~50ms
%timeit rapide(a, b) # ~0.3ms (150× plus rapide)21.9 Structures de données adaptées
| Opération | Structure | Complexité |
|---|---|---|
| Test d’appartenance | set / dict | O(1) |
| Test d’appartenance | list | O(n) |
| Ajout/retrait aux extrémités | collections.deque | O(1) |
| File prioritaire | heapq | O(log n) |
| Recherche triée | bisect | O(log n) |
| Regroupement | itertools.groupby | O(n) |
| Cache | functools.lru_cache | O(1) |
# Mauvais : list pour recherche
noms = ["alice", "bob", "charlie"]
if "bob" in noms: # O(n)
# Bon : set
noms = {"alice", "bob", "charlie"}
if "bob" in noms: # O(1)21.10 itertools pour l’efficacité mémoire
from itertools import chain, islice, product
# LENT : crée une liste en mémoire
tous = [x * y for x in range(1000) for y in range(1000)]
# RAPIDE : générateur paresseux
tous = (x * y for x in range(1000) for y in range(1000))21.11 Conventions Python rapides vs lentes
# LENT
résultat = []
for x in données:
if condition(x):
résultat.append(transform(x))
# RAPIDE (comprehension)
résultat = [transform(x) for x in données if condition(x)]# LENT
d = {}
for k, v in paires:
if k not in d:
d[k] = []
d[k].append(v)
# RAPIDE (setdefault)
d = {}
for k, v in paires:
d.setdefault(k, []).append(v)
# RAPIDE (defaultdict)
from collections import defaultdict
d = defaultdict(list)
for k, v in paires:
d[k].append(v)21.12 local vs global
L’accès aux variables locales est plus rapide :
# LENT
total = 0
for i in range(1_000_000):
total += i # variable globale
# RAPIDE
def somme():
total = 0
for i in range(1_000_000):
total += i # variable locale
return total21.13 sys.setrecursionlimit
import sys
sys.setrecursionlimit(10_000) # défaut: 100021.14 gc — garbage collector
import gc
gc.collect() # forcer la collecte
gc.disable() # désactiver (rarement utile)
gc.set_threshold(700, 10, 10) # seuils de déclenchement21.15 Outils complémentaires
pip install pyinstrument # profilage minimaliste
pip install scalene # profilage CPU + mémoire + GPU
pip install line_profiler # profilage ligne par ligne (via @profile)
pip install cython # compilation vers CLine profiler :
from line_profiler import LineProfiler
lp = LineProfiler()
lp.add_function(fonction_lourde)
lp.enable()
fonction_lourde()
lp.disable()
lp.print_stats()21.16 Checklist optimisation
- As-tu profilé ? (Sinon → profiler d’abord)
- As-tu utilisé les bonnes structures ? (set/dict, deque, heap)
- Peux-tu vectoriser ? (NumPy, boucles → opérations array)
- Comprehensions > boucles ? (souvent 2× plus rapide)
__slots__pour les classes avec beaucoup d’instances ?@lru_cachepour des appels redondants ?- Local vars > global vars ?
- Algo adapté ? (O(n log n) vs O(n²))
- Parallélisation ? (multiprocessing pour CPU, threading pour I/O)
- Compilation ? (numba pour les boucles, cython pour le code métier)