Coroutines via générateurs améliorés en Python

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

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 and B; when programming A, we may think of B as our subroutine, but when programming B, we may think of A 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 :

  1. yield peut être utilisé comme une expression (produisant une valeur) plutôt qu'une instruction

  2. ajout 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'expression yield

  3. 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 prochain yield

  4. 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 exception GeneratorExit

  5. close() est appelée quand un générateur-itérateur passe au ramasse-miettes

  6. yield peut être utilisé dans les blocs try/finally afin de permettre à une coroutine en échec de faire le ménage, la clause finally étant exécutée suite à un appel explicite à la méthode close() 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 et send() 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

Avant Générateurs en Python Après Coroutines avec asyncio en Python

Tag Kemar Joint