Mesurez avant d'optimiser : timeit et cProfile, ce que le GIL bloque vraiment, la frugalité des générateurs et les pièges mémoire des chaînes et des tranches.
Ouvrir cette leçon dans KodokonNe devinez jamais, mesurez. timeit isole un micro-benchmark fiable : il désactive le garbage collector et répète l'exécution pour lisser le bruit. cProfile dresse la carte des appels d'un programme entier - triez par cumtime (temps cumulé, sous-appels inclus) pour identifier les vrais coupables. La règle d'or de tout travail de performance : la ligne lente n'est presque jamais celle que vous soupçonnez.
python -m timeit "sum(x * x for x in range(1000))"
python -m timeit -s "s = 'abc' * 100" "s.upper()"
python -m cProfile -s cumtime -m timeit "min(range(50))"Le GIL (Global Interpreter Lock) garantit qu'un seul thread exécute du bytecode Python à la fois dans un processus. Conséquence : les threads n'accélèrent pas le calcul pur. Ils restent en revanche parfaits pour l'I/O, car le GIL est relâché pendant les lectures réseau ou disque, ainsi que par de nombreuses bibliothèques C comme NumPy. Pour saturer plusieurs cœurs en calcul, passez par multiprocessing ou concurrent.futures.ProcessPoolExecutor. Python 3.13 propose une variante expérimentale « free-threaded » sans GIL, encore rare en production.
import sys
squares_list = [x * x for x in range(1_000_000)]
squares_gen = (x * x for x in range(1_000_000))
print(sys.getsizeof(squares_list), "bytes")
print(sys.getsizeof(squares_gen), "bytes")
print(sum(squares_gen))Le générateur pèse quelques centaines d'octets quelle que soit la longueur de la séquence : il ne stocke que son état d'exécution et produit chaque valeur à la demande. Chaînez-les en pipelines - sum(x * x for x in data) - pour traiter des flux sans jamais matérialiser de liste intermédiaire. Contrepartie à connaître : un générateur est à usage unique. Une fois épuisé, il ne produit plus rien, silencieusement - une source classique de bugs où la deuxième itération semble « vide ».
import timeit
def concat(n: int) -> str:
out = ""
for i in range(n):
out += str(i)
return out
def join(n: int) -> str:
return "".join(str(i) for i in range(n))
t1 = timeit.timeit(lambda: concat(20_000), number=10)
t2 = timeit.timeit(lambda: join(20_000), number=10)
print(f"concat: {t1:.3f} s / join: {t2:.3f} s")sys.getsizeof d'un générateur reste-t-il minuscule face à la liste équivalente ?''.join(parts) bat-il out += part répété en boucle ?