17.7. plural.py, étape 6

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

Exemple 17.17. plural6.py


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

>>> 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 À 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


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

>>> 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 À 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


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. À 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. À 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 ? À 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