Python n'est pas un langage rapide mais cela n'empêche pas que certaines façons d'écrire du code son plus lentes que d'autres. On peut accélérer un code de différentes manières. En voici quelques-unes. L'article se conclut par l'utilisation d'un profiler.
Optimisation du code
L'exemple suivant trie des éléments. Mais comme ceux-ci sont soit des chiffres sous formes de lettres, soit des entiers, il faut d'abord tous les convertir en entiers.
rangs = ['6', 7, '5', 9, 8, '3'] rangs = sorted([int(r) for r in rangs])
Il est plus simple de définir que ces nombres seront des entiers une bonne fois pour toutes et ne jamais faire de conversions. Il faut toujours avoir des listes d'objets de même type. Dans le cas contraire, c'est l'assurance de faire des erreurs. Dans le même genre d'idées, il faut éviter les fonctions qui retournent des résultats de types différents.
def gagnant_numero(main1, main2): if gagnant(main1,main2)==main1: return 1 elif gagnant(main1,main2)==main2: return 2 else: return "egalite"
De plus, dans cette fonction, la fonction gagnant est appelée deux fois avec les mêmes paramètres. Il ne sert à rien de la rappeler si le premier test est faux.
def gagnant_numero(main1, main2): g = gagnant(main1,main2) if g == main1: return 1 elif g == main2: return 2 else: return "egalite"
Optimisation algorithmique
Les deux fonctions suivantes calcule la fréquence des lettres dans un texte :
def compte_lettre(texte, alphabet): alphabet = [ chr(65+i) for i in range(0,26) ] return { l:texte.count(l) for l in alphabet } def compte_lettre2(texte): d = { } for c in texte: d[c] = d.get(c,0)+1 return d
Quelle est la plus rapide ? A priori, la première a un coût de O(26 len(texte)). La seconde a un coût de O(ln2(26) len(texte)). On mesure :
import time t = time.clock() for u in range(0,10): res = compte_lettre(texte) d = time.clock() - t print (d/10) # ~ 0.03s t = time.clock() for u in range(0,10): res = compte_lettre2(texte) d = time.clock() - t print (d/10) # ~ 0.15s
Etrange ? Pas forcément : la fonction count est une fonction du langage Python qui a été optimisée. Même si elle parcourt la chaîne de caractères complète, elle est beaucoup plus rapide que si on la parcourait soi-même (elle est codée en C++). Le coût de l'algorithme ci-dessus ne tient pas compte des caractéristiques du langage. En revanche, si on augmente la taille de l'alphabet pour compter tous les couples de lettres :
alpha2 = [ a+b for a in alphabet for b in alphabet ] def compte_lettre(texte, alphabet): return { l:texte.count(l) for l in alphabet } def compte_lettre2(texte): d = { } for i in range(1,len(texte)): c = texte[i-1:i+1] d[c] = d.get(c,0)+1 return d import time t = time.clock() for u in range(0,10): res = compte_lettre(texte, alpha2) d = time.clock() - t print (d/10) # ~ 0.40 t = time.clock() for u in range(0,10): res = compte_lettre2(texte) d = time.clock() - t print (d/10) # ~ 0.28
Comme l'alphabet a pour taille 262, la seconde version est plus efficace. L'aspect langage a moins d'importance.
Profiler
Lorsqu'on ne sait pas par où commencer pour accélérer son code, on peut utiliser profiler. C'est un outil qui espionne les fonctions pour compter le nombre d'appels, et le temps d'exécution. Il permet de détecter les parties du programme les plus exécutés. En Python, le profiler par défaut s'utilise comme ceci :
def main(): # le code à tester # ... import cProfile import re cProfile.run('main()')
Le programme est évidemment plus lent mais il enregistre les temps d'exécution de chaque fonction :
134123237 function calls (127083237 primitive calls) in 89.196 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 89.196 89.196 <string>:1(<module>) 4 0.125 0.031 89.196 22.299 fonctions_statistiques.py:6(proba_preflop_vs_random) 1144000 1.848 0.000 1.962 0.000 random.py:173(randrange) 1144000 0.566 0.000 2.528 0.000 random.py:236(randint) 902000 1.281 0.000 78.048 0.000 regles.py:102(gagnant_numero) 8008000/968000 5.612 0.000 5.715 0.000 regles.py:113(combinationsUniques) 44000 0.338 0.000 6.053 0.000 regles.py:122(combinaisons) 44000 0.544 0.000 83.608 0.002 regles.py:132(meilleure_main_joueur) 22000 0.107 0.000 84.784 0.004 regles.py:141(gagnant_showdown) 4842428 17.352 0.000 31.492 0.000 regles.py:25(main_triee) 4842428 14.593 0.000 19.743 0.000 regles.py:38(rangs_couleurs) 1095880 7.442 0.000 8.993 0.000 regles.py:50(estSuite) 3372472 11.857 0.000 57.545 0.000 regles.py:64(valeur) 1686236 3.902 0.000 76.767 0.000 regles.py:84(gagnant) 22000 1.471 0.000 4.245 0.000 structure.py:8(creer_jeu) 1 0.000 0.000 89.196 89.196 test_fonctions_statistiques.py:6(main) 3823880 0.326 0.000 0.326 0.000 {len} 1437872 0.387 0.000 0.387 0.000 {method 'add' of 'set' objects} 69628829 7.397 0.000 7.397 0.000 {method 'append' of 'list' objects} 20060549 3.462 0.000 3.462 0.000 {method 'count' of 'list' objects} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 1437872 0.176 0.000 0.176 0.000 {method 'discard' of 'set' objects} 44000 0.042 0.000 0.042 0.000 {method 'index' of 'list' objects} 3372472 0.649 0.000 0.649 0.000 {method 'join' of 'str' objects} 1144000 0.114 0.000 0.114 0.000 {method 'random' of '_random.Random' objects} 66004 0.056 0.000 0.056 0.000 {range} 5938308 9.546 0.000 9.546 0.000 {sorted}
On s'intéresse d'abord aux fonctions qui sont appelées un grand nombre de fois et dont le temps cumulé est important. Si on arrive à accélérer une de ces fonctions, le programme sera significativement plus rapide. Par exemple, dans la fonction main_triee, on remplace :
mult = [] for c in s: mult.append((rangs.count(c), CONV[c]))
Par
mult = [ (rangs.count(c), CONV[c]) for c in s ]
On réduit le temps cumulé passé dans la fonction de 5 secondes, ce qui correspond plus ou moins à la moitié du temps passé dans la méthode append.
L'inconvénient de ce profiler est qu'il ne distingue pas les appels à une fonction selon qu'elle est appelée depuis la fonction A ou B. Pour remédier à cela, il faut utiliser un profiler plus outillé : pycallgraph. Une fois installé (ainsi que Graphviz pour la visualisation des graphes), on l'utilise comme suit :
import os os.environ["PATH"] += r";C:\Program Files (x86)\Graphviz2.34\bin" from pycallgraph import PyCallGraph from pycallgraph.output import GraphvizOutput with PyCallGraph(output=GraphvizOutput()): main()
Pour obtenir le résultat suivant :
On voit mieux qui appelle qui et combien de fois. De cette façon, on ne perd pas son temps à réfléchir à l'optimisation d'une fonction qui n'est quasiment jamais appelée. Pour les programmes plus gros, il est préférable de ne lancer le profiler que sur une partie du programme.
<-- --> |