Ce billet est le troisième d'une série en quatre volets. Voir le volet 1, le volet 2 ou le volet 4.
La PEP 342 améliore les générateurs simples afin de pouvoir les utiliser comme des coroutines.
Coroutines
Pour commencer, essayons de comprendre le sens du mot coroutine. Et autant vous dire qu'il est difficile de trouver un consensus.
Voici la définition très conceptuelle qu'en donne Donald Knuth qui tend à considérer que tout est coroutine :
Subroutines are special cases of more general program components, called “coroutines.” In contrast to the unsymmetric relationship between a main routine and a subroutine, there is complete symmetry between coroutines, which call on each other.
To understand the coroutine concept, let us consider another way of thinking about subroutines. The viewpoint adopted in the previous section was that a subroutine merely was an extension of the computer hardware, introduced to save lines of coding. This may be true, but another point of view is possible: We may consider the main porogram and the subroutine as a team of programs, with each member of the team having a certain job to do. The main program, in the course of doing its job, will activate the subprogram; the subprogram performs its own function and then activates the main program. We might stretch our imagination to believe that, from the subroutine's point of view, when it exits it is calling the main routine; the main routine continues to perform its duty, then “exits” to the subroutine. The subroutine acts, then calls the main routine again.
This somewhat far-fetched philosophy actually takes place with coroutines, when it is impossible to distinguish which is a subroutine of the other. Suppose we have coroutines
A
andB
; when programmingA
, we may think ofB
as our subroutine, but when programmingB
, we may think ofA
as our subroutine. … It represents teamwork as in a relay race. Whenever a coroutine is activated, it resumes execution of its program at the point where the action was last suspended.
Sinon en plus succinct :
Coroutines are nothing more than routines that communicate via pipes!
Pour A. Jesse Jiryu Davis et Guido van Rossum :
The concept of a coroutine, dating to the elder days of computer science, is simple: it is a subroutine that can be paused and resumed.
Et comme j'aime les citations, je vous en colle encore une :
The difference between coroutines and subroutines is conceptual: subroutines slot into an overarching function to which they are subordinate, whereas coroutines are all colleagues, they cooperate to form a pipeline without any supervising function responsible for calling them in a particular order.
La PEP 342 décrit les coroutines comme une façon naturelle d'exprimer certains algorithmes dont les jeux, les entrées/sorties asynchrones ou la programmation évènementielle : en permettant de passer des valeurs ou des exceptions aux générateurs à l'endroit où ils sont suspendus, un simple ordonnanceur de coroutines ou une fonction trampoline laisserait les coroutines s'appeler les unes les autres sans blocage.
Ce sont ces améliorations que cette PEP apporte aux générateurs simples.
Générateurs améliorés
Les changements suivants ont été apportés :
yield
peut être utilisé comme une expression (produisant une valeur) plutôt qu'une instructionajout d'une méthode
send()
aux générateur-itérateurs afin de pouvoir leur transmettre des données qui deviennent le résultat de l'expressionyield
ajout d'une méthode
throw()
aux générateur-itérateurs qui permet d'envoyer des exceptions à la fonction génératrice en la forçant à lever une exception au prochainyield
ajout d'une méthode
close()
aux générateur-itérateurs qui force la fonction génératrice à s'arrêter prématurément en levant une exceptionGeneratorExit
close()
est appelée quand un générateur-itérateur passe au ramasse-miettesyield
peut être utilisé dans les blocstry/finally
afin de permettre à une coroutine en échec de faire le ménage, la clausefinally
étant exécutée suite à un appel explicite à la méthodeclose()
par exemple
Pour communiquer avec une coroutine il faut d'abord appeler la méthode .send(None)
(ou __next__()
) d'un générateur-itérateur une première fois de manière à faire avancer l'exécution de la fonction génératrice jusqu'à sa première expression yield
. Ensuite on peut commencer à utiliser .send()
avec un argument autre que None
.
Ce comportement est à l'origine des décorateurs dont la seule fonction est de faire avancer une fonction génératrice jusqu'au premier yield
qui nécessite une valeur autre que None
.
Produire, filtrer et consommer
Ces changements font passer les générateurs d'un statut de producteurs à sens unique vers un statut leur permettant d'être à la fois des producteurs et des consommateurs de données.
Il devient possible de construire des tubes avec des coroutines ayant différents rôles en fonction de leur utilisation de yield
et send()
:
- un producteur crée une série de données et utilise
send()
, mais pas(yield)
- un filtre utilise à la fois
(yield)
pour consommer etsend()
pour envoyer des résultats à la prochaine étape - un consommateur utilise
(yield)
pour consommer, mais n'envoie rien
Cette technique est largement utilisée par David Beazley dans sa présentation Generator Tricks for Systems Programmers.
Ordonnanceur de coroutines ou fonction trampoline
Il devient également possible d'utiliser les coroutines comme une alternative aux fils d'exécution pour implémenter des traitements concurrentiels. On parle parfois de green threads.
En partant du fait que yield
peut suspendre l'exécution d'un générateur, il devient "facile" d'écrire une fonction trampoline ou un ordonnanceur qui traite les générateurs comme des tâches et alterne leurs exécutions.
yield from
Il s'agit d'une syntaxe détaillée dans la PEP 380 permettant à un générateur de déléguer une partie de ses opérations et de récupérer le résultat du travail. Cela permet la factorisation d'une section de code contenant yield
en la plaçant dans un autre générateur.
Pour ce faire l'expression yield from <expr>
est autorisée dans le corps d'un générateur où <expr>
est une expression évaluée comme un iterable
, duquel un iterator
est extrait : on parle de sous-itérateur. Il peut prendre la forme d'un générateur ou bien d'un itérateur arbitraire (range
etc.).
Toute valeur produite par le sous-itérateur est passée directement à l'appelant. Dans l'autre sens, les envois avec send()
ou throw()
sont passés au sous-itérateur si ce dernier dispose de la méthode adéquate, sinon une exception est levée. En résumé, il s'agit d'un canal bidirectionnel transparent depuis l'appelant vers le sous-itérateur.
Les sous-itérateurs sont autorisés à retourner une valeur via return
qui devient le résultat de yield from
dans le générateur qui délègue.
Ressources
- Passing values into a generator — Python Functional Programming HOWTO
test_generators.py
— Core Python generators test file- Iterables, Iterators and Generators: Part 2 — Ian Ward
- A Curious Course on Coroutines and Concurrency — David Beazley
- Generators: The Final Frontier — David Beazley
- Python 3: Using "yield from" in Generators - Part 1
- Using sub-generators for lexical scanning in Python