17.5. plural.py, étape 4

Nous allons extraire la duplication de code pour rendre plus facile la définition de nouvelles règles.

Exemple 17.9. plural4.py


import re

def buildMatchAndApplyFunctions((pattern, search, replace)):  
    matchFunction = lambda word: re.search(pattern, word)      1
    applyFunction = lambda word: re.sub(search, replace, word) 2
    return (matchFunction, applyFunction)                      3
1 buildMatchAndApplyFunctions est une fonction qui construit d'autres fonctions dynamiquement. Elle prend en argument pattern, search et replace (en fait elle prend un tuple, mais nous expliquerons cela plus loin) et elle construit la fonction de recherche à l'aide de lambda comme une fonction prenant un argument (word) et appelant re.search avec le motif pattern passé à buildMatchAndApplyFunctions et l'argument word qui lui a été passé. Ouf !
2 La construction de la fonction de transformation se fait de la même manière. C'est une fonction qui prend un argument et appelle re.sub avec les arguments search et replace passés à buildMatchAndApplyFunctions, ainsi que cet argument word. Cette technique consistant à utiliser les valeurs d'arguments externes est appelée fermeture. Nous définissons en fait des constantes dans la fonction de transformation que nous construisons : elle prend un argument (word), mais agit ensuite sur cet argument et sur deux autres valeurs (search et replace) qui ont été définies lorsque la fonction de transformation a été créée.
3 Finalement, la fonction buildMatchAndApplyFunctions retourne un tuple de deux valeurs : les deux fonctions qui viennent d'être créées. Les constantes définies dans ces deux fonctions (pattern dans matchFunction, search et replace dans applyFunction) sont conservées avec ces fonctions, y compris après le retour de l'appel à buildMatchAndApplyFunctions. C'est incroyablement puissant.

Si cela semble totalement confus (et ça l'est certainement, c'est très inhabituel), la manière dont cette fonction est utilisée devrait éclaircir ces définitions.

Exemple 17.10. plural4.py, suite

patterns = \
  (
    ('[sxz]$', '$', 'es'),
    ('[^aeioudgkprt]h$', '$', 'es'),
    ('(qu|[^aeiou])y$', 'y$', 'ies'),
    ('$', '$', 's')
  )                                                 1
rules = map(buildMatchAndApplyFunctions, patterns)  2
1 Nos règles de pluriel des noms sont maintenant définies comme une série de chaînes (pas de fonctions). La première chaîne est l'expression régulière que l'on utilise avec re.search pour vérifier si la règle s'applique, les deuxième et troisième sont les expressions de recherche et de remplacement que l'on utilise avec re.sub pour appliquer effectivement la règle.
2 Cette ligne est magique. Elle prend la liste de chaînes de patterns et la transforme en une liste de fonctions. Comment ? En appliquant les chaînes à la fonction buildMatchAndApplyFunctions, qui prend trois chaînes en argument et retourne un tuple de deux fonctions. Cela veut dire que rules contient exactement la même chose que dans la version précédente : une liste de tuples de deux fonctions, la première étant la fonction de recherche qui appelle re.search et la seconde la fonction de transformation qui appelle re.sub.

Je vous assure que c'est vrai : rules contient exactement la même liste de fonction que dans la version précédente. Déplions la définition de rules, nous obtenons ce qui suit :

Exemple 17.11. La définition de rules dépliée

rules = \
  (
    (
     lambda word: re.search('[sxz]$', word),
     lambda word: re.sub('$', 'es', word)
    ),
    (
     lambda word: re.search('[^aeioudgkprt]h$', word),
     lambda word: re.sub('$', 'es', word)
    ),
    (
     lambda word: re.search('[^aeiou]y$', word),
     lambda word: re.sub('y$', 'ies', word)
    ),
    (
     lambda word: re.search('$', word),
     lambda word: re.sub('$', 's', word)
    )
   )                                          

Exemple 17.12. plural4.py, suite et fin


def plural(noun):                                  
    for matchesRule, applyRule in rules:            1
        if matchesRule(noun):                      
            return applyRule(noun)                 
1 Puisque la liste rules est la même que dans la version précédente, il n'est pas étonnant que la fonction plural ne soit pas modifiée. Rappelez-vous qu'elle est totalement générique, elle prend une liste de fonctions de règle et les appelles dans l'ordre. Elle ne s'occupe pas de la manière dont ces règles sont définies. À l'étape 2, elles étaient définies comme des fonctions nommées indépendantes. À l'étape 3, elles étaient définies comme des fonctions anonymes lambda. Maintenant, à l'étape 4, elles sont construites dynamiquement par la fonction buildMatchAndApplyFunctions à partir d'une liste de chaînes. De toute manière, la fonction plural agit de la même façon.

Au cas où tout cela ne suffirait pas à vous tourner la tête, je dois ajouter qu'il y a une petite subtilité dans la définition de buildMatchAndApplyFunctions que je n'ai pas expliquée. Regardons à nouveau cette définition.

Exemple 17.13. Retour sur buildMatchAndApplyFunctions


def buildMatchAndApplyFunctions((pattern, search, replace)):   1
1 Avez-vous remarqué les doubles parenthèses ? Cette fonction ne prend en fait pas trois arguments mais un seul : un tuple de trois éléments. Mais le tuple est développé lorsque la fonction est appelée et les trois éléments sont assignés à trois variables différentes : pattern, search et replace. Vous vous demandez pourquoi ? Voyons cela en détail

Exemple 17.14. Développement des tuples à l'appel de fonctions

>>> def foo((a, b, c)):
...     print c
...     print b
...     print a
>>> parameters = ('apple', 'bear', 'catnap')
>>> foo(parameters) 1
catnap
bear
apple
1 L'appel à la fonction foo se fait avec un tuple de trois éléments. Lorsque la fonction est appelée, les éléments sont assignés à trois variables locales à foo.

Maintenant, examinons pourquoi il est nécessaire d'utiliser le développement automatique de tuple. patterns est une liste de tuples, chacun ayant trois éléments. Lorsque nous appelons map(buildMatchAndApplyFunctions, patterns), cela signifie que buildMatchAndApplyFunctions n'est pas appelé avec trois arguments. Utiliser map pour appliquer une liste à une fonction appelle cette fonction avec à chaque fois un seul paramètre : chacun des éléments de la liste. Dans le cas de patterns, chaque élément de la liste est un tuple, donc buildMatchAndApplyFunctions est à chaque fois appelé avec le tuple et nous utilisons le développement automatique de tuple dans la définition de buildMatchAndApplyFunctions pour assigner les éléments de ce tuple à des variables locales avec lesquelles nous pouvons travailler.