LighTender / October 16, 2023

Améliorer les performances de Python : Voici mes conseils

python
optimisation
Test alt

Sommaire

Introduction

Tous les calculs ont été réalisés sur Python3.10

Cet article ne traite pas de l'optimisation des algorithmes, mais plutôt de l'optimisation spécifique à Python. Nous allons explorer toutes les optimisations que j'ai découvertes au cours de mon expérience avec ce langage.

1. Version de Python

En fonction des versions, Python est plus ou moins rapide. Il est important d'utiliser la dernière version de Python si vous voulez profiter de toutes les optimisations faites par les développeurs.

Par exemple, entre Python 3.10 et 3.11, les développeurs ont mesuré une accélération moyenne de x1.25, ce qui n'est pas négligeable dans notre quête d'optimisation.

2. Où se situent vos instructions ?

Lorsqu'on débute en programmation, on a souvent tendance à tout mettre dans un fichier sans y réfléchir.

programme python sans fonctions

Cependant, saviez-vous que regrouper l'ensemble de votre code au sein d'une fonction peut accélérer l'exécution de votre programme ?

programme python avec des fonctions

En enveloppant simplement nos instructions à l'intérieur d'une fonction, on peut gagner jusqu'à environ 30% en termes de rapidité !

Ici, on passe de 7.9 secondes à 5.8 secondes !

3. Optimisez vos listes

Les listes sont l'un des éléments les plus couramment utilisés en développement. Cependant, en Python, il existe de nombreuses optimisations pas forcément connues.


Objectif : Créer une liste d'éléments de 0 à 100 000 000.


Pour mes tests, j'ai exécuté chaque fonction 20 fois de suite afin d'obtenir des valeurs plutôt précises, en utilisant le module timeit.

# Exemple de code avec timeit
import timeit

def my_function():
    pass # Your code ...
print(timeit.timeit(my_function, number=20))

3.1. Concaténation

def concat_function():
    liste = []
    for i in range(100_000_00):
        liste + [i]

Temps : 13 seconde (environ)

Dans cet exemple, Python doit allouer de la mémoire pour la liste [i], puis il doit à nouveau allouer de la mémoire pour recréer la liste finale à chaque itération, ce qui ralentit considérablement l'exécution.

3.2. Append

def append_function():
    liste = []
    for i in range(100_000_00):
        liste.append(i)

Temps : 11.5 seconde (environ)

Python a mieux optimisé la fonction append, la rendant légèrement plus rapide. Cependant, lorsque la liste devient trop grande, Python alloue davantage de mémoire pour agrandir la liste et réécrire toutes les valeurs dans un tableau plus grand.

3.3. Les listes en compréhension

Les listes en compréhension sont une manière concise de créer des listes en Python. Bien que cela puisse sembler être une préférence des puristes souhaitant exhiber leur connaissance de Python, cette méthode présente en réalité un réel intérêt en termes d'optimisation.

def comprehention_function():
    liste = [i for i in range(100_000_00)]

Temps : 7.5 secondes (environ)

L'utilisation de listes en compréhension permet de gagner un temps considérable. C'est la méthode que je vous recommande d'utiliser. Avec un peu d'entraînement, elle devient facile à maîtriser.

3.4. Utilisation de la bibliothèque NumPy

NumPy est une bibliothèque spécialisée en Python dans la construction de tableaux. En effet, j'ai bien dit "tableaux" et non "listes", mais quelle est la différence ?

Les tableaux sont des structures de données à plage de données fixe. Il est impossible de changer leur taille. Cependant, il faut spécifier leur taille initiale.

Les listes, quant à elles, sont des structures de données variables. Elles permettent de changer leur taille dynamiquement, sans nécessité de spécifier la taille du tableau à l'avance.

Comme NumPy est une bibliothèque qui utilise exclusivement des tableaux et qu'elle est partiellement écrite en C, ce qui est bien plus rapide que Python, les performances obtenues peuvent être impressionnantes.

import numpy as np
def numpy_function():
    liste = np.arange(100_000_00)

Temps : 0.25 seconde (environ)

Oui, vous avez bien lu : dans certains cas, l'utilisation de tableaux de NumPy peut drastiquement accélérer l'exécution de votre programme.

4. Entrées/Sorties (input/output)

Un élément qui peut ralentir la vitesse de votre programme concerne les opérations de lecture/écriture, plus couramment utilisées sous print() et input().

4.1. La fonction print()

Il existe une fonction un peu moins connue qui améliore la rapidité de l'écriture dans le terminal. Elle permet de gagner environ 50% de temps.

import sys
sys.stdout.write("<votre text>")

Cette fonction ne prend qu'un unique paramètre, ce qui peut être plus contraignant, mais elle est plus rapide.

