I. Brève Introduction▲
Cython est une bibliothèque, ce n’est pas la même que CPython, l’implémentation de référence du langage de programmation Python. Elle convertit du code Python en C qui peut être joint à l’exécution du runtime CPython.
Pour installer la bibliothèque, utilisez la commande suivante :
pip install cython
L’idée derrière Cython est de gagner en rapidité avec Python en le convertissant en code C/C++ et d’intégrer du code C/C++ directement au code Python existant. L’optimisation du code Python devrait se produire, car Python est un langage interprété et cela entraîne une surcharge d’interprétation et les appels de fonctions coûtent plus de temps. Une autre raison peut être le GIL (Global Interpreter Lock) qui réduit l’exécution des threads à 1 en CPython, ce qui signifie que les tâches demandant des ressources CPU ne bénéficient pas du multitâche (pour plus d’informations sur ce sujet : http://www.discoversdk.com/blog/parallel-processing-in-python).
L’inconvénient avec l’utilisation de Cython est que vous devez savoir coder en C bien que le langage lui-même est un surensemble de Python.
Petite note de ce que j’apprécie vraiment à propos de Cython, c’est qu’à la compilation de code, la génération d’erreur fournit un fichier .c avec un contenu similaire à : #erreur. N’utilisez pas ce fichier, il est le résultat d’erreur de compilation Cython.
II. Un simple exemple▲
Après cette introduction, il est temps de vous montrer un simple exemple. Je dois vous dire avant de commencer que je ne fournis qu’un exemple basique pour vous donner un aperçu de comment le code Cython est écrit et comment il fonctionne.
Voici le code que nous allons convertir avec Cython dans le prochain bout de code :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
__author__
=
'GHajba'
def
factors
(
n, result):
if
n <=
1
:
return
result
for
i in
range(
2
, n +
1
):
if
n %
i ==
0
:
result.append
(
i)
return
factors
(
n //
i, result)
def
main
(
):
numbers =
range(
1
, 50001
)
for
n in
numbers:
factors
(
n, [1
])
if
__name__
==
'__main__'
:
main
(
)
Comme vous pouvez le constater ci-dessus, ce code est trivial et peut être optimisé : la fonction factors calcule le facteur d’un nombre donné, la totalité du code calcule le facteur des nombres de 1 à 50 000 inclus.
J’ai sauvegardé ce code dans le fichier factorizer.pyx. Il s’agit d’un code Python 3 valide, vous pouvez donc l’essayer et mesurer son temps exécution avec Python 3 :
2.
3.
4.
5.
GHajba$ time python3.5
factorizer.pyx
real 0
m26.515
s
user 0
m26.369
s
sys 0
m0.073
s
Comme vous pouvez le constater, cela prend un peu plus de 26 secondes pour exécuter ce bout de code.
Il est maintenant temps de le compiler avec Cython :
cythonize -
b -
i factorizer.pyx
L’option -b construit les modules d’extension que vous pouvez charger dans l’interpréteur Python lors de son exécution.
L’option -i construit le module sur place.
Cela implique que l’option -b soit présente. Si vous omettez l’option -i, vous vous retrouverez avec une structure de répertoire comme à l’utilisation de distutils.
Après ceci, nous allons importer le code généré à Python et voir comment cela fonctionne :
2.
3.
4.
5.
GHajba$ time python3.5
-
c "import factorizer;factorizer.main()"
real 0
m17.192
s
user 0
m17.154
s
sys 0
m0.019
s
Comme vous pouvez le constater, le code s’exécute plus rapidement après sa compilation avec Cython. Nous avons éliminé la surcharge d’interprétation mentionnée précédemment. L’inconvénient est la taille du code :
GHajba$ wc -
l factorizer.pyx
18
factorizer.pyx
GHajba$ wc -
l factorizer.c
3384
factorizer.c
Le code généré en C a « quelques » lignes de plus. Si vous y regardez d’un peu plus près, c’est un peu déconcertant sauf si vous êtes un développeur C confirmé.
Il contient beaucoup de #défine pour la portabilité, des commentaires utiles avec des extraits de code Python pour comprendre quelle partie de votre code a généré du code C.
En somme, il s’agit de beaucoup de code que n’avez pas envie d’écrire vous-même.
III. Fonctionnalités de Cython▲
L’exemple ci-dessus était vraiment basique : nous n’y avons utilisé aucune fonctionnalité de Cython.
Réécrivons l’application pour voir si nous pouvons gagner un peu plus en vitesse. Pour cela, nous allons modifier un peu notre code en utilisant les définitions de C pour laisser le compilateur tirer meilleur parti de notre code.
La première partie de code vous montre le code en pur Python et nous le réécrivons en un code valide pour Cython :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
__author__
=
'GHajba'
def
factors
(
n, counter, result):
if
n <=
1
:
return
result
for
i in
range(
2
, n +
1
):
if
n %
i ==
0
:
result[counter] =
i
return
factors
(
n //
i, counter +
1
, result)
def
main
(
):
max_value =
50000
for
n in
range(
1
, max_value+
1
):
f =
[0
] *
max_value
factors
(
n, 0
, f)
if
__name__
==
'__main__'
:
main
(
)
Ce bloc de code utilise les tableaux comme vous pouvez vous y attendre en C ou Java : en utilisant leur index. Naturellement, ce n’est pas économiseur de mémoire en Python parce que chaque fois dans la boucle for nous réallouons tout le tableau f. Si vous exécutez ce code avec Python 3.5, il prendra +/- 37 secondes pour s’exécuter. Nous perdons un peu en performance.
Après avoir converti et exécuté ce code en Cython, l’exécution tourne autour de 27 secondes, ce qui signifie une modification du temps d’exécution en code C, plus rapide que le Python pur.
Mais comme mentionné précédemment, nous allons convertir ce code pour être encore plus efficace.
Voici la première approche :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
import
array
__author__
=
'GHajba'
def
factors
(
n, counter, result):
if
n <=
1
:
return
result
for
i in
range(
2
, n +
1
):
if
n %
i ==
0
:
result[counter] =
i
return
factors
(
n //
i, counter +
1
, result)
def
main
(
):
cdef int max_value =
50000
cdef object a
cdef int[:] f
cdef int n
for
n in
range(
1
, max_value+
1
):
a =
array.array
(
'i'
, [0
])*
max_value
f =
a
factors
(
n, 0
, a)
if
__name__
==
'__main__'
:
main
(
)
Le code ci-dessus ressemble presque à l’identique à sa version en Python, mais nous avons défini de manière concrète le type de la variable max_value, créé l’espace mémoire du tableau qui nous permet de générer du code qui accédera directement aux données de celui-ci.
Comme vous pouvez le constater, j’ai laissé la fonction factors inchangée.
« Cythonisons » ce code et exécutons-le :
GHajba$ time python3.5
-
c "import factorizer_cython;factorizer_cython.main()"
real 0
m17.504
s
user 0
m17.439
s
sys 0
m0.036
s
Bien, nous avons atteint le temps que nous avions précédemment avec du code plus orienté Python. Je peux comprendre que vous soyez quelque peu déçu parce que vous vous attendiez à un réel gain de temps. Mais ne perdons pas de vue qu’il nous reste une fonction à changer.
Ajoutons quelques définitions de type et regardons le résultat :
À la première étape, j’ai défini la variable i comme un entier (int). Cela n’apporte pas un gros changement, j’ai donc ajouté quelques définitions de type aux arguments de la fonction également.
Comme vous pouvez le deviner, cela ne fait pas encore l’affaire.
Le bout de code ci-dessus a une définition de type pour les variables i et n uniquement. Si je le convertis et l’exécute, j’obtiens les résultats suivants :
GHajba$ time python3.5
-
c "import factorizer_cython;factorizer_cython.main()"
real 0
m1.268
s
user 0
m1.241
s
sys 0
m0.019
s
Et voilà le gain de performance que nous attendions ! Factoriser 50 000 nombres en moins de 2 secondes.
IV. Conclusion▲
Comme nous avons pu le voir, Cython est principalement utilisé pour optimiser du code Python. Toutefois, ne vous lancez pas à corps perdu à apprendre le C et à réécrire tous vos programmes Python ! Premièrement, si votre application est vraiment lente, commencez par créer une version alternative en Cython et si le résultat est suffisamment rapide, modifiez votre code d’origine. Vous pouvez également identifier les petites parties lentes de votre code et les optimiser/réécrire.
Et pour terminer, quelques fois il est suffisant de « cythoniser » le code et d’utiliser le module d’extension compilé pour obtenir un gain de performance. Il est parfois nécessaire également de réécrire le code en Cython qui permet l’optimisation pour le compilateur C comme l’ajout de typage de variables.