Image par auteur
Écrire du code Python efficace est crucial pour optimiser les performances et l’utilisation des ressources, que ce soit pour des projets de science des données, des applications Web ou d’autres tâches de programmation.
Grâce aux fonctionnalités puissantes et aux meilleures pratiques de Python, vous pouvez réduire le temps de calcul et améliorer la réactivité et la maintenabilité de vos applications.
Dans ce didacticiel, nous explorerons cinq conseils essentiels pour vous aider à écrire du code Python plus efficace, avec des exemples de code pour chacun. Commençons.
1. Utilisez des compréhensions de liste au lieu de boucles
Les compréhensions de listes permettent de créer des listes à partir de listes existantes et d’autres itérables comme des chaînes et des tuples. Elles sont généralement plus concises et plus rapides que les boucles classiques pour les opérations de liste.
Supposons que nous ayons un ensemble de données d’informations sur les utilisateurs et que nous souhaitions extraire les noms des utilisateurs ayant un score supérieur à 85.
Utiliser une boucle
Tout d’abord, faisons cela en utilisant une boucle for et une instruction if :
data = [{'name': 'Alice', 'age': 25, 'score': 90},
{'name': 'Bob', 'age': 30, 'score': 85},
{'name': 'Charlie', 'age': 22, 'score': 95}]
# Using a loop
result = []
for row in data:
if row['score'] > 85:
result.append(row['name'])
print(result)
Vous devriez obtenir le résultat suivant :
Output >>> ['Alice', 'Charlie']
Utiliser une compréhension de liste
Maintenant, réécrivons en utilisant une compréhension de liste. Vous pouvez utiliser la syntaxe générique [output for input in iterable if condition]
ainsi :
data = [{'name': 'Alice', 'age': 25, 'score': 90},
{'name': 'Bob', 'age': 30, 'score': 85},
{'name': 'Charlie', 'age': 22, 'score': 95}]
# Using a list comprehension
result = [row['name'] for row in data if row['score'] > 85]
print(result)
Ce qui devrait vous donner le même résultat :
Output >>> ['Alice', 'Charlie']
Comme on l’a vu, la version de compréhension de liste est plus concise et plus facile à maintenir. Vous pouvez essayer d’autres exemples et profiler votre code avec timeit pour comparer les temps d’exécution des boucles par rapport aux compréhensions de listes.
Les compréhensions de listes vous permettent donc d’écrire du code Python plus lisible et plus efficace, en particulier dans la transformation de listes et les opérations de filtrage. Mais attention à ne pas en abuser. Lire Pourquoi vous ne devriez pas abuser des compréhensions de listes en Python pour savoir pourquoi en abuser peut devenir une trop bonne chose.
2. Utilisez des générateurs pour un traitement efficace des données
Les générateurs en Python permettent de parcourir de grands ensembles de données et des séquences sans les stocker tous en mémoire au préalable. Ceci est particulièrement utile dans les applications où l’efficacité de la mémoire est importante.
Contrairement aux fonctions Python classiques qui utilisent le mot-clé return
pour renvoyer la séquence entière, les fonctions génératrices génèrent un objet générateur. Vous pouvez ensuite effectuer une boucle pour obtenir les éléments individuels, à la demande et un à la fois.
Supposons que nous disposions d’un gros fichier CSV contenant des données utilisateur et que nous souhaitions traiter chaque ligne, une par une, sans charger l’intégralité du fichier en mémoire en une seule fois.
Voici la fonction génératrice pour cela :
import csv
from typing import Generator, Dict
def read_large_csv_with_generator(file_path: str) -> Generator[Dict[str, str], None, None]:
with open(file_path, 'r') as file:
reader = csv.DictReader(file)
for row in reader:
yield row
# Path to a sample CSV file
file_path="large_data.csv"
for row in read_large_csv_with_generator(file_path):
print(row)
Note: N’oubliez pas de remplacer ‘large_data.csv’ par le chemin d’accès à votre fichier dans l’extrait ci-dessus.
Comme vous pouvez déjà le constater, l’utilisation de générateurs est particulièrement utile lorsque vous travaillez avec des données en streaming ou lorsque la taille de l’ensemble de données dépasse la mémoire disponible.
Pour un examen plus détaillé des générateurs, lisez Premiers pas avec les générateurs Python.
3. Mettre en cache les appels de fonctions coûteux
La mise en cache peut améliorer considérablement les performances en stockant les résultats d’appels de fonctions coûteux et en les réutilisant lorsque la fonction est à nouveau appelée avec les mêmes entrées.
Supposons que vous codiez un algorithme de clustering k-means à partir de zéro et que vous souhaitiez mettre en cache les distances euclidiennes calculées. Voici comment mettre en cache les appels de fonction avec le décorateur @cache
:
from functools import cache
from typing import Tuple
import numpy as np
@cache
def euclidean_distance(pt1: Tuple[float, float], pt2: Tuple[float, float]) -> float:
return np.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
def assign_clusters(data: np.ndarray, centroids: np.ndarray) -> np.ndarray:
clusters = np.zeros(data.shape[0])
for i, point in enumerate(data):
distances = [euclidean_distance(tuple(point), tuple(centroid)) for centroid in centroids]
clusters[i] = np.argmin(distances)
return clusters
Prenons l’exemple d’appel de fonction suivant :
data = np.array([[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [8.0, 9.0], [9.0, 10.0]])
centroids = np.array([[2.0, 3.0], [8.0, 9.0]])
print(assign_clusters(data, centroids))
Quelles sorties :
Outputs >>> [0. 0. 0. 1. 1.]
Pour en savoir plus, lisez Comment accélérer le code Python avec la mise en cache.
4. Utilisez les gestionnaires de contexte pour la gestion des ressources
En Python, les gestionnaires de contexte assurent que les ressources, telles que les fichiers, les connexions aux bases de données et les sous-processus, sont correctement gérées après utilisation.
Supposons que vous deviez interroger une base de données et que vous souhaitiez vous assurer que la connexion est correctement fermée après utilisation :
import sqlite3
def query_db(db_path):
with sqlite3.connect(db_path) as conn:
cursor = conn.cursor()
cursor.execute(query)
for row in cursor.fetchall():
yield row
Vous pouvez maintenant essayer d’exécuter des requêtes sur la base de données :
query = "SELECT * FROM users"
for row in query_database('people.db', query):
print(row)
Pour en savoir plus sur les utilisations des gestionnaires de contexte, lisez 3 utilisations intéressantes des gestionnaires de contexte de Python.
5. Vectoriser les opérations à l’aide de NumPy
NumPy permet d’effectuer des opérations élément par élément sur des tableaux (comme des opérations sur des vecteurs) sans avoir besoin de boucles explicites. C’est souvent beaucoup plus rapide que les boucles car NumPy utilise C sous le capot.
Supposons que nous ayons deux grands tableaux représentant les scores de deux tests différents et que nous souhaitions calculer le score moyen de chaque élève. Faisons-le en utilisant une boucle :
import numpy as np
# Sample data
scores_test1 = np.random.randint(0, 100, size=1000000)
scores_test2 = np.random.randint(0, 100, size=1000000)
# Using a loop
average_scores_loop = []
for i in range(len(scores_test1)):
average_scores_loop.append((scores_test1[i] + scores_test2[i]) / 2)
print(average_scores_loop[:10])
Voici comment les réécrire avec les opérations vectorisées de NumPy :
# Using NumPy vectorized operations
average_scores_vectorized = (scores_test1 + scores_test2) / 2
print(average_scores_vectorized[:10])
Boucles et opérations vectorisées
Mesurons les temps d’exécution de la boucle et des versions de NumPy à l’aide de timeit :
setup = """
import numpy as np
scores_test1 = np.random.randint(0, 100, size=1000000)
scores_test2 = np.random.randint(0, 100, size=1000000)
"""
loop_code = """
average_scores_loop = []
for i in range(len(scores_test1)):
average_scores_loop.append((scores_test1[i] + scores_test2[i]) / 2)
"""
vectorized_code = """
average_scores_vectorized = (scores_test1 + scores_test2) / 2
"""
loop_time = timeit.timeit(stmt=loop_code, setup=setup, number=10)
vectorized_time = timeit.timeit(stmt=vectorized_code, setup=setup, number=10)
print(f"Loop time: {loop_time:.6f} seconds")
print(f"Vectorized time: {vectorized_time:.6f} seconds")
Comme on l’a vu, les opérations vectorisées avec Numpy sont beaucoup plus rapides que la version en boucle :
Output >>>
Loop time: 4.212010 seconds
Vectorized time: 0.047994 seconds
Conclusion
C’est tout pour ce tutoriel !
Nous avons examiné les conseils suivants : utiliser la compréhension de listes sur des boucles, tirer parti des générateurs pour un traitement efficace, mettre en cache les appels de fonctions coûteux, gérer les ressources avec des gestionnaires de contexte et vectoriser les opérations avec NumPy – qui peuvent vous aider à optimiser les performances de votre code.
Si vous recherchez des conseils spécifiques aux projets de science des données, lisez 5 meilleures pratiques Python pour la science des données.
Bala Priya C est une développeuse et rédactrice technique indienne. Elle aime travailler à l’intersection des mathématiques, de la programmation, de la science des données et de la création de contenu. Ses domaines d’intérêt et d’expertise incluent le DevOps, la science des données et le traitement du langage naturel. Elle aime lire, écrire, coder et prendre le café ! Actuellement, elle travaille à l’apprentissage et au partage de ses connaissances avec la communauté des développeurs en créant des didacticiels, des guides pratiques, des articles d’opinion, etc. Bala crée également des aperçus de ressources attrayants et des didacticiels de codage.