IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Plongez au coeur de Python ,De débutant à expert


précédentsommairesuivant

XVII. Fonctions dynamiques

XVII-A. Plonger

Je vais vous parler du pluriel des noms (en anglais). Nous verrons ensuite les fonctions qui retournent d'autres fonctions, les expressions régulières avancées et les générateurs. Les générateurs sont une nouveauté de Python 2.3. Mais commençons par le pluriel des noms.

Si vous n'avez pas lu le Chapitre 7, Expressions régulières, c'est un bon moment pour le faire. Ce chapitre part du principe que vous comprenez les bases des expressions régulières et traite de questions plus avancées.

L'anglais est une langue complexe qui emprunte à de nombreuses autres langues, ses règles pour le pluriel des noms sont multiples et complexes. Il y a les règles, les exceptions à ces règles et les exceptions à ces exceptions.

Si vous avez grandi dans un pays de langue anglaise ou si vous avez appris l'anglais à l'école, vous connaissez sans doute les règles de base :

  1. Si un mot se termine par S, X, ou Z, ajouter ES. «Bass» donne «basses», «fax» donne «faxes» et «waltz» donne «waltzes».
  2. Si un mot se termine par un H prononcé, ajouter ES, si il se termine par un H silencieux, ajouter S. Qu'est-ce qu'un H prononcé ? C'est un H associé à d'autres lettres pour produire un son. Donc «coach» donne «coaches» et «rash» donne «rashes», car on prononce les sons CH et SH. Mais «cheetah» devient «cheetahs», car le H est silencieux.
  3. Si un mot se termine par un Y prononcé comme I, remplacer le Y par IES, si le Y est associé à une voyelle pour donner un autre son, ajouter S. Donc «vacancy» donne «vacancies», mais «day» donne «days».
  4. Si le mot ne répond à aucune de ces règles, ajouter un S et prier pour que ce soit juste.

(Je sais qu'il y a beaucoup d'exceptions. «Man» donne «men» et «woman» donne «women», mais «human» donne «humans». «Mouse» donne «mice» et «louse» donne «lice», mais «house» donne «houses». «Knife» donne «knives» et «wife» donne «wives», mais «lowlife» donne «lowlifes». Sans parler des mots qui sont invariables, comme «sheep», «deer» et «haiku».)

Les autres langues ont, bien sûr, des règles complètement différentes.

Nous allons concevoir un module qui met les noms au pluriel. Nous commencerons par l'anglais, avec ce quatre règles, mais gardez à l'esprit qu'il faudra inévitablement ajouter d'autres règles et peut-être d'autres langues.

XVII-B. plural.py, étape 1

Nous avons donc des mots, qui, en anglais du moins, sont constitués de chaînes de caractères. Par ailleurs, nous avons des règles qui disent que nous devons reconnaître différentes combinaisons de caractères, et leur faire subir certaines modifications. C'est un problème qui semble être fait pour les expressions régulières.

Exemple 17.1. plural1.py

 
Sélectionnez
import re

def plural(noun):                            
    if re.search('[sxz]$', noun):             ***1***
        return re.sub('$', 'es', noun)        ***2***
    elif re.search('[^aeioudgkprt]h$', noun):
        return re.sub('$', 'es', noun)       
    elif re.search('[^aeiou]y$', noun):      
        return re.sub('y$', 'ies', noun)     
    else:                                    
        return noun + 's'

***1*** C'est une expression régulière, mais elle utilise une syntaxe que vous n'avez pas vue au Chapitre 7, Expressions régulières. Les crochets signifient «reconnaître exactement un de ces caractères». Donc [sxz] signifie «s ou x ou z», mais seulement l'une de ces trois lettres. Le $ doit vous être familier, il reconnaît la fin de la chaîne. Il s'agit donc de vérifier si noun se termine par s, x ou z.

***2*** La fonction re.sub effectue des remplacements à partir d'une expression régulière. Examinons-la en détail.

