I. Présentation▲
En tant que fils d'un bibliothécaire diplômé en anglais, j'ai toujours été fasciné par les langages. Pas les langages de programmation. En fait, si, les langages de programmation, mais aussi les langues naturelles. Prenez l'anglais par exemple, c'est une langue schizophrène qui emprunte des mots à l'allemand, au français, à l'espagnol et au latin (pour n'en citer que quelques-uns). En fait, « emprunter » n'est pas le bon mot, il s'agit plus de « pillage ». Ou peut-être d'assimilation - comme les Borgs. Oui, ça me plaît bien.
Nous sommes les Borgs. Nous intégrerons vos caractéristiques linguistiques et étymologiques aux nôtres. Toute résistance serait futile.
Dans ce tutoriel, nous allons parler du pluriel des noms. Mais aussi de fonctions qui renvoient d'autres fonctions, d'expressions régulières avancées et de générateurs. Tout d'abord, comment former le pluriel ? (Si vous n'avez pas lu le chapitre sur les expressions régulières, ce serait le bon moment. Dans ce tutoriel, nous supposons que vous comprenez les fondamentaux des expressions régulières, et nous plongerons rapidement vers des utilisations plus avancées.)
Si vous avez grandi dans un pays anglophone ou avez appris l'anglais dans un cursus scolaire, vous êtes probablement à l'aise avec les règles de base :
- si un mot se termine par S, X ou Z, on ajoute ES. Ainsi, « Bass »' devient « basses », « fax » devient « faxes » et « waltz » devient « waltzes » ;
- si un mot se termine par un H prononcé, on ajoute ES ; s'il se termine par un H muet, on ajoute S. Qu'est-ce qu'un H prononcé ? C'est une combinaison avec d'autres lettres pour former un son. Par exemple, « coach » devient « coaches » et « rash » devient « rashes », on entend les sons CH et SH lorsque l'on prononce ces mots. Par contre, « cheetah » devient « cheetahs », car il s'agit d'un H muet.
- Si un mot se termine par un Y sonnant comme un I, on change le Y en IES ; si le Y est combiné à une voyelle formant ainsi un son différent, on ajoute S. Ainsi, « vacancy » devient « vacancies » alors que « day » devient « days ».
Dans tous les autres cas, on ajoute S en espérant que tout se passera bien.
(Je sais, il y a beaucoup d'exceptions. « Man » devient « men » et « woman » devient « women » alors que « human » devient « humans ». « Mouse » devient « mice » et « louse » devient « lice » alors que « house » devient « houses ». « Knife » devient « knives » et « wife » devient « wives » alors que « lowlife » devient « lowlifes ». Et ne me lancez pas sur les mots qui sont leur propre forme plurielle comme « sheep », « deer » et « haiku ».)
Les autres langues sont évidemment complètement différentes.
Nous allons concevoir une bibliothèque Python qui pluralise automatiquement les noms anglais. Nous commencerons avec ces quatre règles, mais n'oubliez pas que l'on devra inévitablement en ajouter d'autres.
II. Je sais, utilisons les expressions régulières !▲
Regarder des mots revient à regarder des chaînes de caractères, en anglais du moins. Nous avons des règles qui nous disent de trouver différentes combinaisons de caractères pour ensuite les modifier. Voilà bien une tâche pour les expressions régulières !
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
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'
- C'est une expression régulière, mais il s'agit d'une syntaxe différente de celle vue dans le chapitre sur les expressions régulières. Les crochets signifient « correspondant exactement à l'un de ces caractères ». Ainsi, [sxz] signifie « correspondant à s ou x ou z », mais seulement à l'un d'entre eux. Le $ devrait vous être familier ; il identifie la fin d'une chaîne de caractères. En combinant ces éléments, cette expression régulière teste si noun (nom en anglais) se termine par s, x ou z.
- La fonction re.sub() permet la substitution de caractères, basée sur les expressions régulières.
Observons l'expression régulière de substitution plus en détail.
2.
3.
4.
5.
6.
7.
8.
9.
>>>
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'
- Est-ce que la chaîne de caractères Mark contient la lettre a, b ou c ? Oui, il y a un a.
- OK, il faut maintenant trouver les caractères a, b ou c et les remplacer par o. Mark devient Mork.
- Cette même fonction transforme rock en rook.
- On peut se dire que cela transformerait caps en oaps, mais ce n'est pas le cas. re.sub() remplace toutes les correspondances, pas seulement la première. Cette expression régulière transforme donc caps en oops, en effet les deux caractères c et a sont changés en o.
Et maintenant, retour à la fonction plural()…
2.
3.
4.
5.
6.
7.
8.
9.
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)
elif
re.search
(
'[^aeiou]y$'
, noun): # (3)
return
re.sub
(
'y$'
, 'ies'
, noun)
else
:
return
noun +
's'
- Ici, nous remplaçons la fin de la chaîne de caractères (identifiée par $) par la chaîne es. Autrement dit, on ajoute es à la chaîne de caractères. On pourrait obtenir le même résultat en appliquant la concaténation de chaînes, par exemple noun + 'es' mais j'ai fait le choix d'utiliser les expressions régulières pour chaque règle, pour des raisons que vous comprendrez plus loin dans le chapitre.
- Observons de plus près, il y a une nouvelle variante. Le ^ placé en tête dans les crochets signifie quelque chose de spécial : la négation. Ainsi, [^abc] signifie « tout caractère excepté a, b et c ». [^aeioudgkprt] signifie donc tout caractère excepté a, e, i, o, u, d, g, k, p, r, et t. Ensuite, il faut que ce caractère soit suivi de h puis de la fin de la chaîne. Cela nous permet de chercher les mots se terminant par un H prononcé.
- Même principe ici : on cherche les mots finissant par Y, où le caractère précédant Y n'est pas a, e, i, o ou u. Cela nous permet de chercher des mots se terminant par Y prononcé I.
Observons l'expression régulière de négation plus en détail.
2.
3.
4.
5.
6.
7.
8.
9.
>>>
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)
>>>
- vacancy correspond à cette expression régulière, car ce mot se termine par cy et c n'est pas a, e, i, o ou u.
- boy ne correspond pas, car ce mot se termine par oy et nous avons spécifié que le caractère précédant Y ne pouvait pas être o. De même, day ne correspond pas, car il se termine par ay.
- pita ne correspond pas, car cette chaîne de caractères ne se termine pas par y.
2.
3.
4.
5.
6.
>>>
re.sub
(
'y$'
, 'ies'
, 'vacancy'
) # (1)
'vacancies'
>>>
re.sub
(
'y$'
, 'ies'
, 'agency'
)
'agencies'
>>>
re.sub
(
'([^aeiou])y$'
, r'\1ies'
, 'vacancy'
) # (2)
'vacancies'
- Cette expression régulière transforme vacancy en vacancies et agency en agencies, ce qui correspond à ce que l'on souhaitait. Notons que cela convertirait aussi boy en boies, mais cela n'arrivera jamais dans la fonction, car on applique d'abord re.search() afin de voir si la fonction re.sub() doit être appliquée ou non.
- Soit dit en passant, je tiens à vous faire remarquer qu'il est possible de combiner ces deux expressions régulières (celle qui vérifie la règle et celle qui applique la modification) en une seule expression régulière. Voici à quoi cela ressemblerait. Tout ceci devrait vous sembler familier : on utilise un groupe capturé en mémoire, comme on l'a vu dans l'étude de cas Parsing Phone Numbers. Le groupe est utilisé pour se souvenir du caractère précédant la lettre y. Ensuite, dans la chaîne de substitution, on utilise une nouvelle syntaxe, \1, qui signifie « Tiens, ce premier groupe que t'as gardé en mémoire ? Mets-le là. » Dans notre cas, on se souvient du c avant le y ; quand on effectue la substitution, on remplace le c par c et le y par ies. (Si on a capturé plus d'un groupe en mémoire, on peut utiliser \2 et \3 et ainsi de suite.)
Les substitutions par expressions régulières sont très puissantes, et la syntaxe \1 les rend même encore plus pratiques. Mais combiner l'opération entière en une seule expression régulière rend aussi sa lecture plus difficile et cela ne correspond pas parfaitement à la façon dont nous avions décrit les règles de pluralisation. Dans un premier temps, nous avions exposé les règles comme « si le mot se termine par S, X ou Z alors on ajoute ES ». Cette règle peut être codée en deux lignes. On ne peut pas faire plus simple.
III. Une liste de fonctions▲
Nous allons maintenant ajouter un niveau d'abstraction. Nous avions commencé par définir une liste de règles : si ceci, fais cela, sinon va à la règle suivante. Nous allons provisoirement compliquer une partie du code pour en simplifier une autre.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
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): # (1)
return
re.search
(
'[^aeiou]y$'
, noun)
def
apply_y
(
noun): # (2)
return
re.sub
(
'y$'
, 'ies'
, noun)
def
match_default
(
noun):
return
True
def
apply_default
(
noun):
return
noun +
's'
rules =
((
match_sxz, apply_sxz), # (3)
(
match_h, apply_h),
(
match_y, apply_y),
(
match_default, apply_default)
)
def
plural
(
noun):
for
matches_rule, apply_rule in
rules: # (4)
if
matches_rule
(
noun):
return
apply_rule
(
noun)
- Maintenant, chaque règle de correspondance (match rule) est sa propre fonction qui renvoie les résultats de l'appel à la fonction re.search().
- Chaque règle d'application (apply rule) est aussi sa propre fonction qui fait appel à la fonction re.sub() afin d'appliquer la règle de pluralisation appropriée.
- Au lieu d'avoir une fonction (plural()) avec de multiples règles, nous avons la structure de données rules, qui correspond à une séquence de paires de fonctions.
- Les règles ayant été séparées en une structure de données à part, la nouvelle fonction plural() peut être réduite à quelques lignes de code. En utilisant une boucle for, on peut ainsi extraire les deux règles (de correspondance et d'application) à la fois depuis la structure rules. À la première itération de la boucle for, matches_rule obtiendra match_sxz, et apply_rule obtiendra apply_sxz. À la deuxième itération (en supposant qu'on arrive jusque là), matches_rule prendra la valeur match_h et apply_rule la valeur apply_h. En définitive, la fonction renverra toujours quelque chose, car la règle finale de correspondance (match_default) renvoie True, signifiant que la règle d'application (apply_default) sera toujours appliquée.
Cette technique fonctionne bien, car en Python, tout est objet, même les fonctions. La structure de données rules contient des fonctions, non pas des noms de fonctions, mais bel et bien des objets de type fonction. Quand elles sont affectées dans la boucle for, matches_rule et apply_rule deviennent des fonctions auxquelles on peut faire appel. À la première itération de la boucle for, cela revient à appeler matches_sxz(noun) et si l'on a une correspondance, on appelle apply_sxz(noun).
Si ce niveau d'abstraction supplémentaire vous perturbe, essayez de dérouler la fonction pour comprendre son fonctionnement. La boucle for est équivalente à ce qui suit :
2.
3.
4.
5.
6.
7.
8.
9.
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 de tout cela est que la fonction plural() est maintenant simplifiée. On a simplement besoin d'une séquence de règles, définies ailleurs et on répète le processus à travers ces règles de façon générique :
1. Obtenir une règle de correspondance ;
2. Le motif est-il reconnu ? Si oui, appeler ensuite la fonction d'application et renvoyer le résultat ;
3. Aucune correspondance ? Aller à l'étape 1.
On peut définir les règles où l'on veut, comme l'on veut. La fonction plural() ne s'en préoccupe pas.
L'ajout de ce niveau d'abstraction en vaut-il la peine ? Eh bien, pas encore. Regardons comment l'on s'y prendrait pour ajouter une nouvelle règle à la fonction. Dans le premier exemple, il faudrait pour cela ajouter une clause if à la fonction plural(). Dans le second exemple, il faudrait ajouter deux fonctions, match_foo() et apply_foo() et ensuite mettre à jour la séquence rules pour spécifier l'ordre dans lequel les nouvelles règles de correspondance et d'application devraient être appelées par rapport aux autres règles.
Mais tout ceci n'est qu'un tremplin pour la prochaine section. Allons-y…
IV. Une liste de motifs▲
Définir des noms séparés de fonctions pour chaque règle de correspondance et d'application n'est pas vraiment nécessaire. On ne les appelle jamais directement ; on les ajoute à la séquence rules et on les appelle de là. De plus, chaque fonction suit l'un des deux modèles suivants. Toutes les fonctions de correspondance appellent re.search() et toutes les fonctions d'application appellent re.sub(). Simplifions ces modèles afin de faciliter la définition de nouvelles règles.
2.
3.
4.
5.
6.
7.
8.
import
re
def
build_match_and_apply_functions
(
pattern, search, replace):
def
matches_rule
(
word): # (1)
return
re.search
(
pattern, word)
def
apply_rule
(
word): # (2)
return
re.sub
(
search, replace, word)
return
(
matches_rule, apply_rule) # (3)
- build_match_and_apply_functions() est une fonction qui crée d'autres fonctions de façon dynamique. Elle prend en paramètres pattern, search et replace puis définit une fonction matches_rule() qui fait appel à re.search() avec le motif (pattern) qui a été précisé en paramètre de la fonction build_match_and_apply_functions(), et avec le mot (word) qui a été passé à la fonction matches_rule() que l'on est en train de créer. Whaou.
- Construire la fonction d'application fonctionne de la même façon. La fonction d'application est une fonction qui prend un paramètre et appelle re.sub() avec les paramètres search et replace qui ont été passés à la fonction build_match_and_apply_functions() et le mot (word) qui a été passé à la fonction apply_rule() que l'on construit. Cette technique utilisant les valeurs de paramètres extérieurs à l'intérieur d'une fonction s'appelle une fermeture (closure). On définit essentiellement des constantes à l'intérieur de la fonction d'application que l'on construit : cela nécessite un paramètre (word), mais ensuite la fonction prend en compte deux autres valeurs (search et replace) qui ont été définies à la création de la fonction d'application.
- Finalement, la fonction build_match_and_apply_functions() renvoie un tuple de deux valeurs : les deux fonctions que l'on vient de créer. Les constantes définies dans ces fonctions (pattern dans la fonction matches_rule(), search et replace dans la fonction apply_rule()) restent liées à ces fonctions, même après le retour de build_match_and_apply_functions(). C'est super cool.
Si tout ceci est incroyablement confus (et avec tous ces trucs bizarres, ça devrait l'être), tout s'éclaircira quand vous verrez comment l'utiliser.
2.
3.
4.
5.
6.
7.
8.
9.
Patterns =
\ # (1)
(
(
'[sxz]$'
, '$'
, 'es'
),
(
'[^aeioudgkprt]h$'
, '$'
, 'es'
),
(
'(qu|[^aeiou])y$'
, 'y$'
, 'ies'
),
(
'$'
, '$'
, 's'
) # (2)
)
rules =
[build_match_and_apply_functions
(
pattern, search, replace) #(3)
for
(
pattern, search, replace) in
patterns]
- Nos règles de pluralisation sont maintenant définies comme un tuple de tuples de chaînes de caractères (pas des fonctions). La première chaîne de caractères de chaque groupe est le motif d'expression régulière que l'on utiliserait dans re.search() pour vérifier si cette règle correspond. La deuxième et la troisième chaîne de caractères de chaque groupe sont les paramètres search et replace que l'on utiliserait dans re.sub() afin d'appliquer la règle qui modifie le nom en son pluriel.
- Il y a une légère différence ici, dans la règle par défaut. Dans l'exemple précédent, la fonction match_default() renvoyait simplement True, cela signifie que si aucune des règles ne correspond, le code ajouterait simplement un s à la fin du mot donné. Cet exemple fait quelque chose d'équivalent. L'expression régulière finale demande si le mot a une fin ($ correspond à la fin d'une chaîne de caractères). Évidemment, chaque chaîne de caractères a une fin, même une chaîne de caractères vide, cette expression correspond donc toujours. En fait, c'est le même principe que la fonction match_default() qui renvoyait toujours True : cela assure que si aucune autre règle ne correspond, le code ajoute un s à la fin du mot donné.
- Cette ligne est magique. On prend une séquence de chaînes de caractères dans patterns et on les transforme en une séquence de fonctions. Comment ? En appliquant les chaînes de caractères à la fonction build_match_and_apply_functions(). Ce qui signifie qu'on prend chaque triplet de chaînes de caractères et qu'on appelle la fonction build_match_and_apply_functions() avec ces trois chaînes de caractères comme arguments. La fonction build_match_and_apply_functions() renvoie un tuple de deux fonctions. Cela signifie qu'en fin de compte, rules se comporte comme dans l'exemple précédent : une liste de tuples, dans laquelle chaque tuple est une paire de fonctions. La première fonction est la fonction de correspondance qui fait appel à re.search() et la seconde fonction est la fonction d'application qui appelle re.sub().
Pour compléter cette version de script, il nous faut le point d'entrée principal, la fonction plural().
2.
3.
4.
def
plural
(
noun):
for
matches_rule, apply_rule in
rules: # (1)
if
matches_rule
(
noun):
return
apply_rule
(
noun)
- Comme la liste rules est la même que dans l'exemple précédent (vraiment, ça l'est), le fait que la fonction plural() ne change pas du tout ne devrait pas vous surprendre. C'est complètement générique ; on prend une liste de « fonctions règles » et on les appelle dans l'ordre. Le processus se moque de comment les règles ont été définies. Dans l'exemple précédent, les règles étaient définies comme fonctions nommées séparément. Elles sont maintenant créées dynamiquement en appliquant la sortie de la fonction build_match_and_apply_functions() sur une liste de chaînes de caractères. Cela n'a pas d'importance ; la fonction plural() fonctionne toujours de la même façon.
V. Un fichier de motifs▲
Nous avons simplifié tout le code et ajouté suffisamment d'abstraction pour que les règles de pluralisation soient définies en une liste de chaînes de caractères. La suite logique est de prendre ces chaînes de caractères et de les mettre dans un fichier séparé, que l'on peut mettre à jour séparément du code qui les utilise.
Tout d'abord, nous allons créer un fichier texte qui contient les règles que l'on souhaite. Pas de structures de données fantaisistes, simplement trois chaînes de caractères délimitées par des espaces. Nous l'appellerons plural4-rules.txt.
[sxz]$ $ es
[^aeioudgkprt]h$ $ es
[^aeiou]y$ y$ ies
$ $ s
Voyons maintenant comment nous pouvons utiliser ce fichier de règles.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
import
re
def
build_match_and_apply_functions
(
pattern, search, replace): # (1)
def
matches_rule
(
word):
return
re.search
(
pattern, word)
def
apply_rule
(
word):
return
re.sub
(
search, replace, word)
return
(
matches_rule, apply_rule)
rules =
[]
with
open(
'plural4-rules.txt'
, encoding=
'utf-8'
) as
pattern_file: # (2)
for
line in
pattern_file: # (3)
pattern, search, replace =
line.split
(
None
, 3
) # (4)
rules.append
(
build_match_and_apply_functions
(
# (5)
pattern, search, replace))
- La fonction build_match_and_apply_functions() n'a pas changé. On utilise toujours les fermetures pour créer deux fonctions dynamiquement qui utilisent des variables définies dans la fonction principale.
- La fonction globale open() ouvre un fichier et renvoie un objet fichier. Dans notre cas, le fichier que nous ouvrons contient les chaînes de caractères des motifs pour la pluralisation des noms. L'instruction with crée ce que l'on appelle un contexte : quand le bloc with se termine, Python fermera automatiquement le fichier, même si une exception est survenue à l'intérieur du bloc with. Nous nous intéresserons plus attentivement aux blocs with et aux fichiers objets dans le chapitre Fichiers.
- L'instruction
for
linein
<
fileobject>
lit les données du fichier ouvert, une ligne à la fois, et affecte le texte à la variable line. Nous nous intéresserons plus attentivement à la lecture à partir de fichiers dans le chapitre Fichiers. - Chaque ligne du fichier a trois valeurs, mais celles-ci sont séparées par des espaces (tabulations ou espaces, ça ne fait pas de différence). Pour les découper, nous utilisons la méthode de chaîne de caractères split(). Le premier argument de la méthode split() est None, ce qui signifie « séparer à chaque espace (tabulation ou espace, ça ne fait pas de différence) ». Le second argument est 3, ce qui signifie « séparer les espaces trois fois, ensuite laisser le reste de la ligne intact ». Une ligne comme [sxz]$ $ es sera divisée en liste ['[sxz]$', '$', 'es'], ce qui signifie qu'on assignera '[sxz]$' à pattern, '$' à search et 'es' à replace. C'est beaucoup de puissance en une petite ligne de code.
- Finalement, on passe pattern, search et replace à la fonction build_match_and_apply_functions(), celle-ci renvoie un tuple de fonctions. On ajoute ce tuple à la liste rules et rules finit par stocker la liste de fonctions de correspondance et d'application que la fonction plural() attend.
L'amélioration ici est que nous avons complètement séparé les règles de pluralisation dans un fichier externe, ainsi ces règles sont maintenues séparément du code qui les utilise. Le code c'est le code, les données c'est les données, et la vie est belle.
VI. Les générateurs▲
Ne serait-il pas magnifique d'avoir une fonction générique plural() qui analyse le fichier de règles ? Obtenir les règles, vérifier la correspondance, appliquer la transformation appropriée, aller à la règle suivante. C'est tout ce que la fonction plural() doit faire et c'est tout ce qu'elle devrait faire.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
def
rules
(
rules_filename):
with
open(
rules_filename, encoding=
'utf-8'
) as
pattern_file:
for
line in
pattern_file:
pattern, search, replace =
line.split
(
None
, 3
)
yield
build_match_and_apply_functions
(
pattern, search, replace)
def
plural
(
noun, rules_filename=
'plural5-rules.txt'
):
for
matches_rule, apply_rule in
rules
(
rules_filename):
if
matches_rule
(
noun):
return
apply_rule
(
noun)
raise
ValueError
(
'no matching rule for {0}'
.format
(
noun))
Mais comment cela fonctionne-t-il ? Observons tout d'abord un exemple en mode interactif.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
>>>
def
make_counter
(
x):
... print
(
'entering make_counter'
)
... while
True
:
... yield
x # (1)
... print
(
'incrementing x'
)
... x =
x +
1
...
>>>
counter =
make_counter
(
2
) # (2)
>>>
counter # (3)
<
generator object at 0x001C9C10
>
>>>
next
(
counter) # (4)
entering make_counter
2
>>>
next
(
counter) # (5)
incrementing x
3
>>>
next
(
counter) # (6)
incrementing x
4
- La présence du mot clé
yield
dans make_counter signifie que ce n'est pas une fonction normale. C'est un genre spécial de fonction qui génère des valeurs, une à la fois. Imaginez ça comme une fonction reprenant son exécution là où elle était arrivée la fois précédente. Faire appel à cette fonction renverra un générateur qui peut être utilisé pour générer des valeurs successives de x. - Pour créer une instance du générateur make_counter, on l'appelle comme n'importe quelle autre fonction. Notons que cela n'exécute pas le code de la fonction. On peut le voir, car la première ligne de la fonction make_counter() appelle la fonction print(), mais rien n'a encore été affiché.
- La fonction make_counter() renvoie un objet générateur.
- La fonction next() prend comme argument un objet générateur et renvoie la valeur suivante. La première fois qu'on appelle la fonction next() avec le générateur counter, cela exécute le code jusqu'à l'instruction
yield
et renvoie ensuite la valeur produite paryield
. Dans notre cas, ce sera 2, car nous avons créé le générateur en appelant make_counter(2). - Quand on appelle next() à plusieurs reprises avec le même objet générateur, la fonction reprend exactement là où elle s'était arrêtée et continue jusqu'à la prochaine instruction
yield
. Toutes les variables, l'état local, etc. sont sauvegardés lors duyield
et rétablis dans next(). La prochaine ligne de code attendant d'être exécutée fait appel à print(), qui affiche incrementing x. Après cela, l'instruction x = x + 1 incrémente x. Ensuite, on repasse dans la boucle while et la première chose que l'on rencontre est l'élémentyield
x, qui sauvegarde l'état de tout et renvoie la valeur actuelle de x (qui vaut 3 maintenant). - La seconde fois qu'on appelle next(counter), on refait les mêmes choses sauf que cette fois, x vaut maintenant 4.
Comme make_counter crée une boucle infinie, on pourrait théoriquement le faire indéfiniment et ça ne ferait qu'incrémenter x et afficher des valeurs. Mais au lieu de ça, observons des utilisations plus productives des générateurs.
VI-A. Un générateur de nombres de Fibonacci▲
yield met une fonction en pause.
next() reprend là où elle s'était arrêtée.
- La suite de Fibonacci est une suite de nombres dans laquelle chaque nombre est la somme des deux nombres le précédant. Elle commence par 0 et 1, croît doucement au début puis de plus en plus rapidement. Pour commencer la séquence, nous avons besoin de deux variables : a commence à 0 et b commence à 1.
- a est le nombre actuel dans la séquence ; donc
yield
le renvoie ; - b est le nombre suivant de la séquence, on lui affecte donc a, mais on calcule aussi la valeur suivante (a + b) que l'on affecte à b pour une utilisation ultérieure. Notons que cela se passe en parallèle ; si a vaut 3 et b vaut 5, alors, à l'étape suivante, a prendra la valeur 5 (la valeur précédente de b) et b la valeur 8 (la somme des valeurs précédentes de a et b).
Nous avons donc une fonction qui affiche successivement des nombres de Fibonacci. On pourrait bien sûr le faire avec la récursivité, mais c'est plus facile de le lire de cette façon. Cela fonctionne bien aussi avec des boucles for.
2.
3.
4.
5.
6.
>>>
from
fibonacci import
fib
>>>
for
n in
fib
(
1000
): # (1)
... print
(
n, end=
' '
) # (2)
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
>>>
list(
fib
(
1000
)) # (3)
[0
, 1
, 1
, 2
, 3
, 5
, 8
, 13
, 21
, 34
, 55
, 89
, 144
, 233
, 377
, 610
, 987
]
- On peut utiliser un générateur comme fib() directement dans une boucle for. La boucle for appellera automatiquement la fonction next() afin d'obtenir les valeurs du générateur fib() et leur affectera la variable d'index (n) de la boucle for.
- À chaque itération, n reçoit une nouvelle valeur de l'instruction
yield
dans fib() et tout ce que vous avez à faire, c'est l'afficher. Une fois que fib() ne génère plus de nombres (a devient plus grand que max, qui vaut 1000 dans notre cas), on sort de la boucle for. - Il s'agit d'une construction courante très utile : on passe un générateur à la fonction list() et celui-ci va itérer sur toutes les valeurs produites par le générateur (tout comme la boucle for dans l'exemple précédent) et renverra une liste de toutes les valeurs.
VI-B. Un générateur à règles de pluralisation▲
Revenons à plural5.py et voyons comment cette version de la fonction plural() fonctionne.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
def
rules
(
rules_filename):
with
open(
rules_filename, encoding=
'utf-8'
) as
pattern_file:
for
line in
pattern_file:
pattern, search, replace =
line.split
(
None
, 3
) # (1)
yield
build_match_and_apply_functions
(
pattern, search, replace) # (2)
def
plural
(
noun, rules_filename=
'plural5-rules.txt'
):
for
matches_rule, apply_rule in
rules
(
rules_filename): # (3)
if
matches_rule
(
noun):
return
apply_rule
(
noun)
raise
ValueError
(
'no matching rule for {0}'
.format
(
noun))
- Pas de magie ici. Souvenez-vous que les lignes du fichier de règles ont trois valeurs séparées par un espace. On utilise line.split(None, 3) pour obtenir ces 3 colonnes et on les affecte à trois variables locales.
- Ensuite, on appelle
yield
. Qu'est-ce queyield
nous donne ? Deux fonctions, créées dynamiquement avec notre vieil ami, build_match_and_apply_functions(), qui est identique aux précédents exemples. Autrement dit, rules() est un générateur qui renvoie des fonctions de correspondance et d'application à la demande. - Comme rules() est un générateur, on peut l'utiliser directement dans une boucle for. À la première itération, on appellera la fonction rules() qui ouvrira le fichier de motifs, lira la première ligne, créera dynamiquement une fonction de correspondance et une fonction d'application à partir des motifs de cette ligne, et
yield
renverra les fonctions créées dynamiquement. À la seconde itération, on reprendra exactement où on en était dans rules() (c'est-à-dire au milieu de la boucle for line in pattern_file). La première chose que cela fera, c'est lire la prochaine ligne du fichier (qui est toujours ouvert), créer dynamiquement dans le fichier une autre paire de fonctions de correspondance et d'application basée sur les motifs de cette ligne etyield
renvoie les deux fonctions.
VII. Remerciements▲
Nous tenons à remercier Flo71 pour la traduction, Lolo78 pour la relecture technique et la mise au gabarit et Claude Lelouppour la relecture orthographique.