Thématiques principales

lundi 11 mars 2019

Python : Les Coroutines

Je vous avais promis des aspects technologiques sur python et l’utilisation de quelques librairies intéressantes, et bien c’est loupé, initialement je voulais voir les coroutines en même temps que le décorateurs [decorateur] , mais finalement je les aies séparés. Donc nous revoilà avec un autre article sur cette petite spécificité du langage [pep-342].

Vous vous souvenez du mots clefs yield? C’est celui la que nous utilisions dans les générateurs pour produire des résultats sans interrompre le déroulement du contexte d'exécution… c'était assez magique… à bien sachez qu’il est aussi possible de lui envoyer des données en contre partie…. dur à imaginer…. effectivement…Prenons un exemple:


import unittest 
import re

def producteur(file):
    print("PROD: begin producer")
    with open(file,'r') as lfile:
        continu=True
        print("PROD: ouverture du fichier {0} avec boucle init en {1}".format(file,continu))
        while(continu):
            print("PROD: on est dans la boucle")
            chaine=lfile.readline()
            print("PROD: dans la boucle, la chaine lu est {0}".format(chaine))
            continu= (yield chaine)
            print("PROD: on nous repond si on peut continuer {0}".format(continu))
        print("PROD: on est sorti de la boucle et on se met en attente de cloture")
        yield


class TestCoroutine(unittest.TestCase):

    def testCouroutineSimpel(self):
        count=3
        gen=producteur('cor.txt')
        #help(gen)
        print("TEST: on initialise le contexte du test avec un nombre de recherche limité a {0} et un generateur {1}".format(count,gen))
        chaine=gen.__next__()
        while(True):
            print("TEST: la chaine vaut {0}, le nombre d'element a reconnaitre {1}".format(chaine,count))
            if(re.search(r".*mas",chaine) is not None) :
                print("TEST: on a reconnu un element on va decrementer")
                count-=1
                chaine=gen.send(True)
            if(count==0):
                print("TEST: on a suffisament decrementé, on envoye un message pour mettre fin a l'iteration ")
                gen.send(False)
                break
        print("TEST: on cloture le generateur")
        gen.close()
        self.assertEqual(0,count)


if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)


Cet exemple est simpliste mais il illustre bien le mécanisme mis en oeuvre et le principe de la coroutine dans laquelle au travers de la méthode send, il va être possible d’injecter des données à la fonction lui permettant de conserver son contexte d'exécution mais d'être adapté à la circonstance (résultant de l’execution du renvoie de la valeur par le yield.)

En fait il s’agit ici d’un sorte d’artifice permettant d'éviter de découper une fonction en deux car l’un de ses paramètres est dépendant d’un résultat externe qui devrait être calculé dans l’entre temps.


TEST: on initialise le contexte du test avec un nombre de recherche limité a 3 et un generateur <generator object producteur at 0x03030E70>
PROD: begin producer
PROD: ouverture du fichier cor.txt avec boucle init en True
PROD: on est dans la boucle
PROD: dans la boucle, la chaine lu est thomas1

TEST: la chaine vaut thomas1
, le nombre d'element a reconnaitre 3
TEST: on a reconnu un element on va decrementer
PROD: on nous repond si on peut continuer True
PROD: on est dans la boucle
PROD: dans la boucle, la chaine lu est thomas2

TEST: la chaine vaut thomas2
, le nombre d'element a reconnaitre 2
TEST: on a reconnu un element on va decrementer
PROD: on nous repond si on peut continuer True
PROD: on est dans la boucle
PROD: dans la boucle, la chaine lu est thomas3

TEST: la chaine vaut thomas3
, le nombre d'element a reconnaitre 1
TEST: on a reconnu un element on va decrementer
PROD: on nous repond si on peut continuer True
PROD: on est dans la boucle
PROD: dans la boucle, la chaine lu est thomas4

TEST: on a suffisament decrementé, on envoye un message pour mettre fin a l'iteration 
PROD: on nous repond si on peut continuer False
PROD: on est sorti de la boucle et on se met en attente de cloture
TEST: on cloture le generateur

----------------------------------------------------------------------
Ran 1 test in 0.009s

OK


Ainsi dans l’exemple, le générateur va s'exécuter au fil des sollicitations produite dans le test. On initialise le générateur, celui ci va s'exécuter jusqu’au premier yield sans l'exécuter. C’est l’instruction __init__ qui va alors demander au générateur de renvoyer le résultat obtenu sans pour autant avancer dans l'exécution, pour cela, il va attendre un send qui lui permettra d’assigner la valeur à la variable continu et la il va poursuivre son exécution jusqu’à boucler jusqu’au yield suivant. La il donnera alors la main à la boucle principale qui va elle aussi boucler jusqu’au send suivant.

La boucle est bouclé, il reste alors à prévoir un mécanisme de sortie en comptant ici le nombre de fois que l’on rencontre une chaîne de caractères. Alors à ce moment la on arrête la boucle principale via un break ainsi que la boucle du générateur. On n’oubliera pas alors de clôturer complètement le générateur via la méthode close.

Ici on a le détail du fonctionnement du générateur si il est nécessaire de lui passer des paramètres au cours de son exécution afin qu’il puisse s’adapter dynamiquement. Bien sur dans le cas contraire l’exemple fourni dans l’article [generateur] sera préférable.

Accessoirement, vous aurez noté l'utilisation des librairies unittest et re pour réaliser cet article. L'idée n'est pas de rentrer pas dans le détail de ces librairies aujourd'hui puisqu'elles serviront a l'avenir très souvent et que nous les parleront mais pour en donner deux mots, la première est une librairie permettant la mise en oeuvre de tests unitaires en python, c'est donc un indispensable et la second est une librairie permettant la gestion d'expression régulière et leur reconnaissance/extraction dans des données. (accessoirement on a aussi vu une approche pour accéder a des fichiers aussi).

Enfin bref, pour conclure, les coroutines ne sont pas triviales a manipuler mais elles permettent de simuler des exécutions concurrentes assez efficacement. Pour aller plus loin, je vous invite a consulter la référence sur le sujet [pep-342]

Références

[pep-342] https://www.python.org/dev/peps/pep-0342/
[generateur] https://un-est-tout-et-tout-est-un.blogspot.com/2019/03/python-listes-et-generateurs.html
[decorateur] https://un-est-tout-et-tout-est-un.blogspot.com/2019/03/python-les-decorateurs.html
[uniitest] https://medium.com/@vladbezden/using-python-unittest-in-ipython-or-jupyter-732448724e31

Aucun commentaire:

Enregistrer un commentaire