Exemple 17.2. Présentation de re.sub

 
Sélectionnez
>>> import re
>>> re.search('[abc]', 'Mark')   ***1***
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.sub('[abc]', 'o', 'Mark') ***2***
'Mork'
>>> re.sub('[abc]', 'o', 'rock') ***3***
'rook'
>>> re.sub('[abc]', 'o', 'caps') ***4***
'oops'

***1*** La chaîne Mark contient-elle a, b ou c ? Oui, elle contient a.

***2*** Maintenant, recherchons a, b ou c, et remplaçons-le par o. Mark devient Mork.

***3*** La même fonction transforme rock en rook.

***4*** Vous auriez pu croire qu'elle transformerait caps en oaps, mais ce n'est pas le cas. re.sub remplace tout ce qui a été reconnu, pas seulement la première occurrence. Cette expression régulière transforme donc caps en oops, aussi bien le c que le a sont remplacés par o.

Exemple 17.3. Retour à plural1.py

 
Sélectionnez
import re

def plural(noun):                            
    if re.search('[sxz]$', noun):            
        return re.sub('$', 'es', noun)        ***1***
    elif re.search('[^aeioudgkprt]h$', noun): ***2***
        return re.sub('$', 'es', noun)        ***3***
    elif re.search('[^aeiou]y$', noun):      
        return re.sub('y$', 'ies', noun)     
    else:                                    
        return noun + 's'

***1*** Retour à la fonction plural. Que faisons nous ? Nous remplaçons la fin de chaîne par es. En d'autre termes, nous ajoutons es à la chaîne. Nous pourrions accomplir la même chose avec une concaténation, par exemple noun + 'es', mais j'utilise les expressions régulières pour tous les cas pour des raisons de cohérence, raisons qui deviendront plus claires plus loin dans le chapitre.

***2*** Regardez attentivement, c'est encore une nouvelle variation. Le ^ comme premier caractère entre les crochets signifie quelque chose de spécial : la négation. [^abc] signifie «n'importe quel caractère unique sauf a, b ou c». Donc [^aeioudgkprt] signifie n'importe quel caractère sauf a, e, i, o, u, d, g, k, p, r ou t. Ensuite, ce caractère doit être suivi de h, suivi par la fin de chaîne. Nous cherchons les mots qui finissent par H dans lesquels le H est prononcé.

***3*** Même motif ici : reconnaître les mots qui finissent par Y, dans lesquels le caractère qui précède Y n'est pas a, e, i, o ou u. Nous recherchons des mots qui se finissent par un Y prononcé comme I.

Exemple 17.4. Expressions régulières avec négation

 
Sélectionnez
>>> import re
>>> re.search('[^aeiou]y$', 'vacancy') ***1***
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.search('[^aeiou]y$', 'boy')     ***2***
>>> 
>>> re.search('[^aeiou]y$', 'day')
>>> 
>>> re.search('[^aeiou]y$', 'pita')    ***3***
>>>

***1*** vacancy est reconnu par cette expression régulière parce qu'il se termine par cy, c n'est pas a, e, i, o ou u.

***2*** boy n'est pas reconnu, il se termine par oy et nous avons spécifié que le caractère précédent le y ne pouvait pas être o. day n'est pas reconnu car il se termine par ay.

***3*** pita n'est pas reconnu parce qu'il ne se termine pas par y.

Exemple 17.5. Fonctionnement de re.sub

 
Sélectionnez
>>> re.sub('y$', 'ies', 'vacancy')              ***1***
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'
>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy') ***2***
'vacancies'

***1*** Cette expression régulière transforme vacancy en vacancies et agency en agencies, ce qui est le but recherché. Notez qu'elle transformerait également boy en boies, mais cela n'arrivera jamais puisque nous faison d'abord le re.search pour savoir si nous devons effectuer le re.sub.

