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
... print 'incrementing x'
... x = x + 1
...
>>> counter = make_counter(2)
>>> counter
<generator object at 0x001C9C10>
>>> counter.next()
entering make_counter
2
>>> counter.next()
incrementing x
3
>>> counter.next()
incrementing x
4
|
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.
|
|
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é.
|
|
La fonction make_counter retourne un
objet générateur.
|
|
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).
|
|
À 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).
|
|
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
while a < max:
yield a
a, b = b, a+b
|
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.
|
|
a est le nombre en cours dans la
séquence, donc nous le produisons par
yield.
|
|
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):
... print n,
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
|
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).
|
|
À 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):
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
|
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.
|
|
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.
|
|
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.
|
|
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.