Certains l’auront noté, les tampons sont de retour.
J’ai aussi, à la demande d’un lecteur, ajouté un plugin pour permettre aux commentaires d’être écrits en markdown (avec les extensions de github).
Pour obtenir la parité avec le précédent blog, il nous faudra donc restaurer encore quelques easter eggs et améliorer les images sur la page d’accueil, mais c’est presque fini. Comme quoi, une migration, c’est long. Et chiant. Et ingrat.
Bon, j’ai pas fini mon slideshow sur WAMP, donc je vais faire un article sur ça.
iPython notebook, c’est pratique, mais une seule personne peut coder à la fois. Si quelqu’un veut voir les changements, il faut recharger avec F5. A l’heure des Google Doc temps réel, c’est con. Surtout que c’est du ZMQ derrière.
Du coup je me suis lancé dans une expérimentation avec TogetherJS, la lib de Momo pour le travail collaboratif.
Pour lancer le truc, il suffit d’aller dans son dossier de profile iPython et éditer le fichier Javascript vierge fait pour étendre l’outil. Par exemple, sous Ubuntu, mon profile par défaut est dans ~/ipython/profile_default et le fichier à éditer est ./profile_default/static/custom/custom.js.
Et on est bon. Il faut ouvrir deux browsers différents pour bien tester le truc, par exemple un chrominou et un firefoune. Moi j’ai ouvert deux FF parce que j’ai pleins de profiles dessus.
La frappe est bien synchro, mais malheureusement pas l’affichage du résultat. Y a donc bien un truc intéressant à creuser, mais pour le moment, c’est pas utilisable.
Ensuite, il y a des services Web. SageMathCloud annonce faire cela, mais impossible de s’inscrire, le formulaire ne marche pas. Codebox propose un env complet de dev en ligne, mais l’inscription est temporairement suspendue. Codebunk marche pas mal, est plutôt bien foutu et clair, mais Python 2.7 uniquement, et aucun moyen d’installer une lib, donc adieu scipy ou matplotlib.
Celui qui s’en rapproche le plus est Pythonanywhere : des consoles synchronisées pour toutes les versions de Python, beaucoup de libs préinstallées (dont scipy et matplotlib), un vrai système de fichier sur lequel travailler, du pip a dispo. Et en prime un éditeur de texte pour des fichiers complets, mais qui lui, n’est pas collaboratif. Damn it. Et impossible d’afficher en ligne une image pondue via matplotlib. En plus, on a pas le concept des cellules d’iPython, qui est super pratique.
Bref, il manque vraiment un outil pour faire ce genre de choses.
Depuis quelques jours je suis en discussion avec Tobias de Tavendo. Comme vous avez pu le remarquer avec mes précédents articles sur WAMP et Crossbar :
Ils sont bons techniquement, et nuls pour expliquer ce qu’ils ont techniqué.
Cette techno est une techno de rêve pour moi. J’y crois à mort.
Je suis le seul à avoir pondu des explications décentes sur WAMP et Crossbar. Et ça n’a pas suffit à faire battre un cil.
Bref, ils ont embauché des mecs de haute voltige pour la technique (du genre un contributeur PyPy). Et ils m’ont contacté pour me demander si je n’étais pas chaud pour faire de l’évangélisme, rémunéré, autour de WAMP, Autobahn et Crossbar.
L’idée : écrire des tutos, des articles, améliorer la doc, répondre sur le chan IRC, etc.
J’adore le concept, vu que j’aime leur projet et que je le faisais gratos avant, surtout qu’ils sont pas trop contraignants sur le temps que je vais passer dessus.
Donc voilà le deal : quand je vais pondre des tutos et des articles sur WAMP et Co, je vais d’abord les faire en français ici. Comme ça j’aurai les retours des lecteurs du blog qui pourront, comme d’habitude, me faire part de leurs douces remarques sur à quel point on ne pige rien.
Une fois la prose aiguisée, je traduis et je publie chez Tavendo.
Je disclose donc ici que vous verrez peut-être des prochaines rédactions qui seront attachées à une activité pro. Pas impartial donc. Mais bon, depuis quand je suis impartial ? Javascript c’est de la merde, et je préfère les rousses.
Par saucisse d’honnêteté, je signalerai chaque choucroute concernée avec un lien vers ce post.
Enfin, le contrat est pas signé encore, mais vu que je vais commencer à taffer dessus aujourd’hui, je pense à une première publication demain sous la forme d’un slide show expliquant avec de jolies diapos ce que sont WAMP, Autobahn et Crossbar. À quoi ça sert et ce qu’on peut faire avec.
Du coup, je pense qu’il est plus judicieux de se poser la question inverse : quand ne pas utiliser Python ?
Pour le moment, j’évite d’utiliser Python dans les cas suivants :
L’équipe avec laquelle je travaille est bien meilleure dans un autre langage. Il faut profiter des compétences de chacun au maximum plutôt que privilégier une technologie, c’est plus productif.
Je reprends du code d’un projet solide, dans un autre langage. Inutile de réécrire quelque chose qui est propre et qui fonctionne.
Je travaille dans un environnement où d’autres technologies sont mieux intégrées. Par exemple, dans une entreprise avec une forte culture Java, faire les choses dans son coin ne va pas permettre un bon travail d’équipe. Travailler avec une boîte qui a des serveurs qu’ils ne peuvent pas changer et d’autres technos ? Inutile de forcer la main.
Des technologies plus productives existent pour ce domaine : c’est le cas de l’embarqué, ou des jeux vidéos. Ça ne veut pas dire qu’on ne peut pas en faire en Python. En fait, il y a un fort travail de la communauté pour améliorer cela en ce moment même. Mais difficile de combattre les outils en C/C++ dans ces domaines.
Je suis pressé, et la solution de facto n’est pas en Python. Par exemple, je veux faire du Web temps réel. Si j’ai le temps, je ferai du Python, car ce n’est pas grave si il me faut aiguiser moi-même quelques outils pour obtenir le résultat désiré. Si par contre j’ai 2 semaines devant moi, je prendrais NodeJS, malgré mon dégoût pour le Javascript, car je suis pragmatique.
J’ai mesuré mes besoins objectivement, et est évalué que Python ne serait pas assez performant malgré toute les mesures d’optimisation que je sais mettre en œuvre. Dans ce cas, on se tourne vers des solutions plus lourdes mais nativement plus rapides : C, GO, Erlang etc.
Je vise une plateforme en particulier (Windows, iOS…), je n’ai pas besoin de porter mon code, et je sais qu’utiliser les outils de la boîte qui fait l’OS me donnera de meilleurs résultats. Par exemple si je vise Android uniquement, utiliser leur SDK donnera une app plus fluide et une UI mieux intégrée.
Dans tous les autres cas, Python est un choix excellent.
Si vous avez aimé les générateurs, vous avez du creuser un peu yield et vous apercevoir qu’on pouvait créer des coroutines avec. Mais sans vraiment comprendre ce que ça faisait.
On va se faire une petit intro. C’est un sujet vraiment avancé, donc si vous avez autre chose de moins compliqué à comprendre en Python (n’importe quoi à part les métaclasses :)), ne vous prenez pas la tête sur cet article. Ecoutez juste la musique :
D’abord, rappel sur le fonctionnement des générateurs (qui sont un prérequis de l’article, donc si besoin, relisez le tuto dédié) :
def soleil():
print('Premier next()')print('Yield 1')yield1print('Deuxième next()')print('Yield 2')yield2print('Troisième next()')print('Yield 3')yield3# pas de quatrième next(),# donc on ne passe jamais iciprint('Pas vu')# rappel, ceci ne déclenche pas le code # de soleil() puisqu'il y a yield dedansprint("Creation du generateur")
undeuxtrois = soleil()# On execute le code jusqu'au yield 1
res = next(undeuxtrois)print('res = %s' % res)# On execute le code jusqu'au yield 2
res = next(undeuxtrois)print('res = %s' % res)# On execute le code jusqu'au yield 3
res = next(undeuxtrois)print('res = %s' % res)print('Good bye')## Premier next()## Yield 1## res = 1## Deuxième next()## Yield 2## res = 2## Troisième next()## Yield 3## res = 3## Good bye
Chaque fois qu’on appelle next() sur le générateur, il va exécuter le code jusqu’au prochain yield, et retourner la valeur de celui-ci, puis mettre le générateur en pause.
On peut assigner le résultat d’un yield, mais si on fait des next(), on obtient toujours None :
def lune():
print('Premier next()')print('Yield 1')
x =(yield1)print('Deuxième next()')print('Avant le yield 2, x = %s' % x)print('Yield 2')
x =(yield2)print('Troisième next()')print('Avant le yield 3, x = %s' % x)print('Yield 3')
x =(yield3)print('Pas vu')print("Creation du generateur")
generateur = lune()
res = next(generateur)print('res = %s' % res)
res = next(generateur)print('res = %s' % res)
res = next(generateur)print('res = %s' % res)print('Good bye')## Creation du generateur## Premier next()## Yield 1## res = 1## Deuxième next()## Avant le yield 2, x = None## Yield 2## res = 2## Troisième next()## Avant le yield 3, x = None## Yield 3## res = 3## Good bye
La raison est que cette valeur doit venir de l’extérieur. Pour la fournir, il faut utiliser la méthode send() et non la fonction next().
Mais elle ne fonctionne pas du tout pareil. En fait, si on l’appelle cash pistache, ça plante :
print("Creation du generateur")
generateur = lune()
res = generateur.send("A")print('res = %s' % res)## Creation du generateur## Traceback (most recent call last):## File "test.py", line 24, in ## res = generateur.send("A")## TypeError: can't send non-None value to a just-started generator
C’est parce que, contrairement à next() qui va jusqu’au prochain yield, send() PART du dernier yield atteint pour aller au suivant.
Il faut donc d’abord arriver à un premier yield avant de faire un send(). On peut le faire en utilisant au moins un next().
Voici donc notre nouveau code :
def lune():
print('On fait au moins un next()')print('Yield 1')
x =(yield1)print('Premier send(), x = %s' % x)print('Yield 2')
x =(yield2)print('Deuxième send(), x = %s' % x)print('Yield 3')
x =(yield3)# Comme on fait un next() et 3 send()# on arrive làprint('Troisième send(), x = %s' % x)print('YOLOOOOO')print("Creation du generateur")
generateur = lune()
next(generateur)# Ou generateur.send(None)
res = generateur.send("A")print('res = %s' % res)
res = generateur.send("B")print('res = %s' % res)
res = generateur.send("C")print('res = %s' % res)print('Good bye')## Creation du generateur## On fait au moins un next()## Yield 1## Premier send(), x = A## Yield 2## res = 2## Deuxième send(), x = B## Yield 3## res = 3## Troisième send(), x = C## YOLOOOOO## Traceback (most recent call last):## File "test.py", line 33, in ## res = generateur.send("C")## StopIteration
send() agit donc comme next(). Il va aller jusqu’au prochain yield et lui faire retourner sa valeur. Mais il y a des différences :
Elle doit partir d’un précédent yield.
Donc il faut au moins avoir atteint un yield via next().
Ce précédent yield peut retourner une valeur : celle passée via send(val).
La valeur peut être n’importe quel objet : string, int, classe, list, etc.
Bref, send() permet de créer un générateur donc le comportement n’est pas figé dans le marbre.
Par exemple :
def creer_fontaine():
contenu ="soda"whileTrue:
x =yield contenu
if x:
contenu = x
fontaine = creer_fontaine()for x inrange(5):
print(next(fontaine))# on change le contenu de la fontaine
fontaine.send("lait")for x inrange(5):
print(next(fontaine))
soda
soda
soda
soda
soda
lait
lait
lait
lait
lait
On peut même s’en servir pour faire des trucs chelou comme injecter une dépendance à la volée ou contrôler le flux de son générateur :
def fuckitjaiplusdenomcool(start, inc=lambda x: x + 1):
x = start
# on controle le flux du générateur en changeant# la valeur de x qui peut tout stopperwhile x:
sent =yield x
if sent:
inc = sent
# la valeur de x dépend de ce bout de code# qui est injectable
x = inc(x)
generateur = fuckitjaiplusdenomcool(1)for x in generateur:
print(x)if x >10:
# si on dépasse 10, on décrémente
generateur.send(lambda x: x - 1)## 1## 2## 3## 4## 5## 6## 7## 8## 9## 10## 11## 9## 8## 7## 6## 5## 4## 3## 2## 1
Mais bon, pas la peine de rentrer dans des cas si compliqués.
Néanmoins, un cas d’usage de send() est de créer une coroutine. Une coroutine est simplement une tâche.
C’est un bout de code qui fait une tache, avec un bout d’initialisation, et un bout de finalisation, et un bout d’exécution.
Par exemple, j’ai un filtre qui prend un fichier rempli d’adresses IP. Il va recevoir du texte, et si le texte contient une adresse IP, il le signale, et remplit un compteur sur le disque.
Si on devait coder ça en objet on dirait :
importreclass Filtre:
# initialisationdef__init__(self, ipfile, counterfile):
withopen(ipfile,'r')as f:
self.banned_ips=set(f)withopen(counterfile)as f:
self.count=int(f.read())self.counterfile=open(counterfile,'w')def check(self, line):
# récupère les ip et check celles qui sont # à filtrer
ips =re.findall( r'[0-9]+(?:\.[0-9]+){3}', line)
bad_ips =[ip for ip in ips if ip inself.banned_ips]# si il y a des ip à filtrer, on incrémente le compteurif bad_ips:
self.count +=len(bad_ips)self.counterfile.seek(0)self.counterfile.write(str(self.count))# on retourn les valeurs trouvéesreturn bad_ips
def close(self):
self.counterfile.close()
On l’utiliserait comme ça :
f = Filtre("/chemin/vers/liste","/chemin/vers/counteur")for line in text:
print(f.check(line))
f.close()
Notez que pour une tâche, l’API est toujours la même : initialiser, exécuter la tâche autant de fois que nécessaire, puis finaliser.
Les coroutines sont un mot qu’on met sur ce principe (initialiser, exec, finaliser), mais avec une API sous forme de générateur. Le même code en coroutine :
def filtre(ipfile, counterfile):
# Initialisationwithopen(ipfile,'r')as f:
banned_ips =set(f)withopen(counterfile)as f:
count =int(f.read())
counterfile =open(counterfile,'w')# Exécution
bad_ips =[]whileTrue:
try:
# entree et sortie de notre send(), qui équivaut# aux params de "check()"
line =yield bad_ips
# GeneratorExit est levé is on fait generator.close()# On ne peut pas ignorer cette erreur, mais# on peut mettre du code de finalisation ici.# Bon en vrai faudrait faire un finally quelque part# mais c'est pour l'exemple bande de peer reviewersexcept GeneratorExit:
self.counterfile.close()
ips =re.findall( r'[0-9]+(?:\.[0-9]+){3}', line)
bad_ips =[ip for ip in ips if ip inself.banned_ips]# si il y a des ip à filtrer, on incrémente le compteurif bad_ips:
self.count +=len(bad_ips)self.counterfile.seek(0)self.counterfile.write(str(self.count))
On l’utiliserait comme ça :
f = filtre("/chemin/vers/liste","/chemin/vers/counteur")
next(f)for line in text:
print(f.send(line))# ceci raise GeneratorExit
f.close()
Généralement on veut pas se faire chier à appeler next() à chaque fois, donc toutes les libs à base de coroutine ont ce genre de décorateur :
Ca a un double usage : ça appelle next() automatiquement, et ça signale que la fonction est destinée à être utilisée comme coroutine.
Mais voilà, c’est tout, une coroutine c’est juste ça : utiliser un générateur pour faire une tâche qui consiste à s’initialiser, faire un traitement plusieurs fois, et optionellement, se finaliser. On utilisera une coroutine pour ne pas reinventer la roue car c’est un problème bien défini, qui a une solution. D’autant qu’une coroutine bouffe moins de ressources qu’une classe.
Les usages avancés des coroutines impliquent de chaîner plusieurs coroutines, comme des tuyaux.
Souvenez-vous, en Python il est courant de chaîner des générateurs :
def mettre_au_carre(iterable):
for x in iterable:
yield x * x
def filtrer_les_pairs(iterable):
for x in iterable:
if x % 2==0:
yield x
def strigifier(iterable):
for x in iterable:
yieldstr(x)# on pipe les données d'un générateur à l'autre
nombres =range(10)
carres = mettre_au_carre(nombres)
carres_pairs = filtrer_les_pairs(carres)
fete_du_string = strigifier(carres_pairs)for x in fete_du_string:
print(repr(x))## '0'## '4'## '16'## '36'## '64'
On peut faire pareil avec les coroutines. Cependant, la logique est inversée : au lieu de lire les données, on les envoie :
@coroutine
def mettre_au_carre(ouput):
whileTrue:
x =(yield)
ouput.send(x * x)@coroutine
def filtrer_les_paires(ouput):
whileTrue:
x =(yield)if x % 2==0:
ouput.send(x)@coroutine
def strigifier(ouput):
whileTrue:
x =(yield)
ouput.send(str(x))@coroutine
def afficher():
whileTrue:
x =(yield)print(x)
nombres =range(10)# chaque coroutine est la sortie d'une autre
afficheur = afficher()
fete_du_string = strigifier(afficheur)
paires = filtrer_les_paires(fete_du_string)
carre = mettre_au_carre(paires)# on envoit les données vers la première coroutine# et elle fait suivre aux autresfor x in nombres:
carre.send(x)## '0'## '4'## '16'## '36'## '64'
Vous allez me dire : “ça fait la même chose, et c’est plus compliqué, quel interêt ?”.
En fait, ça ne fait pas exactement la même chose.
Dans le cas des générateurs ordinaires, on déclenche le traitement par la fin. On fait une boucle qui demande quelle est la prochaine donnée, et si il y en a une, on l’affiche. C’est pratique si on sait qu’on a des données sous la main car on demande (next() est appelée par la boucle for) la donnée suivante à chaque fois : c’est du PULL.
Mais que se passe-t-il si on n’a pas encore les données ? Si on traite des données qui arrivent par évenement ?
Par exemple, si on écrit un serveur HTTP qui doit réagir aux requêtes ?
Dans ce cas, on ne peut envoyer (send()) la donnée suivante dans notre pipeline de générateurs uniquement quand elle arrive, et les coroutines font exactement cela : c’est du PUSH.
En résumé :
yield permet de faire des générateurs
On peut demander la prochaine valeur du générateur avec next(). Dans ce cas, le code s’exécute jusqu’au prochain yield.
On peut envoyer une valeur au générateur avec send(). Dans ce cas, on DOIT partir d’un yield existant duquel on récupère la valeur envoyée via une assignation. Donc il faut au moins un next() avant d’utiliser un send() et un signe égal sur le yield.
send() va aussi aller au prochain yield et retourner sa valeur.
Une coroutine n’est qu’une formalisation de la manière d’éffectuer une tâche avec un init, une exécution et une finalisation optionelle en utilisant un générateur. C’est une solution générique à un problème courant, mais plus léger qu’une classe.
Généralement on décore les générateurs coroutines avec un décorateur @coroutine pour s’éviter d’appeler next() à la main et notifier l’usage qu’il est fait de ce générateur.
On peut chaîner des coroutines comme on chaîne des générateurs, mais au lieu de lire les données une à une (PULL), on les envoie une par une (PUSH). Cela est pratique quand on ne sait pas à l’avance quand une nouvelle donnée va arriver.
Si vous êtes arrivé jusqu’ici, vous méritez un cookie.
Ca tombe bien, ce blog utilise des cookies, et la loi m’oblige à vous le notifier.