***2*** Juste au passage, je veux souligner qu'il est possible de combiner ces deux expressions régulières (une pour savoir si la règle s'applique, l'autre pour l'appliquer effectivement) en une seule expression régulière. Voilà à quoi ça ressemblerait. Elle devrait être familière : elle utilise un groupe mémorisé, ce que vous avez appris à la Section 7.6, «Etude de cas : reconnaissance de numéros de téléphone», pour se rappeler du caractère avant le y. Ensuite, dans la chaîne de substitution, elle utilise une nouvelle syntaxe, \1, qui signifie «hep, ce premier groupe que tu as mémorisé, met le ici». Dans ce cas, elle a mémorisé le c devant le y et donc elle substitue un c au c et ies à y (si on a plus d'un groupe mémorisé, on utilise \2 et \3 etc.).

Les remplacements par expressions régulières sont extrêmement puissants et la syntaxe \1 les rend encore plus puissants. Mais combiner l'opération entière en une seule expression régulière est également beaucoup plus difficile à lire et ne correspond pas à la manière dont les règles de pluriel des noms ont été définies au début. Les règles se présente de la manière suivante : «si le mot se termine par S, X ou Z, alors ajouter ES». Si vous regardez cette fonction, vous verrez deux lignes de codes qui disent «si le mot se termine par S, X ou Z, alors ajouter ES». Difficile de trouver une correspondance plus directe.

XVII-C. plural.py, étape 2

Maintenant, nous allons ajouter un niveau d'abstraction. Nous avons commencé par définir une liste de règles : si telle condition est remplie, alors effectuer telle action, sinon passer à la règle suivante. Nous allons temporairement rendre plus complexe une partie du programme pour pouvoir en simplifier une autre.

Exemple 17.6. plural2.py

 
Sélectionnez
import re

def match_sxz(noun):                          
    return re.search('[sxz]$', noun)          

def apply_sxz(noun):                          
    return re.sub('$', 'es', noun)            

def match_h(noun):                            
    return re.search('[^aeioudgkprt]h$', noun)

def apply_h(noun):                            
    return re.sub('$', 'es', noun)            

def match_y(noun):                            
    return re.search('[^aeiou]y$', noun)      
        
def apply_y(noun):                            
    return re.sub('y$', 'ies', noun)          

def match_default(noun):                      
    return 1                                  
        
def apply_default(noun):                      
    return noun + 's'                         

rules = ((match_sxz, apply_sxz),
         (match_h, apply_h),
         (match_y, apply_y),
         (match_default, apply_default)
         )                                     ***1***

def plural(noun):                             
    for matchesRule, applyRule in rules:       ***2***
        if matchesRule(noun):                  ***3***
            return applyRule(noun)             ***4***

***1*** Cette version a l'air plus compliqué (en tout cas, elle est certainement plus longue), mais elle fait exactement la même chose : elle tente de reconnaître les mêmes quatre règles, dans l'ordre, et applique l'expression régulière appropriée lorsqu'un motif est reconnu. La différence est que chaque règle de recherche et de modification est définie dans sa propre fonction et que ces fonctions sont assemblées et assignées à la variable rules, qui est un tuple de tuples.

***2*** A` l'aide d'une boucle for, nous pouvons appliquer deux règles à la fois (une de recherche et une de transformation) à partir du tuple rules. A` la première itération de la boucle for loop, matchesRule référencera match_sxz et applyRule apply_sxz. A` la seconde itération (si il y en a une), matchesRule référencera match_h et applyRule apply_h.

***3*** Rappelez vous que tout est objet en Python, y compris les fonctions. rules contient des fonctions, pas seulement des noms de fonctions. Lorsqu'elles sont assignées dans la boucle for, les variables matchesRule et applyRule sont des fonctions que vous pouvez appeler. Donc, à la première itération de la boucle for, cette ligne est l'équivalent d'un appel à matches_sxz(noun).

***4*** A` la première itération de la boucle for, ceci est l'équivalent d'un appel à apply_sxz(noun), etc

Si ce niveau d'abstraction vous semble obscur, essayer de déplier les fonctions. Cette boucle for est l'équivalent de ce qui suit :

Exemple 17.7. La fonction plural dépliée

 
Sélectionnez
def plural(noun):
    if match_sxz(noun):
        return apply_sxz(noun)
    if match_h(noun):
        return apply_h(noun)
    if match_y(noun):
        return apply_y(noun)
    if match_default(noun):
        return apply_default(noun)

L'avantage ici est que la fonction plural est simplifiée. Elle prend une liste de règles, définie ailleurs, et les parcours de manière générique. On prend une règle de recherche, la chaîne est-elle reconnue ? Alors on appelle la règle de transformation. Les règles pourraient être définies à n'importe quel endroit et de n'importe quelle manière, la fonction plural ne s'en occupe pas.

Maintenant, est-ce que l'ajout de ce niveau d'abstraction en valait la peine ? Et bien, pas pour le moment. Considérons ce qu'il faudrait faire pour ajouter une nouvelle règle. Dans la version précédente, il aurait fallu ajouter une instruction if à la fonction plural. Dans cette version, il faut ajouter deux fonctions, match_foo et apply_foo, et mettre à jour la liste de règles rules pour spécifier entre quelles autres règles la nouvelle règle doit être appelée.

Cette étape n'est qu'une base pour la prochaine section, continuons donc.

XVII-D. plural.py, étape 3

Définir de fonctions séparément pour chaque règle de recherche et de transformation n'est pas vraiment nécessaire. Nous ne les appelons jamais séparément, elles sont définies dans la liste de règles rules et appelées à partir de cette liste. Nous allons simplifier la définition des règles en rendant ces fonctions anonymes.

Exemple 17.8. plural3.py

 
Sélectionnez
import re

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)
    )
   )                                           ***1***