Cette fonction n'ajoute pas automatiquement le caractère \n à la fin du texte.

4.2. La fonction input()

La fonction input() est relativement lente car elle gère des fonctionnalités d'affichage avancées pour le terminal. Ainsi, l'appeler de nombreuses fois peut considérablement augmenter le temps d'exécution.

Cependant, il existe une alternative bien plus rapide à cette fonction, qui est jusqu'à 8 fois plus rapide que la précédente.

import sys
ligne = sys.stdin.readline()

Il est important de noter que cette fonction laisse le caractère \n (retour à la ligne) à la fin de l'entrée.

4.3. Remplacement des Fonctions de Base

Vous pouvez remplacer les fonctions de base par celles qui sont optimisées, comme ceci :

import sys
input = sys.stdin.readline
print = sys.stdout.write

5. Cython : Optimiser Python avec du C ?

La bibliothèque Cython est un outil très puissant qui permet de convertir du code Python en code C, ce qui en fait une ressource précieuse pour optimiser votre code. Cependant, elle peut être un peu déroutante à utiliser pour les débutants. Dans cette section, je vais vous présenter un mini tutoriel pour vous aider à démarrer avec Cython.

Avant d'entrer dans les détails, jetons un coup d'œil à ce que nous pouvons obtenir en appliquant l'accélération avec Cython à un objectif majeur de Python :

5.1. Petit Test de Cython (sans optimisation)

Comparons deux programmes, un en python classique, et l'autre utilisant Cython

FonctionPythonCython
concaténation13 secondes10.18 secondes
append11.5 secondes4.79 secondes
compréhension7.5 secondes4.7 secondes
NumPy0.25 seconde0.25 seconde

Nous pouvons observer une nette amélioration des performances pour les trois premières fonctions grâce à Cython. Cependant, pour NumPy, la différence n'est pas significative, car cette bibliothèque est déjà optimisée en C.

De plus, il est important de noter que nous n'avons pas encore effectué d'optimisations avancées avec Cython. Il est possible de typer chaque variable pour optimiser l'exécution des fonctions et les rendre encore plus rapides !

5.2. Utilisation de Cython

Commençons par installer les dépendances nécessaires.

pip install cython setuptools

Ensuite, créons notre fichier nom_fichier.py.

Une fois votre fichier créé, renommez-le en .pyx.

L'extension .pyx est associée à un fichier Python qui sera optimisé avec Cython. Cette étape est obligatoire pour pouvoir compiler le fichier Python.

Ensuite, créons notre "constructeur" de fichier C.

Créez et nommez votre nouveau fichier setup.py.

from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("nom_fichier.pyx")
)

Ensuite, dans votre terminal, exécutez la commande suivante :

python3 setup.py build_ext --inplace

Après cette étape, vous obtenez un fichier en .c et un fichier en .so.

Le fichier .so fait la liaison entre votre fichier .pyx et .c.

Ensuite, votre fichier nom_fichier.pyx agit comme une librairie écrite en C.

Supposons que dans mon fichier nom_fichier.pyx, j'ai ce code :


def append_function():
    liste = []
    for i in range(100_000_00):
        liste.append(i)
    return liste

def main():
    time_exec_append = timeit.timeit(append_function, number=20)

    print(f"append_function: {round(time_exec_append,2)} secondes")

Alors, dans un fichier test.py, il vous suffit d'écrire :

import nom_fichier

nom_fichier.main()

En exécutant la fonction principale (main) et en enregistrant le temps d'exécution du fichier, vous verrez déjà une nette amélioration des performances ! (cf. Conclusion)

Bien entendu, vous pouvez explorer toutes les façons d'optimiser Cython pour rendre votre code encore plus rapide !

Conclusion

FonctionPython3.10 (Hors fonction)Python3.11 (Hors fonction)
concaténation17.31 secondes20.12 secondes
append16.18 secondes14.42 secondes
compréhension7.7 secondes6.2 secondes
NumPy0.31 seconde0.33 seconde
FonctionPython3.10 (Dans une fonction)Cython (Python3.10)
concaténation13.08 secondes10.18 secondes
append11.5 secondes4.79 secondes
compréhension7.5 secondes4.7 secondes
NumPy0.25 seconde0.25 seconde

Voici un tableau récapitulatif de la plupart des améliorations

Il est important de noter que les temps indiqués dans le tableau sont indicatifs. Ils dépendent de la puissance de votre ordinateur, de la charge de travail de votre machine et d'autres facteurs. Cependant, en suivant ces conseils, vous pourrez observer des améliorations significatives des performances de vos programmes Python.

Notez que les temps sont à prendre avec des pincettes car elle dépende de votre ordinateur et des applications en cours sur votre machine