Comment optimiser vos scripts Python pour de meilleures performances

Comment Optimiser Vos Scripts Python Pour De Meilleures Performances



L'optimisation des scripts Python pour de meilleures performances implique d'identifier et de résoudre les goulots d'étranglement dans notre code, le rendant ainsi plus rapide et plus efficace. Python est un langage de programmation populaire et puissant qui est aujourd'hui utilisé dans de nombreuses applications, notamment l'analyse de données, les projets ML (apprentissage automatique), le développement Web et bien d'autres. L'optimisation du code Python est une stratégie visant à améliorer la vitesse et l'efficacité du programme de développement lors de l'exécution de toute activité en utilisant moins de lignes de code, moins de mémoire ou des ressources supplémentaires. Un code volumineux et inefficace peut ralentir le programme, ce qui peut entraîner une mauvaise satisfaction des clients et une perte financière potentielle, ou la nécessité de travailler davantage pour réparer et dépanner.

Il est nécessaire lors de l'exécution d'une tâche qui nécessite le traitement de plusieurs actions ou données. Par conséquent, la suppression et l’amélioration de certains blocs de code et fonctionnalités inefficaces peuvent donner des résultats étonnants comme les suivants :

  1. Boostez les performances de l’application
  2. Créer du code lisible et organisé
  3. Rendre la surveillance des erreurs et le débogage plus simples
  4. Conserver une puissance de calcul considérable, etc.

Profilez votre code

Avant de commencer l’optimisation, il est essentiel d’identifier les parties du code du projet qui le ralentissent. Les techniques de profilage en Python incluent les packages cProfile et profile. Utilisez de tels outils pour évaluer la rapidité d’exécution de certaines fonctions et lignes de code. Le module cProfile produit un rapport qui détaille la durée d'exécution de chaque fonction de script. Ce rapport peut nous aider à trouver les fonctions qui s'exécutent lentement afin que nous puissions les améliorer.







Extrait de code:



