
Quand une fonction Python passe son temps dans une boucle numérique, la réécrire en Cython la rend souvent 50 à 100 fois plus rapide sans quitter votre projet Python. Dans le lab de ce guide, une intégration numérique de dix millions de pas passe de 399 ms en Python pur à 6,2 ms en Cython, soit une accélération mesurée de x64, avec un résultat identique. Cython compile un dialecte de Python en code C natif : vous ajoutez un typage statique (cdef, cpdef), vous compilez un module .pyx, et la boucle chaude tourne à la vitesse du C. Les exemples ont été exécutés avec Python 3.12 et Cython 3.2.8.
Ce guide s'adresse aux développeurs qui ont déjà identifié un hotspot CPU par profiling et qui veulent le rendre nettement plus rapide. Vous verrez comment écrire et compiler un module Cython, comment mesurer le gain, comment relâcher le GIL avec nogil, et quelles sont les autres passerelles vers le C (ctypes, cffi, pybind11). Vous verrez surtout quand ce travail en vaut la peine et quand il vaut mieux rester en Python pur.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Reconnaître le moment où le Python pur atteint sa limite CPU.
- Écrire un module
.pyxtypé aveccdef,cpdefet les annotations. - Compiler l'extension via
cythonizedans unsetup.pyoupyproject.toml. - Mesurer le gain réel d'une boucle numérique face au Python pur.
- Relâcher le GIL avec
nogilet choisir entrectypes,cffietpybind11.
Quand le Python pur atteint sa limite
Section intitulée « Quand le Python pur atteint sa limite »Python est interprété et typé dynamiquement : à chaque tour de boucle, l'interpréteur vérifie le type de chaque variable, gère le comptage de références et repasse par la machine virtuelle. Sur une boucle serrée qui fait des millions d'opérations arithmétiques, ce surcoût domine le temps de calcul. C'est là que le code natif devient intéressant : une même boucle compilée en C ne paie plus aucun de ces frais.
La règle d'or reste de mesurer avant d'agir. On ne réécrit pas un programme entier en Cython : on cible la poignée de fonctions qui concentrent le temps CPU, repérées par un profileur. Notre guide sur le profiling Python montre comment isoler ces hotspots avec cProfile et timeit. Sans cette étape, vous risquez d'optimiser du code qui ne représente que 2 % du temps d'exécution, pour un gain global invisible.
Cython vise un profil précis de problèmes : boucles numériques, traitement de tableaux, calculs scientifiques, parsing bas niveau. Pour un code dominé par des appels réseau, des accès disque ou une base de données, le goulot n'est pas le CPU et Cython n'apportera rien. Dans ces cas, regardez plutôt l'optimisation algorithmique et les entrées-sorties asynchrones.
Écrire un premier module Cython
Section intitulée « Écrire un premier module Cython »Un module Cython vit dans un fichier .pyx. Vous pouvez y écrire du Python quasiment normal, mais le gain vient du typage statique : en déclarant qu'une variable est un double ou un int C, vous permettez au compilateur de générer des opérations natives au lieu de manipuler des objets Python. Trois mots-clés structurent ce typage.
Le mot-clé cdef déclare une variable ou une fonction en types C purs. Une fonction cdef est la plus rapide, mais elle n'est appelable que depuis le code Cython, pas depuis Python. Le mot-clé cpdef crée une fonction hybride : une version C rapide pour les appels internes, et une enveloppe Python pour l'extérieur. C'est le bon choix pour la fonction d'entrée de votre module, celle que le reste du programme appellera.
Voici la boucle chaude de notre lab : une intégration numérique de la fonction mathématique qui à chaque abscisse vaut x * x - x, calculée par la méthode des rectangles. Le fichier integ_cy.pyx type toutes les variables de travail.
# cython: boundscheck=False, wraparound=False, cdivision=True
cdef inline double f(double x) nogil: return x * x - x
cpdef double integrer(double a, double b, int n): cdef double dx = (b - a) / n cdef double total = 0.0 cdef double x = a cdef int i for i in range(n): total += f(x) * dx x += dx return totalChaque déclaration compte. La fonction interne f est en cdef inline : elle sera insérée directement dans la boucle, sans coût d'appel. Les variables dx, total, x et i sont des types C (double, int), donc la boucle ne crée aucun objet Python. La directive cdivision=True évite la vérification de division par zéro à la mode Python, et boundscheck=False désactive le contrôle des indices. La fonction publique integrer est en cpdef pour rester appelable depuis Python.
Cython accepte aussi une syntaxe d'annotations proche des type hints Python, pratique quand vous voulez garder un fichier lisible par les outils Python. Les deux styles se compilent de la même façon.
import cython
@cython.cfunc@cython.inlinedef f(x: cython.double) -> cython.double: return x * x - xCompiler le module en extension native
Section intitulée « Compiler le module en extension native »Un fichier .pyx n'est pas directement importable : il faut le transformer en C puis le compiler en bibliothèque partagée. Cette étape s'appuie sur cythonize, la fonction fournie par Cython qui pilote la traduction. Il vous faut un compilateur C installé sur la machine (gcc ou clang sous Linux, les Build Tools sous Windows).
La méthode classique passe par un setup.py qui appelle cythonize sur votre fichier. Ce script décrit l'extension à construire.
from setuptools import setupfrom Cython.Build import cythonize
setup(ext_modules=cythonize("integ_cy.pyx", language_level="3"))On déclare aussi les dépendances de build dans un pyproject.toml, pour que l'installation dans un environnement propre récupère Cython et setuptools avant de compiler. C'est la forme moderne attendue par pip.
[build-system]requires = ["setuptools>=68", "Cython>=3.0"]build-backend = "setuptools.build_meta"
[project]name = "integ-cy"version = "0.1.0"La compilation en place se lance avec build_ext. Elle génère un fichier integ_cy.c puis une bibliothèque partagée .so (ou .pyd sous Windows) importable comme n'importe quel module.
python setup.py build_ext --inplaceLa sortie se termine par la création du fichier natif, que vous pouvez vérifier ainsi. Le module est alors importable dans Python.
$ ls *.sointeg_cy.cpython-312-x86_64-linux-gnu.soMesurer le gain sur la boucle chaude
Section intitulée « Mesurer le gain sur la boucle chaude »Un gain annoncé ne vaut rien sans mesure. On compare donc la version Python pure et la version Cython sur exactement le même calcul, avec timeit pour lisser les variations. La version Python pure est un fichier integ_py.py au code identique, mais sans aucun typage.
def f(x): return x * x - x
def integrer(a, b, n): dx = (b - a) / n total = 0.0 x = a for _ in range(n): total += f(x) * dx x += dx return totalLe banc d'essai appelle les deux fonctions sur dix millions de pas et affiche le temps moyen de chaque version ainsi que le rapport. On vérifie d'abord que les deux résultats sont identiques, sinon la comparaison n'aurait aucun sens.
import timeitimport integ_pyimport integ_cy
N = 10_000_000a, b = 0.0, 100.0
print(f"resultat python : {integ_py.integrer(a, b, N):.4f}")print(f"resultat cython : {integ_cy.integrer(a, b, N):.4f}")
t_py = timeit.timeit(lambda: integ_py.integrer(a, b, N), number=3) / 3t_cy = timeit.timeit(lambda: integ_cy.integrer(a, b, N), number=3) / 3print(f"python pur : {t_py*1000:.1f} ms")print(f"cython : {t_cy*1000:.1f} ms")print(f"acceleration : x{t_py/t_cy:.1f}")La sortie réelle du lab est sans appel. Le calcul donne le même résultat au centième près, mais la version compilée écrase la version interprétée.
resultat python : 328333.2839resultat cython : 328333.2839python pur : 399.0 mscython : 6.2 msacceleration : x64.2Ce facteur x64 est typique d'une boucle purement numérique correctement typée. Le message essentiel : le gain vient du typage statique, pas de la magie du compilateur. Un fichier .pyx sans aucun cdef reste presque aussi lent que le Python pur, car chaque variable resterait un objet Python. Ce sont les déclarations de type qui débloquent la vitesse du C.
Relâcher le GIL avec nogil
Section intitulée « Relâcher le GIL avec nogil »Le verrou global de l'interpréteur (GIL) empêche deux threads Python d'exécuter du bytecode en même temps, ce qui bride le parallélisme CPU en Python pur. Les extensions natives ont un privilège que le Python pur n'a pas : elles peuvent relâcher le GIL pendant un calcul qui ne touche pas aux objets Python. C'est ce que signale le mot-clé nogil sur notre fonction f.
Une fois le GIL relâché dans une section de code, plusieurs threads peuvent calculer en parallèle sur plusieurs cœurs, même depuis le module threading classique. Cython offre pour cela le bloc with nogil: et la boucle parallèle prange (basée sur OpenMP), qui distribue les itérations sur les cœurs disponibles.
from cython.parallel import prange
cpdef double somme_parallele(int n): cdef double total = 0.0 cdef int i for i in prange(n, nogil=True): total += i * 0.5 return totalAttention à un piège de compilation : prange ne distribue le travail que si l'extension est compilée avec OpenMP (extra_compile_args=["-fopenmp"] et extra_link_args=["-fopenmp"] sur l'Extension du setup.py avec gcc). Sans ce drapeau, le code compile mais la boucle reste séquentielle.
C'est un avantage décisif face au Python pur : là où threading ne parallélise jamais un calcul CPU à cause du GIL, une extension Cython bien écrite exploite réellement tous les cœurs. La contrepartie est la rigueur : dans une section nogil, vous ne pouvez manipuler aucun objet Python, uniquement des types C.
Les autres passerelles vers le C
Section intitulée « Les autres passerelles vers le C »Cython n'est pas la seule façon d'appeler du code natif depuis Python. Selon que vous voulez réutiliser une bibliothèque existante ou écrire du nouveau code, d'autres outils sont plus adaptés. Il est utile de connaître les trois principaux pour choisir le bon.
Le module ctypes fait partie de la bibliothèque standard : il charge une bibliothèque partagée déjà compilée (.so, .dll) et appelle ses fonctions, sans rien compiler côté Python. C'est idéal pour piloter une lib C dont vous avez le binaire. Dans le lab, une même intégration écrite en C pur et compilée avec gcc -shared -fPIC s'appelle directement.
import ctypes
lib = ctypes.CDLL("./libmathutils.so")lib.integrer.argtypes = [ctypes.c_double, ctypes.c_double, ctypes.c_int]lib.integrer.restype = ctypes.c_double
print(lib.integrer(0.0, 100.0, 10_000_000))Il faut déclarer explicitement les types des arguments (argtypes) et du retour (restype), sinon ctypes suppose des entiers et renvoie des valeurs fausses. Cet appel a bien renvoyé 328333.2839, le même résultat que Cython. La bibliothèque cffi joue le même rôle mais de façon plus robuste : vous lui donnez les déclarations C du header, et elle génère l'interface, ce qui évite les erreurs de type manuelles de ctypes.
Pour du C++, l'outil de référence est pybind11 : une bibliothèque d'en-têtes qui expose des classes et fonctions C++ à Python avec une syntaxe concise. C'est le choix habituel quand vous écrivez une extension en C++ moderne ou que vous enrobez une bibliothèque C++ existante. Le tableau ci-dessous résume les usages.
| Outil | Rôle principal | Quand le choisir |
|---|---|---|
| Cython | Compiler du Python typé en C | Accélérer vos propres boucles chaudes |
| ctypes | Charger une lib partagée | Appeler un binaire C sans compiler |
| cffi | Interface C par déclarations | Même besoin que ctypes, plus sûr |
| pybind11 | Exposer du C++ à Python | Enrober ou écrire du C++ |
Quand rester en Python pur
Section intitulée « Quand rester en Python pur »Passer une fonction en Cython n'est pas gratuit. Vous ajoutez une étape de compilation, une dépendance à un compilateur C, un code plus verbeux et plus difficile à déboguer, et des binaires à construire pour chaque plateforme cible. Ce coût ne se justifie que si le gain est réel et mesuré sur un hotspot qui pèse lourd dans votre profil.
Avant de sortir Cython, épuisez les options plus simples. Un meilleur algorithme peut battre n'importe quelle optimisation bas niveau : passer d'une recherche linéaire à un set, mémoïser avec lru_cache, éviter des recalculs. Notre guide optimiser son code Python détaille ces leviers à coût nul. Et pour du calcul sur tableaux, NumPy vectorise déjà les boucles en C : réécrire en NumPy est souvent plus rapide à faire et aussi performant que Cython.
Restez donc en Python pur tant que le CPU n'est pas votre goulot, tant qu'un profileur n'a pas désigné une fonction précise, et tant qu'une solution vectorisée ou algorithmique suffit. Cython est un outil de dernière étape, quand vous avez confirmé par la mesure qu'une boucle numérique incompressible domine votre temps d'exécution.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »- Cython compile un Python typé en code C natif : le gain vient du typage statique, pas du compilateur seul.
- Le typage repose sur
cdef(types C purs, interne),cpdef(hybride, appelable depuis Python) et les annotationscython.double. - La compilation passe par
cythonizedans unsetup.py, avec les dépendances déclarées enpyproject.toml, et exige un compilateur C. - Sur une boucle numérique de dix millions de pas, le lab mesure 399 ms en Python contre 6,2 ms en Cython, soit x64, résultat identique.
- Le mot-clé
nogilrelâche le GIL et permet un vrai parallélisme CPU (prange), impossible enthreadingPython pur. - Pour appeler du code natif existant, choisissez
ctypesoucffi(lib C) oupybind11(C++), selon le besoin. - Profilez d'abord : Cython ne vaut le coût que sur un hotspot CPU confirmé. Un meilleur algorithme ou NumPy suffit souvent.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Empaqueter et publier sur PyPI : distribuer votre extension compilée en wheels, la suite logique une fois le module au point.
- Automatiser les tests avec tox : vérifier que votre extension compile et passe les tests sur chaque version de Python supportée.