def plural(noun):                             
    for matchesRule, applyRule in rules:       ***2***
        if matchesRule(noun):                 
            return applyRule(noun)

***1*** C'est le même ensemble de règles qu'à l'étape 2. La seule différence est qu'au lieu de définir des fonctions nommées comme match_sxz et apply_sxz, nous avons «inclus» ces définitions directement dans la liste rules, à l'aide de fonctions lambda.

***2*** Notez que la fonction plural n'a pas changé. Elle parcourt l'ensemble des fonctions de règles, vérifie la première règle et, si celle-ci retourne vrai, appelle la seconde règle et retourne sa valeur. C'est la même fonction qu'à l'étape précédent, mot pour mot. La seule différence est dans la définition des fonctions de règles, mais la fonction plural ne s'en occupe pas, elle sait qu'on lui fournit une liste de règles et elle les applique aveuglément.

Maintenant, pour ajouter une nouvelle règle, il suffit de définir les fonctions directement dans la liste rules : une règle de recherche et une règle de transformation. Mais définir les fonctions de règles de cette manière met en valeur une duplication inutile. Nous avons quatre paires de fonctions qui suivent toutes le même motif. La fonction de recherche est un simple appel à re.search et la fonction de transformation un simple appel à re.sub. Nous allons extraire ces similarités.

XVII-E. 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

 
Sélectionnez
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

 
Sélectionnez
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

 
Sélectionnez
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

 
Sélectionnez
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. A` l'étape 2, elles étaient définies comme des fonctions nommées indépendantes. A` 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

 
Sélectionnez
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

 
Sélectionnez
>>> 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.

XVII-F. plural.py, étape 5

Nous avons extrait toute duplication de code et ajouter assez d'abstraction pour que les règles de pluriel des noms soient définies sous forme d'une liste de chaînes. La prochaine étape est logiquement de mettre ces chaînes dans un fichier séparé, pour qu'elles puissent être modifiées séparément du code qui les utilise.

D'abord, nous allons créer un fichier texte qui contient les règles. Ici, pas de structures de données sophistiquées, seulement des chaînes délimitées par des espaces (ou des tabulations) en trois colonnes. Nous l'appelons rules.en, «en» pour English. Ce sont les règles du pluriel des noms pour l'anglais. Nous pourrons ajouter d'autres fichiers de règles pour d'autre langues plus tard.