importer cProfil comme CP
déf calculerSomme ( Numéro d'entrée ) :
sum_of_input_numbers = 0
alors que Numéro d'entrée > 0 :
somme_de_numéros_d'entrée + = Nombre d'entrée % dix
numéro d'entrée // = dix
imprimer ( 'La somme de tous les chiffres du numéro d'entrée est : 'sum_of_input_numbers'' )
retour sum_of_input_numbers
déf main_func ( ) :
CP. courir ( 'calculerSomme(9876543789)' )
si __nom__ == '__principal__' :
main_func ( )

Le programme effectue un total de cinq appels de fonction, comme indiqué sur la première ligne du résultat. Les détails de chaque appel de fonction sont affichés dans les quelques lignes suivantes, y compris le nombre de fois où la fonction a été invoquée, la durée globale de la fonction, la durée par appel et la durée globale de la fonction (y compris toutes les fonctions auxquelles il est appelé).



De plus, le programme imprime un rapport sur l'écran d'invite qui montre que le programme termine le temps d'exécution de toutes ses tâches en 0,000 seconde. Cela montre à quelle vitesse le programme est.





Choisissez la bonne structure de données

Les caractéristiques de performances dépendent de la structure des données. En particulier, les dictionnaires sont plus rapides pour les recherches que les listes concernant le stockage à usage général. Sélectionnez la structure de données la plus adaptée aux opérations que nous effectuerons sur vos données si vous les connaissez. L'exemple suivant étudie l'efficacité de différentes structures de données pour un processus identique afin de déterminer si un élément de la structure de données est présent.



Nous évaluons le temps nécessaire pour vérifier si un élément est présent dans chaque structure de données (une liste, un ensemble et un dictionnaire) et les comparons.

OptimizeDataType.py :

importer Timei comme tt
importer aléatoire comme rndobj
# Générer une liste d'entiers
liste_données_aléatoires = [ rndobj. randint ( 1 , 10000 ) pour _ dans gamme ( 10000 ) ]
# Créer un ensemble à partir des mêmes données
random_data_set = ensemble ( liste_données_aléatoires )

# Créez un dictionnaire avec les mêmes données que les clés
obj_DataDictionary = { sur une: Aucun pour sur une dans liste_données_aléatoires }

# Élément à rechercher (existe dans les données)
random_number_to_find = rndobj. choix ( liste_données_aléatoires )

# Mesurer le temps de vérification de l'appartenance à une liste
liste_heure = tt. Timei ( lambda : random_number_to_find dans liste_données_aléatoires , nombre = 1000 )

# Mesurer le temps nécessaire pour vérifier l'appartenance à un ensemble
régler le temps = tt. Timei ( lambda : random_number_to_find dans random_data_set , nombre = 1000 )

# Mesurer le temps nécessaire pour vérifier l'appartenance à un dictionnaire
dict_time = tt. Timei ( lambda : random_number_to_find dans obj_DataDictionary , nombre = 1000 )

imprimer ( F 'Durée de vérification de l'adhésion à la liste : {list_time:.6f} secondes' )
imprimer ( F 'Définir l'heure de vérification de l'adhésion : {set_time:.6f} secondes' )
imprimer ( F 'Durée de vérification de l'adhésion au dictionnaire : {dict_time:.6f} secondes' )

Ce code compare les performances des listes, des ensembles et des dictionnaires lors des vérifications d'appartenance. En général, les ensembles et les dictionnaires sont nettement plus rapides que les listes pour les tests d'appartenance car ils utilisent des recherches basées sur le hachage, ils ont donc une complexité temporelle moyenne de O(1). Les listes, en revanche, doivent effectuer des recherches linéaires qui aboutissent à des tests d'appartenance avec une complexité temporelle O(n).

  Une capture d'écran d'un ordinateur Description générée automatiquement

Utilisez les fonctions intégrées au lieu des boucles

De nombreuses fonctions ou méthodes intégrées à Python peuvent être utilisées pour effectuer des tâches typiques telles que le filtrage, le tri et le mappage. Utiliser ces routines plutôt que de créer ses boucles permet d’accélérer le code car elles sont souvent optimisées en termes de performances.

Créons un exemple de code pour comparer les performances de création de boucles personnalisées en utilisant les fonctions intégrées pour les tâches typiques (telles que map(), filter() et sorted()). Nous évaluerons les performances des différentes méthodes de cartographie, de filtration et de tri.

BuiltInFunctions.py :

importer Timei comme tt
# Exemple de liste de numéros_liste
liste_numéros = liste ( gamme ( 1 , 10000 ) )

# Fonction pour mettre au carré number_list en utilisant une boucle
déf square_using_loop ( liste_numéros ) :
résultat_carré = [ ]
pour sur une dans liste_numéros :
résultat_carré. ajouter ( sur une ** 2 )
retour résultat_carré
# Fonction pour filtrer même number_list à l'aide d'une boucle
déf filter_even_using_loop ( liste_numéros ) :
filtre_result = [ ]
pour sur une dans liste_numéros :
si sur une % 2 == 0 :
filtre_result. ajouter ( sur une )
retour filtre_result
# Fonction pour trier number_list à l'aide d'une boucle
déf sort_using_loop ( liste_numéros ) :
retour trié ( liste_numéros )
# Mesurez le temps nécessaire pour mettre au carré number_list en utilisant map()
map_time = tt. Timei ( lambda : liste ( carte ( lambda x : x ** 2 , liste_numéros ) ) , nombre = 1000 )
# Mesurez le temps nécessaire pour filtrer même number_list en utilisant filter()
filtre_heure = tt. Timei ( lambda : liste ( filtre ( lambda x : x % 2 == 0 , liste_numéros ) ) , nombre = 1000 )
# Mesurez le temps nécessaire pour trier number_list en utilisant sorted()
heure_triée = tt. Timei ( lambda : trié ( liste_numéros ) , nombre = 1000 )
# Mesurez le temps nécessaire pour mettre au carré number_list à l'aide d'une boucle
boucle_map_time = tt. Timei ( lambda : square_using_loop ( liste_numéros ) , nombre = 1000 )
# Mesurez le temps nécessaire pour filtrer même number_list à l'aide d'une boucle
boucle_filter_time = tt. Timei ( lambda : filter_even_using_loop ( liste_numéros ) , nombre = 1000 )
# Mesurez le temps de tri de number_list à l'aide d'une boucle
boucle_sorted_time = tt. Timei ( lambda : sort_using_loop ( liste_numéros ) , nombre = 1000 )
imprimer ( 'La liste de numéros contient 10 000 éléments' )
imprimer ( F 'Durée Map() : {map_time:.6f} secondes' )
imprimer ( F 'Durée du filtre() : {filter_time:.6f} secondes' )
imprimer ( F 'Temps trié() : {sorted_time:.6f} secondes' )
imprimer ( F 'Durée de boucle (carte) : {loop_map_time:.6f} secondes' )
imprimer ( F 'Durée de boucle (filtre) : {loop_filter_time:.6f} secondes' )
imprimer ( F « Durée de boucle (triée) : {loop_sorted_time : 0,6f} secondes » )

Nous observerons probablement que les fonctions intégrées (map(), filter() et sorted()) sont plus rapides que les boucles personnalisées pour ces tâches courantes. Les fonctions intégrées à Python offrent une approche plus concise et compréhensible pour effectuer ces tâches et sont hautement optimisées pour les performances.

Optimiser les boucles

Si l’écriture des boucles est nécessaire, nous pouvons utiliser quelques techniques pour les accélérer. Généralement, la boucle range() est plus rapide que l’itération vers l’arrière. En effet, range() génère un itérateur sans inverser la liste, ce qui peut être une opération coûteuse pour les longues listes. De plus, comme range() ne crée pas de nouvelle liste en mémoire, elle utilise moins de mémoire.

OptimizeLoop.py :

importer Timei comme tt
# Exemple de liste de numéros_liste
liste_numéros = liste ( gamme ( 1 , 100000 ) )
# Fonction pour parcourir la liste dans l'ordre inverse
déf boucle_reverse_iteration ( ) :
résultat_inverse = [ ]
pour j dans gamme ( seulement ( liste_numéros ) - 1 , - 1 , - 1 ) :
result_reverse. ajouter ( liste_numéros [ j ] )
retour résultat_inverse
# Fonction pour parcourir la liste en utilisant range()
déf boucle_range_iteration ( ) :
plage_résultat = [ ]
pour k dans gamme ( seulement ( liste_numéros ) ) :
plage_résultat. ajouter ( liste_numéros [ k ] )
retour plage_résultat
# Mesurez le temps nécessaire pour effectuer une itération inverse
heure_inverse = tt. Timei ( boucle_reverse_iteration , nombre = 1000 )
# Mesurez le temps nécessaire pour effectuer une itération de plage
plage_heure = tt. Timei ( boucle_range_iteration , nombre = 1000 )
imprimer ( 'La liste de numéros contient 100 000 enregistrements' )
imprimer ( F « Temps d'itération inverse : {reverse_time : 0,6f} secondes » )
imprimer ( F « Durée d'itération de la plage : {range_time : 0,6f} secondes » )

Évitez les appels de fonction inutiles

Il y a une certaine surcharge à chaque fois qu'une fonction est appelée. Le code s'exécute plus rapidement si les appels de fonction inutiles sont évités. Par exemple, au lieu d'exécuter à plusieurs reprises une fonction qui calcule une valeur, essayez de stocker le résultat du calcul dans une variable et de l'utiliser.

Outils de profilage

Pour en savoir plus sur les performances de votre code, en plus du profilage intégré, nous pouvons utiliser des packages de profilage externes tels que cProfile, Pyflame ou SnakeViz.

Résultats du cache

Si notre code doit effectuer des calculs coûteux, nous pourrions envisager de mettre les résultats en cache pour gagner du temps.

Refactorisation du code

Refactoriser le code pour le rendre plus facile à lire et à maintenir est parfois une partie nécessaire de son optimisation. Un programme plus rapide peut également être plus propre.

Utilisez la compilation juste à temps (JIT)

Des bibliothèques comme PyPy ou Numba peuvent fournir une compilation JIT qui peut accélérer considérablement certains types de code Python.

Mettre à niveau Python

Assurez-vous que vous utilisez la dernière version de Python, car les versions plus récentes incluent souvent des améliorations de performances.

Parallélisme et concurrence

Pour les processus pouvant être parallélisés, étudiez les techniques de parallèle et de synchronisation telles que le multitraitement, le threading ou l'asyncio.

N'oubliez pas que l'analyse comparative et le profilage doivent être les principaux moteurs de l'optimisation. Concentrez-vous sur l'amélioration des domaines de notre code qui ont les effets les plus significatifs sur les performances et testez constamment vos améliorations pour vous assurer qu'elles produisent les effets souhaités sans introduire davantage de défauts.

Conclusion

En conclusion, l’optimisation du code Python est cruciale pour améliorer les performances et l’efficacité des ressources. Les développeurs peuvent considérablement augmenter la vitesse d'exécution et la réactivité de leurs applications Python en utilisant diverses techniques telles que la sélection des structures de données appropriées, l'exploitation des fonctions intégrées, la réduction des boucles supplémentaires et la gestion efficace de la mémoire. Une analyse comparative et un profilage continus devraient orienter les efforts d'optimisation, garantissant que les avancées du code correspondent aux exigences de performances réelles. Pour garantir le succès du projet à long terme et réduire le risque d'introduction de nouveaux problèmes, l'optimisation du code doit constamment être équilibrée avec les objectifs de lisibilité et de maintenabilité du code.