Le meilleur avec des tests unitaires exhaustifs, ce n’est pas le
sentiment que vous avez quand tous vos cas de test finissent par
passer, ni même le sentiment que vous avez quand quelqu’un vous
reproche d’avoir endommagé leur code et que vous pouvez véritablement
prouver que vous ne l’avez pas fait. Le meilleur,
c’est que les tests unitaires vous permettent la refactorisation
continue de votre code.
La refactorisation est le processus par lequel on fait d’un code
qui fonctionne un code qui fonctionne mieux. Souvent,
«mieux» signifie «plus vite», bien que cela
puisse aussi vouloir dire «avec moins de mémoire»,
«avec moins d’espace disque» ou
simplement «de manière plus élégante». Quoi que cela
signifie pour vous, pour votre projet, dans votre environnement, la
refactorisation est importante pour la santé à long terme de tout
programme.
Ici, «mieux» veut dire «plus vite». Plus
précisément, la fonction fromRoman est plus lente
qu’elle ne le devrait, à cause de cette énorme expression régulière que
nous utilisons pour valider les nombres romains. Cela ne vaut sans doute
pas la peine de se priver complètement de l’expression régulière (cela
serait difficile et ne serait pas forcément plus rapide), mais nous
pouvons rendre la fonction plus rapide en précompilant l’expression
régulière.
Exemple 15.10. Compilation d’expressions régulières
>>> import re>>> pattern = '^M?M?M?$'>>> re.search(pattern, 'M')<SRE_Match object at 01090490>>>> compiledPattern = re.compile(pattern)>>> compiledPattern<SRE_Pattern object at 00F06E28>>>> dir(compiledPattern)['findall', 'match', 'scanner', 'search', 'split', 'sub', 'subn']>>> compiledPattern.search('M')<SRE_Match object at 01104928>
C’est la syntaxe que nous avons déjà vu :
re.search prend une expression régulière sous
forme de chaîne (motif) et une chaîne dont on
va tester la correspondance ('M'). Si le motif
reconnaît la chaîne, la fonction retourne un objet correspondance
qui peut être interrogé pour savoir exactement ce qui a été
reconnu et comment.
C’est une nouvelle syntaxe : re.compile
prend une expression régulière sous forme de chaîne et retourne un
objet motif. Notez qu’il n’y a pas ici de chaîne à reconnaître.
Compiler une expression régulière n’a rien à voir avec la
reconnaissance d’une chaîne en particulier (comme
'M'), cela n’implique que l’expression
régulière elle-même.
L’objet motif compilé retourné par
re.compile a plusieurs fonctions qui ont
l’air utile, parmi lesquelles plusieurs (comme
search et sub) sont
directement disponible dans le module re.
Appeler la fonction search de l’objet
motif compilé avec la chaîne 'M' accomplit la
même chose qu’appeler re.search avec
l’expression régulière et la chaîne 'M'. Mais
c’est beaucoup, beaucoup plus rapide. (En fait, la fonction
re.search se contente de compiler
l’expression régulière et d’appeler la méthode
search de l’objet motif résultant pour
vous.)
A chaque fois que vous allez utiliser une expression régulière
plus d’une fois, il vaut mieux la compiler pour obtenir un objet motif
et appeler ses méthodes directement.
Exemple 15.11. Expressions régulières compilées dans roman81.py
Ce fichier est disponible dans le sous-répertoire py/roman/stage8/ du répertoire des exemples.
# toRoman and rest of module omitted for clarity
romanNumeralPattern = \
re.compile('^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$') def fromRoman(s):
"""convert Roman numeral to integer"""ifnot s:
raise InvalidRomanNumeralError, 'Input can not be blank'ifnot romanNumeralPattern.search(s): raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
Cela à l’air très similaire mais en fait beaucoup a changé.
romanNumeralPattern n’est plus une chaîne,
c’est un objet motif qui est retourné par
re.compile.
Cela signifie que nous pouvons appeler des méthodes de
romanNumeralPattern directement. Cela sera
beaucoup plus rapide que d’appeler re.search
à chaque fois. L’expression régulière est compilée une seule fois
et est stockée dans romanNumeralPattern quand
le module est importé pour la première fois, puis, à chaque fois
que nous appelons fromRoman, nous pouvons
immédiatement tester la correspondance de la chaîne d’entrée avec
l’expression régulière, sans que des étapes intermédiaire
interviennent en coulisse.
Mais à quel point est-ce plus rapide de compiler notre expression
régulière ? Voyez vous-même :
Exemple 15.12. Sortie de romantest81.py avec roman81.py
.............
----------------------------------------------------------------------
Ran 13 tests in 3.385s
OK
Juste une note en passant : cette fois, j’ai lancé le test
unitaire sans l’option -v,
donc au lieu d’avoir la doc string complète
pour chaque test, nous avons un point pour chaque test qui passe.
(Si un test échouait, nous aurions un F
(failed) et si il y avait une
erreur, nous aurions un E. Nous aurions quand
même la trace de pile pour chaque échec ou erreur de manière à
pouvoir localiser les problèmes.)
Nous avons exécuté 13 tests en
3,385 secondes, au lieu de 3,685
secondes sans précompilation de l’expression régulière.
C’est une amélioration de 8% et rappelez-vous
que la plus grande partie du temps passé dans le test unitaire est
consacré à d’autres chose. (J’ai testé séparément l’expression
régulière et j’ai découvert que sa compilation accélère la
fonction search de 54% en
moyenne.) Pas mal pour une modification aussi simple.
Oh, au cas ou vous vous le demandiez, la précompilation de
l’expression régulière n’a rien endommagé et nous venons de le
prouver.
Il y a une autre optimisation que je veux essayer. Etant donnée la
complexité de la syntaxe des expressions régulières, il n’est pas
étonnant qu’il y ait souvent plus d’une manière d’écrire la même
expression. Après une discussion à propos de ce module sur comp.lang.python,
quelqu’un m’a suggéré d’utiliser la syntaxe
{m,n}
pour des caractères répétés optionnels.
Exemple 15.13. roman82.py
Ce fichier est disponible dans le sous-répertoire py/roman/stage8/ du répertoire des exemples.
# rest of program omitted for clarity#old version#romanNumeralPattern = \# re.compile('^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$')#new version
romanNumeralPattern = \
re.compile('^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$')
Nous avons remplacé M?M?M?M? par
M{0,4}. Les deux signifient la même chose :
«reconnais de 0 à 4 M». De même,
C?C?C? est devenu C{0,3}
(«reconnais de 0 à 3 C») et ainsi
de suite pour X et I.
Cette forme d’expression régulière est un petit peu plus courte
(mais pas plus lisible). La question est, est-elle plus rapide ?
Exemple 15.14. Sortie de romantest82.py avec roman82.py
.............
----------------------------------------------------------------------
Ran 13 tests in 3.315s
OK
Dans l’ensemble, les tests unitaires sont accélérés de 2%
avec cette forme d’expression régulière. Cela n’a pas l’air d’être
grand chose mais rappelez-vous que la fonction
search n’est qu’une petite partie de
l’ensemble de nos tests unitaires, la plus grande partie du temps
est passée à faire autre chose. (En testant séparément
l’expression régulière, j’ai découvert que la fonction
search est accélérée de
11% avec cette syntaxe.) En précompilant
l’expression régulière et en en récrivant une partie, nous avons
amélioré la performance de l’expression régulière de plus de
60% et amélioré la performance d’ensemble des
tests unitaires de plus de 10%.
Plus important que tout bénéfice de performance est le fait
que le module fonctionne encore parfaitement. C’est là la liberté
dont je parlais plus haut : la liberté d’ajuster, de modifier ou
de récrire n’importe quelle partie et de vérifier que rien n’a
été endommagé durant ce processus. Ce n’est pas une autorisation
de fignoler indéfiniment le code pour le plaisir, nous avions un
objectif spécifique (rendre fromRoman
plus rapide) et nous avons rempli cet objectif sans que
subsiste le doute d’avoir introduit de nouveaux bogues.
Il y a une autre modification que j’aimerais faire et ensuite je
promet que j’arrêterai de refactoriser ce module. Comme nous l’avons vu
de manière répétée, les expressions régulières peuvent devenir
emberlificotées et illisibles assez vite. Je voudrais pouvoir revenir à
ce module dans six mois et être capable de le maintenir. Bien sûr les
cas de tests passent, je sais donc qu’il fonctionne mais si je ne peux
pas comprendre comment il fonctionne, je ne serai
pas capable d’ajouter des fonctionnalités, de corriger de nouveaux
bogues et plus généralement de le maintenir. Comme nous l’avons vu au Section 7.5, «Expressions régulières détaillées», Python fournit une manière de
documenter vos expressions régulières ligne à ligne.
Exemple 15.15. roman83.py
Ce fichier est disponible dans le sous-répertoire py/roman/stage8/ du répertoire des exemples.
# rest of program omitted for clarity#old version#romanNumeralPattern = \# re.compile('^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$')#new version
romanNumeralPattern = re.compile('''
^ # beginning of string
M{0,4} # thousands - 0 to 4 M's
(CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
# or 500-800 (D, followed by 0 to 3 C's)
(XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
# or 50-80 (L, followed by 0 to 3 X's)
(IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
# or 5-8 (V, followed by 0 to 3 I's)
$ # end of string
''', re.VERBOSE)
La fonction re.compile peut prendre un
second argument optionnel, un ensemble d’un
flag ou plus qui contrôle diverses
options pour l’expression régulière compilée. Ici, nous spécifions
le flagre.VERBOSE, qui signale à
Python qu’il y a des commentaires à
l’intérieur de l’expression régulière. Les commentaires ainsi que
les espaces les entourant ne sont pas
considérés comme faisant partie de l’expression régulière, la
fonction re.compile les enlève purement et
simplement lorsqu’elle compile l’expression. Cette nouvelle
version documentée est identique à l’ancienne mais elle est
beaucoup plus lisible.
Exemple 15.16. Sortie de romantest83.py avec roman83.py
.............
----------------------------------------------------------------------
Ran 13 tests in 3.315s
OK
Cette nouvelle version documentée s’exécute exactement à la
même vitesse que l’ancienne. En fait, l’objet motif compilé est le
même, puisque la fonction re.compile supprime
tout ce que nous avons ajouté.
Cette nouvelle version passe tous les tests que passait
l’ancienne. Rien n’a changé, sauf que le programmeur qui se
penchera à nouveau sur ce module dans six mois aura une chance de
comprendre le fonctionnement de la fonction.