Exemple 17.15. rules.en

 
Sélectionnez
[sxz]$                  $               es
[^aeioudgkprt]h$        $               es
[^aeiou]y$              y$              ies
$                       $               s

Maintenant, voyons comment utiliser ce fichier de règles.

Exemple 17.16. plural5.py

 
Sélectionnez
import re
import string                                                                     

def buildRule((pattern, search, replace)):                                        
    return lambda word: re.search(pattern, word) and re.sub(search, replace, word) ***1***

def plural(noun, language='en'):                             ***2***
    lines = file('rules.%s' % language).readlines()          ***3***
    patterns = map(string.split, lines)                      ***4***
    rules = map(buildRule, patterns)                         ***5***
    for rule in rules:                                      
        result = rule(noun)                                  ***6***
        if result: return result

***1*** Nous utilisons encore la technique des fermetures (construire dynamiquement une fonction qui utilise des variables définies à l'extérieur de cette fonction), mais maintenant nous avons combiné les fonctions de recherche et de transformation en une seule fonction (la raison de cette modification apparaîtra à la prochaine section). Cela nous permettra de faire la même chose qu'avec les deux fonctions, mais l'appel en sera différent, comme nous allons le voir.

***2*** Notre fonction plural prend maintenant un second argument optionnel, language, qui vaut en par défaut.

***3*** Nous utilisons l'argument language pour construire un nom de fichier, puis nous ouvrons ce fichier et copions sont contenu dans une liste. Si le langage est en, nous allons donc : ouvrir rules.en, le lire en entier, le segmenter à chaque retour à la ligne et retourner une liste. Chaque ligne du fichier est un élément de la liste.

***4*** Comme vous l'avez vu, chaque ligne du fichier contient en fait trois valeurs, séparées par des espaces (espaces ou tabulations ne font pas de différence). En appliquant la fonction string.split à la liste, nous créons une nouvelle liste dans laquelle chaque élément est un tuple de trois chaînes. Donc, une ligne comme [sxz]$ $ es sera segmentée en un tuple ('[sxz]$', '$', 'es'). Cela signifie que patterns contient une liste de tuples, comme nous l'avions fait à la main à l'étape 4.

***5*** Si patterns est une liste de tuples, alors rules sera une liste de fonctions créées dynamiquement par chaque appel à buildRule. L'appel à Calling buildRule(('[sxz]$', '$', 'es')) retourne une fonction qui prend un seul argument, word. Quand cette fonction retournée est appelée, elle exécute re.search('[sxz]$', word) et re.sub('$', 'es', word).

***6*** Comme nous construisons maintenant une fonction combinée de recherche et de transformation, nous devons l'appeler différemment. Nous appelons simplement cette fonction et si elle retourne quelque chose, c'est le pluriel du nom, si elle ne retourne rien (None), alors la règle ne s'applique pas et il faut essayer la règle suivante.

L'amélioration ici est que nous avons complètement séparé les règles de pluriel des noms dans un fichier externe. Non seulement ce fichier peut-être maintenu séparément du code, mais nous avons défini une règle de nommage de fichier grâce à laquelle la même fonction plural peut utiliser des fichiers de règles différents en fonction d'un argument language.

L'inconvénient est que nous lisons le fichier à chaque fois que nous appelons la fonction plural. Je pensais pouvoir finir ce livre sans utiliser la phrase «laissé en exercice au lecteur», mais c'est raté : le développement d'un mécanisme de cache pour les fichiers de règles qui se rafraîchisse automatiquement lorsque un fichier de règle est modifié entre deux appels est laissé en exercice au lecteur. Amusez-vous bien.

XVII-G. plural.py, étape 6

Maintenant, vous êtes prêts à une discussion sur les générateurs.

Exemple 17.17. plural6.py

 
Sélectionnez
import re

def rules(language):                                                                 
    for line in file('rules.%s' % language):                                         
        pattern, search, replace = line.split()                                      
        yield lambda word: re.search(pattern, word) and re.sub(search, replace, word)

def plural(noun, language='en'):      
    for applyRule in rules(language): 
        result = applyRule(noun)      
        if result: return result

Nous utilisons ici une technique appelée un générateur, que je ne vais même pas tenter d'expliquer avant de vous montrer un exemple plus simple.

Exemple 17.18. Présentation des générateurs

 
Sélectionnez
>>> def make_counter(x):
...     print 'entering make_counter'
...     while 1:
...         yield x               ***1***
...         print 'incrementing x'
...         x = x + 1
...     
>>> counter = make_counter(2) ***2***
>>> counter                   ***3***
<generator object at 0x001C9C10>
>>> counter.next()            ***4***
entering make_counter
2
>>> counter.next()            ***5***
incrementing x
3
>>> counter.next()            ***6***
incrementing x
4

***1*** La présence du mot-clé yield dans make_counter signale qu'il ne s'agit pas d'une fonction ordinaire. C'est un genre de fonction spécial qui génère des valeurs une par une. Vous pouvez considérer cela comme une fonction qui reprend sont activité là où elle l'a laissée. L'appeler retourne un générateur qui peut être utilisée pour générer des valeurs successives de x.

***2*** Pour créer une instance du générateur make_counter, il suffit de l'appeler comme toute autre fonction. Notez que cela n'éxécute pas le code de la fonction, cela se voit au fait que la première ligne de make_counter est une instruction print, mais que rien n'est encore affiché.

***3*** La fonction make_counter retourne un objet générateur.

***4*** La première fois que vous appelez la méthode next() de l'objet générateur, elle exécute le code de make_counter jusqu'à la première instruction yield, puis retourne la valeur produite par yield. Dans ce cas, il s'agit de 2, puisque nous avons créé le générateur par l'appel make_counter(2).

***5*** A` chaque appel successif à next(), l'objet générateur reprend l'exécution au point où il l'avait laissé et continue jusqu'à l'instruction yield suivante. Les lignes de code attendant d'être exécutées sont l'instruction print qui affiche incrementing x, puis l'instruction x = x + 1 qui incrémente la variable. Ensuite, on boucle le while et on revient à l'instruction yield x, ce qui retourne la valeur actuelle de x (maintenant égale à 3).

***6*** La seconde fois que nous appelons counter.next(), nous faisons à nouveau la même chose, mais cette fois x vaut 4. Et cela continue de la même manière. Comme make_counter définit une boucle infinie, nous pourrions théoriquement continuer pour l'éternité, le générateur continuerait d'incrémenter x et de produire sa valeur. Mais nous allons examiner un exemple plus productif de l'utilisation des générateurs.

Exemple 17.19. Utilisation des générateurs à la place de la récursion

 
Sélectionnez
def fibonacci(max):
    a, b = 0, 1       ***1***
    while a < max:
        yield a       ***2***
        a, b = b, a+b ***3***

***1*** La suite de Fibonacci est une séquence de nombres dans laquelle chaque nombre est la somme des deux nombres qui le précèdent. Elle commence par 0 et 1, augmente doucement au début, puis de plus en plus vite. Pour commencer la séquence, nous utilisons deux variables : a commence à 0 et b à 1.

***2*** a est le nombre en cours dans la séquence, donc nous le produisons par yield.

***3*** b est le nombre suivant dans la séquence, nous l'assignons donc à a, mais nous calculons également la prochaine valeur (a+b) et l'assignons à b pour l'utiliser au prochain appel. Notez que les assignements sont faits en parallèle, si a vaut 3 et b vaut 5, alors a, b = b, a+b mettra a à 5 (la valeur précédente de b) et b à 8 (la somme des valeurs précédentes de a et b).

Donc, nous obtenons une fonction qui génère les nombres de Fibonacci. Bien sûr, vous pourriez le faire par récursion, mais cette manière est beaucoup plus simple à lire. De plus, elle fonctionne bien avec une boucle for.

Exemple 17.20. Les générateurs dans des boucles for

 
Sélectionnez
>>> for n in fibonacci(1000): ***1***
...     print n,              ***2***
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

***1*** Nous pouvons utiliser un générateur comme fibonacci dans une boucle for directement. La boucle for crée l'objet générateur et appelle successivement la méthode next() pour obtenir des valeurs à assigner à la variable d'index de boucle (n).

***2*** A` chaque parcours de la boucle for loop, n obtient une nouvelle valeur de l'instruction yield de fibonacci et tout ce que nous faisons est de l'afficher. Une fois que fibonacci n'a plus de valeur à retourner (a est plus grand que max, dans ce cas que 1000), alors la boucle for s'achève simplement.

Revenons maintenant à notre fonction plural et voyons l'usage que nous faisons de tout cela.

Exemple 17.21. Les générateurs pour produire des fonctions dynamiques

 
Sélectionnez
def rules(language):                                                                 
    for line in file('rules.%s' % language):                                          ***1***
        pattern, search, replace = line.split()                                       ***2***
        yield lambda word: re.search(pattern, word) and re.sub(search, replace, word) ***3***

def plural(noun, language='en'):      
    for applyRule in rules(language):  ***4***
        result = applyRule(noun)      
        if result: return result

***1*** for line in file(...) est un idiome habituel pour lire le contenu d'un fichier ligne par ligne. Cela fonctionne parce que file renvoit en fait un générateur dont la méthode next() retourne la ligne suivante du fichier. Personnellement, je trouve ça absolument génial.

***2*** Rien de magique ici. Rappelez-vous que les lignes du fichier de règles contiennent trois valeurs séparées par des espaces, line.split() retourne donc un tuple de trois valeurs qui sont assignées à trois variables locales.

***3*** Ici, nous utilisons yield. Qu'est-ce que nous produisons ? Une fonction construite dynamiquement avec lambda, qui est en fait une fermeture (elle utilise les variables locales pattern, search et replace comme constantes). Autrement dit, rules est un générateur de fonctions de règles.

***4 *** Comme rules est un générateur, nous pouvons l'utiliser directement dans une boucle for. A` la première itération à travers la boucle, nous appelons la fonction rules, qui ouvre le fichier de règles, en lit la première ligne, construit dynamiquement une fonction de recherche et de transformation pour la première règle définie dans le fichier et produit la fonction construite dynamiquement. A` la seconde itération, nous reprenons rules au point où nous l'avons laissé (c'est à dire au milieu de la boucle for line in file(...)), qui lit la seconde ligne, construit dynamiquement une nouvelle fonction de recherche et de transformation pour la seconde règle et la produit. Et ainsi de suite.

Qu'avons nous gagné par rapport à l'étape 5 ? A` l'étape 5, nous lisions le fichier de règles entièrement pour construire une liste de toutes les règles avant même d'essayer la première. Maintenant, grâce aux générateurs, nous pouvons faire tout cela de manière paresseuse : nous lisons la première règle et testons si elle s'applique, et si c'est le cas nous ne lisons pas l'ensemble du fichier ni ne créons d'autre fonctions.

Pour en savoir plus

XVII-H. Résumé

Vous devez maintenant vous sentir à l'aise avec toutes ces techniques :

  • Effectuer des remplacement de chaînes avec les expressions régulières.
  • Traiter les fonctions comme des objets, les stocker dans des listes, les assigner à des variables et les appeler par l'intermédiaire de ces variables.
  • Construire des fonctions dynamiques avec lambda.
  • Construire des fermetures, des fonctions dynamiques contenant les variables de leur environnement sous forme de constantes.
  • Construire des générateurs, des fonctions pouvant reprendre leur exécution au point où elle l'ont laissée et qui retournent des valeurs différentes de manière incrémentale à chaque fois qu'elle sont appelées.

Ajouter des abstractions, construire des fonctions dynamiquement, construire des fermetures et utiliser des générateurs sont autant de techniques qui peuvent rendre votre code plus simple, plus lisible et plus souple. Mais elles peuvent également le rendre plus difficile à déboguer par la suite. A` vous de trouver le bon équilibre entre simplicité et puissance.


précédentsommairesuivant