Sam & Max » yield http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Qu’est-ce qu’une coroutine en Python, et à quoi ça sert ? 10 http://sametmax.com/quest-ce-quune-coroutine-en-python-et-a-quoi-ca-sert/ http://sametmax.com/quest-ce-quune-coroutine-en-python-et-a-quoi-ca-sert/#comments Sat, 13 Dec 2014 23:37:06 +0000 http://sametmax.com/?p=12854 yield et vous apercevoir qu'on pouvait créer des coroutines avec. Mais sans vraiment comprendre ce que ça faisait.]]> 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')
    yield 1
 
    print('Deuxième next()')
    print('Yield 2')
    yield 2
 
    print('Troisième next()')
    print('Yield 3')
    yield 3
 
    # pas de quatrième next(),
    # donc on ne passe jamais ici
    print('Pas vu')
 
# rappel, ceci ne déclenche pas le code 
# de soleil() puisqu'il y a yield dedans
print("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 = (yield 1)
 
    print('Deuxième next()')
    print('Avant le yield 2, x = %s' % x)
    print('Yield 2')
    x = (yield 2)
 
    print('Troisième next()')
    print('Avant le yield 3, x = %s' % x)
    print('Yield 3')
    x = (yield 3)
 
    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 = (yield 1)
 
    print('Premier send(), x = %s' % x)
    print('Yield 2')
 
    x = (yield 2)
 
    print('Deuxième send(), x = %s' % x)
    print('Yield 3')
 
    x = (yield 3)
 
    # 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" 
    while True:
        x = yield contenu
        if x:
            contenu = x
 
 
fontaine = creer_fontaine()
 
for x in range(5):
    print(next(fontaine))
 
# on change le contenu de la fontaine
fontaine.send("lait")
 
for x in range(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 stopper
    while 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 :

import re
 
class Filtre:
 
    # initialisation
    def __init__(self, ipfile, counterfile):
 
        with open(ipfile, 'r') as f:
            self.banned_ips = set(f)
        with open(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 in self.banned_ips]
 
        # si il y a des ip à filtrer, on incrémente le compteur
        if bad_ips:
            self.count += len(bad_ips)
            self.counterfile.seek(0)
            self.counterfile.write(str(self.count))
 
        # on retourn les valeurs trouvées
        return 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):
 
    # Initialisation
    with open(ipfile, 'r') as f:
        banned_ips = set(f)
    with open(counterfile) as f:
        count = int(f.read())
    counterfile = open(counterfile, 'w')
 
    # Exécution
    bad_ips = []
    while True:
 
        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 reviewers
        except GeneratorExit:
                counterfile.close()
 
        ips = re.findall( r'[0-9]+(?:\.[0-9]+){3}', line)
        bad_ips = [ip for ip in ips if ip in banned_ips]
 
        # si il y a des ip à filtrer, on incrémente le compteur
        if bad_ips:
            count += len(bad_ips)
            counterfile.seek(0)
            counterfile.write(str(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 :

def coroutine(func):
    def wrapper(*arg, **kwargs):
        generator = func(*arg, **kwargs)
        next(generator)
        return generator
    return wrapper

Afin de pouvoir faire ça :

@coroutine
def filtre(ipfile, counterfile):
    ...

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:
        yield str(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):
    while True:
        x = (yield)
        ouput.send(x * x)
 
@coroutine
def filtrer_les_paires(ouput):
    while True:
        x = (yield)
        if x % 2 == 0:
            ouput.send(x)
 
@coroutine
def strigifier(ouput):
    while True:
        x = (yield)
        ouput.send(str(x))
 
@coroutine
def afficher():
    while True:
        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 autres
for 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.

]]>
http://sametmax.com/quest-ce-quune-coroutine-en-python-et-a-quoi-ca-sert/feed/ 10
5 choses à apprendre en priorité en Python 8 http://sametmax.com/5-choses-a-apprendre-en-priorite-en-python/ http://sametmax.com/5-choses-a-apprendre-en-priorite-en-python/#comments Sun, 22 Dec 2013 08:57:17 +0000 http://sametmax.com/?p=8376 Quand on apprend un nouveau langage de programmation, on apprend d’abord les bases. Et pour la plupart des langages, elles sont communes : déclarer une variable, faire des conditions et des boucles, faire des fonctions, importer un code d’un autre fichier, etc.

Ce qui va différencier le moment où vous savez programmer dans CE langage, ce sont des notions qui lui sont spécifiques et que vous commencez à maitriser.

Voici 5 notions spécifiques au langage qu’il faut apprendre en priorité si vous voulez pouvoir dire “je code en Python” :

Pip

Pip est la moyen le plus utilisé d’installer une bibliothèque externe dans l’environnement Python. Dès qu’on veut faire un projet sérieux, on en a besoin. Tellement qu’il va en fait être inclus par défaut dans Python 3.4.

Lire l’article sur pip.

Virtualenv

Virtualenv permet d’isoler plusieurs installations de Python. A partir du moment où l’on travaille sur plusieurs projets en même temps, il devient vite indispensable. Mais personnellement, je l’utilise même quand je n’ai qu’un projet installé sur une machine car il me permet de le séparer du setup Python du système et d’utiliser des hooks.

Un outil qui a été ajouté dans la lib standard en Python 3.3. J’apprécie que le pragmatisme de l’évolution de Python qui intègre petit à petit les projets qui se sont révélés les outils de facto dans la communauté.

Lire l’article sur virtualenv.

Les listes en intention

J’ai envie de dire l’itération en générale, mais c’est un très vaste sujet, et il est couvert en grande partie par les 3 derniers points.

La liste en intention, ou liste en compréhension, est une manière de boucler sur un itérable (souvent une liste), avec optionellement un filtre, afin de produire une nouvelle liste. En une ligne.

C’est stylistiquement la marque de fabrique de Python (même si c’est piqué à Haskell). C’est également ce qui le rend aussi expressif. On peut presque coder tout un programme en déclaratif avec des enchainements de listes en intention.

C’est beau, propre, efficace et court. IN-DIS-PEN-SA-BLE.

Lire l’article sur les listes en intention.

L’unpacking

L’unpacking est une autre fonctionalité typiquement pythonienne qui permet de prendre un itérable (souvent un tuple), et de mettre ses éléments dans des variables d’une traite.

Cela permet d’augmenter drastiquement la lisibilité des programmes.

Lire les articles sur l’unpacking.

Les générateurs

Les générateurs permettent non seulement un énorme gain en performance, mais en plus ils autorisent le traitement itératif de flux de données dont on ne connait pas la taille en avance, voire de taille infinie. Si vous utilisez des expressions génératrices, vous pourrez le faire en déclaratif. Si vous utilisez yield, vous pourrez cacher un algorithme complet derrière une simple boucle for.

Lire l’article sur yield.

Le reste ?

Tout le reste, c’est du détail. Les décorateurs, la POO, l’opérateur with, les métaclasses, les astuces magiques pour faire ceci ou cela. C’est bien, mais ça peut attendre. Ce sont ces 5 notions, qui, bien utilisées, feront d’un programmeur un dev Python.

]]>
http://sametmax.com/5-choses-a-apprendre-en-priorite-en-python/feed/ 8
map(), filter() et reduce () ? 9 http://sametmax.com/map-filter-et-reduce/ http://sametmax.com/map-filter-et-reduce/#comments Thu, 14 Nov 2013 09:44:58 +0000 http://sametmax.com/?p=7791 map(), filter() et reduce() sont des fonctions de traitement d'itérables typiques de la programmation fonctionnelle, qui ont été marquées comme à retirer des builtins pour Python 3. Finalement, seule reduce() sera déplacée dans le module functools pour Python 3.]]> map(), filter() et reduce() sont des fonctions de traitement d’itérables typiques de la programmation fonctionnelle, qui ont été marquées comme à retirer des builtins pour Python 3. Finalement, seule reduce() sera déplacée dans le module functools pour Python 3.

Les opérations que font ces fonctions sont typiquement quelque chose que l’ont peut faire sans elles, et nous allons les passer en revue pour voir dans quels cas elles sont pertinentes, dans quel cas une alternative est meilleure. L’alternative étant, dans 90% des cas, une liste en intention.

filter()

filter() prend une fonction en paramètre, souvent une lambda, comme ses deux soeurs puisqu’on est dans le paradigme fonctionnel. Elle doit renvoyer True si on garde un élément, et False sinon.

L’usage typique est celui-ci :

ages = range(30)
majeurs = filter(lambda x: x > 18, ages)
print(majeurs)
## [19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]

Typiquement, ce code peut être remplacé par une liste en intention dans le pur style Python :

majeurs = [a for a in ages if a > 18]

Le code est plus court, et fait usage d’une des fonctionalités les plus importantes du langage. On peut en plus ajouter une transformation à a facilement si on le désire, au lieu de devoir coller un map() derrière.

filter() est vraiment la moins utile des 3, et sera une question de style, surtout pour les nostalgiques de Lisp. Je répète souvent que quand on code avec un autre langage, on doit essayer de se tenir au style du nouveau et pas faire un mix avec ses anciennes habitudes. Quand je code en Java, je fais des getter et setter, même si j’ai horreur de ça.

Il existe quand même UNE astuce pour laquelle filter est utile : garder uniquement les éléments vrais d’une liste.

Typiquement :

l = [1, 0, None, [], True]
print filter(bool, l)
[1, True]

Ca marche si la fonction pre-existe et qu’on a pas à faire une lambda, mais c’est vraiment le seul usage potable. Un peu plus court qu’une liste en intention.

map()

Si filter() est l’équivalent de la partie de droite d’une liste en intention, map() est l’équivalent de la partie de gauche. La fonction passée retourne un résultat qui permet de transformer la liste.

Typiquement :

memes = ["It's over 9000 !", "All your base are belong to us."]
print(map(unicode.upper, memes))

Ce qui peut se traduire par :

print(s.upper() for s in memes)

map() est un peu plus utile, dans le sens où sa syntaxe peut être plus concise dans certains cas, comme le casting de types. Par exemple si je reçois une heure sous forme de string :

h, m, s = map(int, '8:19:22'.split(':'))

sera plus court et plus concis, et plus clair que :

h, m, s = (int(i) for i in '8:19:22'.split(':'))

Mais bon, la différence n’est pas non plus incroyable au point d’en faire une fonctionnalitéé clé. Je l’utilise de temps à autre par soucis de brièveté, mais vraiment c’est tout.

reduce()

reduce() est plus tordu. La fonction doit prendre deux paramètres en entrée, et retourner une valeur. Au premier appel, les deux premiers éléments de l’itérable sont passés en paramètres. Ensuite, le résultat de cet appel et l’élément suivant sont passés en paramètre, et ainsi de suite.

Vous n’avez rien pigé ? C’est normal. reduce() est parfaitement cryptique. Voici ce que ça donne en pratique :

def afficher(a, b):
    print("Entrée :", a, b)
    print("Sortie :", a + b)
    return a + b
 
res = reduce(afficher, range(10))
print("Résultat final", res)
 
## Entrée : 0 1
## Sortie : 1
## Entrée : 1 2
## Sortie : 3
## Entrée : 3 3
## Sortie : 6
## Entrée : 6 4
## Sortie : 10
## Entrée : 10 5
## Sortie : 15
## Entrée : 15 6
## Sortie : 21
## Entrée : 21 7
## Sortie : 28
## Entrée : 28 8
## Sortie : 36
## Entrée : 36 9
## Sortie : 45
## Résultat final 45

Vous allez me dire, à quoi ça sert ? Et bien par exemple à appliquer des opérateurs commutatifs, ici nous l’avons fait avec +, nous avons fait la somme de tous les éléments retournés par range(10). La preuve :

print(sum(range(10)))
## 45

Il n’y a pas, en Python, de fonction équivalent à sum() pour la multiplication. Donc on ferait :

print(reduce(lambda a, b: a * b, range(1, 11)))
## 3628800

Ce qui multiplie tous les éléments entre eux. Comme l’ordre dans lequel les éléments sont multipliés n’a pas d’important (d’où le ‘commutatif’), ça fonctionne.

reduce() peut prendre un troisième paramètre, initial, qui sera la valeur passée en premier au premier appel de la fonction. Cela permet de travailler sur des calculs en cascade qui ne fonctionneraient sinon pas. Revenons à notre exemple de temps :

temps = map(int, '8:19:22'.split(':'))
print(reduce(lambda a, b: a * 60 + b, temps, 0))
## 29962

Ce qui peut se traduire par :

h, m, s = map(int, '8:19:22'.split(':'))
print(h * 3600 + m * 60 + s)
## 29962

Bien sûr, cette conversion ne fonctionnerait pas si le calcul était sur un itérable plus long. Mais une version itérative est facile à faire :

res = 0
for i in map(int, '8:19:22'.split(':')):
    res = res * 60 + i
print(res)
## 29962

Maintenant, autant les deux dernières versions sont faciles à comprendre, autant la première prend quelques secondes. Et c’est la raison pour laquelle reduce() a été retirée des builtins, pour encourager l’usage des alternatives. En effet, cette fonction donne toujours un résultat très peu lisible. Je cite et approuve Guido là dessus:

C’est en fait celle que je déteste le plus, car, à part pour quelques exemples impliquant + ou *, presque chaque fois que je vois un appel à reduce() avec une fonction non-triviale passée en argument, j’ai besoin de prendre un crayon et un papier pour faire le diagramme de ce qui est effectivement entrée dans la fonction avant que je comprenne ce qu’est supposé faire reduce(). Donc à mes yeux, l’application de reduce() est plutôt limitée à des opérateurs associatifs, et dans d’autres cas il est mieux d’écrire une boucle d’accumulation explicitement.

Graissage maison.

Bref, reduce() est dur à lire, et une boucle ne l’est pas. Écrivez 3 lignes de plus, ça ne va pas vous tuer. Relire votre one-liner dans un mois par contre…

Cette fonction a été beaucoup utilisée avec les opérateurs or et and pour savoir si tous les éléments étaient vrais au moins un élément vrai dans une liste :

tout_est_vrai = [1, 1, 1, 1]
certains_sont_vrais = [1, 0, 1, 0]
tout_est_faux = [0, 0, 0, 0]
 
# Retourne True si tout est vrai
print(bool(reduce(lambda a, b: a and b, tout_est_vrai)))
## True
print(bool(reduce(lambda a, b: a and b, certains_sont_vrais)))
## False
print(bool(reduce(lambda a, b: a and b, tout_est_faux)))
## False
 
# Retourne True si au moins un élément est vrai
print(bool(reduce(lambda a, b: a or b, tout_est_vrai)))
## True
print(bool(reduce(lambda a, b: a or b, certains_sont_vrais)))
## True
print(bool(reduce(lambda a, b: a or b, tout_est_faux)))
## False

Mais aujourd’hui, c’est parfaitement inutile puisque nous avons les fonctions built-in all() et any(), qui font ça en plus court et plus rapide :

# Retourne True si tout est vrai
print(all(tout_est_vrai))
## True
print(all(certains_sont_vrais))
## False
print(all(tout_est_faux))
## False
 
# Retourne True si au moins un élément est vrai
print(any(tout_est_vrai))
## True
print(any(certains_sont_vrais))
## True
print(any(tout_est_faux))
## False

Petite astuce finale

Souvenez-vous également que les fonctions Python peuvent être déclarées n’importe où à la volée, même dans une autre fonction, une classe, une méthode, un context manager, etc. Or une fonction peut retourner un générateur grâce à yield, ce qui vous permet de déclarer des gros bouts de logique, et de les plugger dans votre process itérative a posteriori :

def traitement_complexe(iterable):
    for x in iterable:
        if x not in (1, 3, 7) and x % 2 != 0:
            if x + x < 13 :
                yield x
            else: 
                yield x - 2
 
print("-".join(map(str, traitement_complexe(range(20)))))
## 5-7-9-11-13-15-17
]]>
http://sametmax.com/map-filter-et-reduce/feed/ 9
S’affranchir des doublons d’un itérable en Python 9 http://sametmax.com/saffranchir-des-doublons-dun-iterable-en-python/ http://sametmax.com/saffranchir-des-doublons-dun-iterable-en-python/#comments Tue, 20 Aug 2013 10:10:32 +0000 http://sametmax.com/?p=7143 Supprimer ou ignorer les doublons d’un itérable tel qu’une liste ou un array est un challenge dans tous les langages. Il faut se poser les questions suivantes :

  • Qu’est-ce qu’un doublon ?
  • Quels types d’itérables traite-t-on ?
  • Quel est la taille de l’itérable ?
  • Et niveau perfs ?

En Python, on a des structures de données qui suppriment automatiquement les doublons : les sets et les dictionnaires. Mais elles ne conservent pas l’ordre des élements.

Il y a aussi le fait qu’un itérable en Python peut avoir une taille inconnue, ou infinie.

Le post est long, donc…

Solution 1 : générateur et hashing

En utilisant conjointement les générateurs, les sets et une petite injection de dépendance, on peut trouver un compromis entre flexibilité et performances :

def skip_duplicates(iterable, key=lambda x: x):
 
    # on va mettre l’empreinte unique de chaque élément dans ce set
    fingerprints = set()
 
    for x in iterable:
        # chaque élement voit son emprunte calculée. Par défaut l’empreinte
        # est l'élément lui même, ce qui fait qu'il n'y a pas besoin de
        # spécifier 'key' pour des primitives comme les ints ou les strings.
        fingerprint = key(x)
 
        # On vérifie que l'empreinte est dans la liste des empreintes  des
        # éléments précédents. Si ce n'est pas le cas, on yield l'élément, et on
        # rajoute sont empreinte ans la liste de ceux trouvés, donc il ne sera
        # pas yieldé si on ne le yieldera pas une seconde fois si on le
        # rencontre à nouveau
        if fingerprint not in fingerprints:
            yield x
            fingerprints.add(fingerprint)

La fonction s’appelle skip_duplicates car c’est ce qu’elle fait. Elle ne retire pas vraiment les doublons, elle produit un flux de d’éléments qui ne comporte pas de doublons en ignorant tout doublons présent dans l’itérable initial.

Cette approche a plusieurs avantages :

  • Les doublons sont bien retirés, et l’ordre est conservé.
  • La complexité est de 0(n).
  • L’utilisateur peut choisir ce qui fait qu’un élément est unique : un attribut, un sous-élément, l’affichage sous forme de string…
  • C’est un générateur, est cela fonctionne donc avec des itérables de toute taille, même inconnue ou infinie.

Il faut néanmoins que l’ensemble des éléments uniques tiennent au moins une fois en mémoire en plus de l’itérable initial, et potentiellement d’un stockage à la sortie du générateur. On fait donc un trade-off sur la mémoire.

Comme la valeur de key par défaut est une valeur saine, ça fonctionne comme on s’y attend pour les cas simples :

>>> list(skip_duplicates([1, 2, 3, 4, 4, 2, 1, 3 , 4]))
[1, 2, 3, 4]
>>> list(skip_duplicates('fjsqlkdmfjsklqmdfjdmsl'))
[u'f', u'j', u's', u'q', u'l', u'k', u'd', u'm']
>>> list(skip_duplicates(((1, 2), (2, 1), (1, 2), (1, 1))))
[(1, 2), (2, 1), (1, 1)]

Pourvoir spécifier ‘key’ permet de faire des choix dans ce qu’est un doublon :

>>> list(skip_duplicates((1, 2, '1', '1', 2, 3, '3')))
[1, 2, u'1', 3, u'3']
>>> list(skip_duplicates((1, 2, '1', '1', 2, 3, '3'), key=lambda x: str(x)))
[1, 2, 3]

Et si on s’attaque à des cas plus complexes, le fonction vous force à préciser votre pensée :

>>> list(skip_duplicates(([], [], (), [1, 2], (1, 2)))
... )
Traceback (most recent call last):
  File "<ipython-input-20-ed44f170c634>", line 1, in <module>
    list(skip_duplicates(([], [], (), [1, 2], (1, 2)))
  File "<ipython-input-18-42dbb94f03f8>", line 7, in skip_duplicates
    if fingerprint not in fingerprints:
TypeError: unhashable type: 'list'

En effet les listes ne sont pas des types hashables en Python, on ne peut donc pas les stocker dans un set.

Mais on peut caster la liste, et faire ainsi le choix de savoir sur quel critère on base notre égalité. Par exemle, considère-t-on que deux itérables ayant le même contenu sont égaux, où alors doivent-ils avoir le même type ?

>>> list(skip_duplicates(([], [], (), [1, 2], (1, 2)), lambda x: tuple(x)))
[[], [1, 2]]
>>> list(skip_duplicates(([], [], (), [1, 2], (1, 2)), lambda x: (type(x), tuple(x))))
[[], (), [1, 2], (1, 2)]

Nous utilisons le fait que :

>>> tuple([1, 2]) == (1, 2)
True
>>> (type([1, 2]), tuple([1, 2])) == (type((1, 2)), (1, 2))
False

Puisque :

>>> (type([1, 2]), tuple([1, 2]))
(<type 'list'>, (1, 2))
>>> (type((1, 2)), (1, 2))
(<type 'tuple'>, (1, 2))

Dans le cas où nous ne sommes pas capables de déterminer ce qu’est un doublon, la fonction ne retire simplement rien :

class Test(object):
    def __init__(self, foo='bar'):
        self.foo = foo
    def __repr__(self):
        return "Test('%s')" % self.foo
 
>>> list(skip_duplicates([Test(), Test(), Test('other')]))
[Test('bar'), Test('bar'), Test('other')]

Mais permet encore une fois de faire le choix de quoi retirer :

>>> list(skip_duplicates([Test(), Test(), Test('other')], key=lambda x: x.foo))
[Test('bar'), Test('other')]

Ce principe de la fonction key, on le retrouve dans sorted(), donc les habitués seront déjà à l’aise. Et j’aime beaucoup ce pattern, car il est très puissant. On peut avoir la fonction key qui renvoit des choses très simples :

  • Un attribut.
  • Un element (x[2], x['cle']…)
  • Une version castée avec int(), str(), tuple(), etc

Mais on peut aussi faire des choses très complexes. En effet, rien ne nous oblige à utiliser une lambda, on peut mettre une fonction complète et lui faire retourner :

  • Un hash md5.
  • Une entrée en base de données.
  • Un nouvel objet customisé.
  • Un tuple de tuples d’objets custos avec des dictionnaires en attributs…
  • Le contenu d’un fichier.

Python sait naturellement comparer tout ça.

Notez que nous trichons un peu, puisque nous retirons les doublons en nous basant sur un set qui va calculer un hash de l’objet, et pas véritablement vérifier l’égalité. La fonction en fonctionnera donc pas si l’utilisateur définie __eq__ et s’attend à ce que les doublons soient retirés. Ce qui nous amène à …

Solution 2 : iterateur et comparaison

Pour ce genre de chose, un autre algo, qui ne fontionerait que sur les itérables de taille finie, et qui serait bien plus lent (complexité n log(n)), peut être utilisé :

def strip_duplicates(iterable, equals=lambda x, y: x == y):
 
    # On transforme l'itérable en iterateur sur lui même, cela va nous
    # permettre d'appeler next() dessus et récupérer le premier élément,
    # même sur un objet non indexable (sur lequel on ne peut utiliser [0])
    iterable = iter(iterable)
 
    res = []
    # Une petite boucle infinie est nécessaire car la boucle 'for' ne nous
    # permet pas de récupérer le premier élément indépendamment des autres,
    # et la boucle 'while' attend une condition de sortie, ce que nous n'avons
    # pas forcément (il n'est pas possible de vérifier le nombre d'éléments
    # restant dans un générateur).
    while True:
 
        # on récupère le premier élément de l'iterable restant, si il n'y en
        # a plus, on sort de la boucle.
        try:
            elem = next(iterable)
        except StopIteration:
            break
 
        # Le premier élément est ajouté au résultat sans doublons. Maintenant
        # on va recréer l'itérable, mais en retirant tout ce qui était égal
        # au premier élément. Notez que 'être égal' est une condition modifiable
        # en passant une fonction en paramètre, comme l'était 'key' précédemment.
        res.append(elem)
 
        iterable = iter([x for x in iterable if not equals(elem, x)])
 
    return res

La fonction s’appelle strip_duplicates car elle produit une nouvelle liste, mais sans les éléments indésirables, comme le fait strip() sur une chaîne (produit une nouvelle chaîne, sans les éléments indésirables).

Ce type de fonction peut être utile dans plusieurs cas :

  • On a pas envie de se poser la question de savoir si nos types à comparer sont hashable ou pas, et on est prêt à payer un peu de CPU pour cela.
  • On a besoin de retirer les doublons sur la base d’une égalité, par exemple sur l’existence de la méthode __eq__.
  • On a besoin de retirer les doublons sur la base d’une logique complexe qui dépend du contexte.

A première vu cela fonctionne presque de la même manière que skip_duplicates, mais en retournant une liste plutôt qu’un générateur :

>>> strip_duplicates('fdjqkslfjdmkfdsqjkfmjqsdmlkfjqslkmfjsdklfl')
['f', 'd', 'j', 'q', 'k', 's', 'l', 'm']

Mais déjà il n’y a pas à se soucier de savoir si une structure de données est hashable :

>>> strip_duplicates(([], [], (), [1, 2], (1, 2)))
[[], (), [1, 2], (1, 2)]

Même si on garde la même flexibilité, bien que la fonction à passer ait une signature légèrement différente :

>>> strip_duplicates(([], [], (), [1, 2], (1, 2)), lambda x, y: tuple(x) == tuple(y))
[[], [1, 2]]

Le plus interessant reste que cela fonctionne sur l’égalité, et donc cela marche d’office avec les objets qui déclarent __eq__ ce qui est le cas dans de nombreuses libs, comme les ORM :

class Test(object):
    def __init__(self, foo='bar'):
        self.foo = foo
    def __repr__(self):
        return "Test('%s')" % self.foo
    def __eq__(self, other):
        return self.foo == other.foo
 
>>> strip_duplicates([Test(), Test(), Test('other')])
[Test('bar'), Test('other')]

Dans certains cas, notamment dans le cas où le point de comparaison est un object non hashable de très grosse taille (par exemple un dico très long), on peut espérer aussi pas mal économiser en mémoire. Mais on est qu’en est-il des besoins en mémoire et en CPU ?

Solution 3 : retirer les doublons, in place

Enfin, pour ceux qui ont de grosses contraintes de mémoire et qui veulent un algo rapide au prix de la flexibilité du code, voici une solution qui oblige à travailler sur des listes et à modifier la liste sur place :

def remove_duplicates(lst, equals=lambda x, y: x == y):
 
    # Normalement en Python on adore le duck typing, mais là cet algo suppose
    # l'usage d'une liste, donc on met un gardefou.
    if not isinstance(lst, list):
        raise TypeError('This function works only with lists.')
 
    # là on est sur quelque chose qui ressemble vachement plus à du code C ^^
    i1 = 0
    l = (len(lst) - 1)
 
    # on itère mécaniquement sur la liste, à l'ancienne, avec nos petites
    # mains potelées.
    while i1 < l:
 
        # on récupère chaque élément de la liste, sauf le dernier
        elem = lst[i1]
 
        # on le compare à l'élément suivant, et chaque élément après
        # l'élément suivant
        i2 = i1 + 1
        while i2 <= l:
            # en cas d'égalité, on retire l'élément de la liste, et on
            # décrément la longueur de la liste ainsi amputée
            if equals(elem, lst[i2]):
                del lst[i2]
                l -= 1
            i2 += 1
 
        i1 += 1
 
    return lst

Et là on est bien dans de la modification sur place :

>>> lst = list('fjdsklmqfjskdfjmld')
>>> lst
[u'f', u'j', u'd', u's', u'k', u'l', u'm', u'q', u'f', u'j', u's', u'k', u'd', u'f', u'j', u'm', u'l', u'd']
>>> remove_duplicates(lst)
[u'f', u'j', u'd', u's', u'k', u'l', u'm', u'q']
>>> lst
[u'f', u'j', u'd', u's', u'k', u'l', u'm', u'q']

La fonction s’appelle cette fois bien remove_duplicates puisque c’est ce qu’elle fait : retirer les doublons de la liste originale.

Et maintenant, c’est l’heure du benchmark à deux balles !

skip_duplicates :

setup = """
def skip_duplicates(iterable, key=lambda x: x):
        fingerprints = set()
        for x in iterable:
                fingerprint = key(x)
                if fingerprint not in fingerprints:
                        yield x
                        fingerprints.add(fingerprint)
import string
lst = list(string.ascii_letters * 100)"""
>>> timeit.timeit('list(skip_duplicates(lst))', setup=setup, number=1000)
0.9810519218444824

strip_duplicates :

>>> setup = """
def strip_duplicates(iterable, equals=lambda x, y: x == y):
    iterable = iter(iterable)
    res = []
    while True:
        try:
            elem = next(iterable)
        except StopIteration:
            break
        res.append(elem)
 
        iterable = iter([x for x in iterable if not equals(elem, x)])
 
    return res
 
import string
lst = list(string.ascii_letters * 100)"""
>>> timeit.timeit('list(strip_duplicates(lst))', setup=setup, number=1000)
41.462974071502686

remove_duplicates :

setup = """
def remove_duplicates(lst, equals=lambda x, y: x == y):
    if not isinstance(lst, list):
        raise TypeError('This function works only with lists.')
    i1 = 0
    l = (len(lst) - 1)
    while i1 < l:
        elem = lst[i1]
        i2 = i1 + 1
        while i2 <= l:
            if equals(elem, lst[i2]):
                del lst[i2]
                l -= 1
            i2 += 1
        i1 += 1
    return lst
 
import string
lst = list(string.ascii_letters * 100)"""
>>> timeit.timeit('list(remove_duplicates(lst))', setup=setup, number=1000)
0.37493896484375

Sans surprise, la version inplace est la plus rapide puisque la plus restrictive. En second vient notre strip_duplicates, beaucoup fois plus lente. Et en dernier, 50 fois plus lente, le compromis entre les deux : souple, consomme moins de mémoire que skip, mais plus que remove.

Mais ce n’est pas très juste pour strip, puisque que skip n’a pas à faire un gros travail de conversion. Essayons avec des clés plus grosses :

skip_duplicates :

setup = """
def skip_duplicates(iterable, key=lambda x: x):
        fingerprints = set()
        for x in iterable:
                fingerprint = key(x)
                if fingerprint not in fingerprints:
                        yield x
                        fingerprints.add(fingerprint)
import string, random
lst = [list(string.ascii_letters * 100) for x in xrange(100)]
for x in lst:
    x.pop(random.randint(0, len(x) - 1))"""
>>> timeit.timeit('list(skip_duplicates(lst, lambda x: tuple(x)))', setup=setup, number=1000)
15.516181945800781

strip_duplicates :

>>> setup = """
def strip_duplicates(iterable, equals=lambda x, y: x == y):
    iterable = iter(iterable)
    res = []
    while True:
        try:
            elem = next(iterable)
        except StopIteration:
            break
        res.append(elem)
 
        iterable = iter([x for x in iterable if not equals(elem, x)])
 
    return res
 
import string, random
lst = [list(string.ascii_letters * 100) for x in xrange(100)]
for x in lst:
    x.pop(random.randint(0, len(x) - 1))"""
>>> timeit.timeit('list(strip_duplicates(lst))', setup=setup, number=1000)
22.047110080718994

remove_duplicates :

setup = """
def remove_duplicates(lst, equals=lambda x, y: x == y):
    if not isinstance(lst, list):
        raise TypeError('This function works only with lists.')
    i1 = 0
    l = (len(lst) - 1)
    while i1 < l:
        elem = lst[i1]
        i2 = i1 + 1
        while i2 <= l:
            if equals(elem, lst[i2]):
                del lst[i2]
                l -= 1
            i2 += 1
        i1 += 1
    return lst
 
import string, random
lst = [list(string.ascii_letters * 100) for x in xrange(100)]
for x in lst:
    x.pop(random.randint(0, len(x) - 1))"""
>>> timeit.timeit('list(remove_duplicates(lst))', setup=setup, number=1000)
14.763166904449463

Comme souvent les résultats sont contre untuitifs, car bien que remove garde son avance, elle s’est largement réduite. A l’inverse, skip n’est pas tant à la ramasse que ça, et strip reste le plus lent.

Il faudrait aussi mesurer la consommation mémoire, je suis certain que ce serait interessant.

Bon, il est temps de mettre tout ça dans batbelt.

]]>
http://sametmax.com/saffranchir-des-doublons-dun-iterable-en-python/feed/ 9
Implémenter une fenêtre glissante en Python avec un deque 7 http://sametmax.com/implementer-une-fenetre-glissante-en-python-avec-un-deque/ http://sametmax.com/implementer-une-fenetre-glissante-en-python-avec-un-deque/#comments Thu, 20 Dec 2012 19:02:43 +0000 http://sametmax.com/?p=3562 l'itération par morceaux sur un itérable de n'importe quelle taille. Grâce au deque, on peut aussi facilement créer une fenêtre glissante.]]> Quand j’entends deque, je pense plus à un deck de Magic de Gathering qu’à un module Python, mais aujourd’hui j’utilise beaucoup plus le dernier que le précédent.

On a déjà vu comment implémenter l’itération par morceaux sur un itérable de n’importe quelle taille. Grâce au deque, on peut aussi facilement créer une fenêtre glissante.

Qu’est-ce qu’une fenêtre glissante ?

Il s’agit de parcourir un itérable par tranches de “n” élements, en faisant en sorte que l’insertion d’un nouvel élément dans la tranche implique la supression du premier entré. Mouai, dit comme ça c’est pas clair. Un p’tit exemple ?

Fenêtre glissante de 3 éléments sur une liste de 10 éléments, à la mano:

>>> l = range(10)
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[:3]
[0, 1, 2]
>>> l[1:4]
[1, 2, 3]
>>> l[2:5]
[2, 3, 4]
>>> l[3:6]
[3, 4, 5]
...

A ne pas confondre avec le parcours par morceaux, qu’on déjà traité, et qui donne ça:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[0:3]
[0, 1, 2]
>>> l[3:6]
[3, 4, 5]
>>> l[6:9]
[6, 7, 8]
...

Et bien sûr, comme d’hab, vous allez dire “Cher Sam, malgré ton génie qui n’a d’égal que ta beauté et l’incohérence de cette remarque, je ne vois pas vraiment à quoi ça sert.”

Et bien essentiellement dans les situations où un élément est dépendant des précédents, par exemple pour répondre à des questions existentielles comme “quel est le premier élément qui est la somme des 5 le précédant dans cette liste ?”. Ça n’arrive certes pas tous les jours, mais fuck, j’avais pas de meilleur exemple en tête, là, tout de suite.

Qu’est-ce qu’un deque ?

Un deque est une structure de données qui permet d’implémenter des comportements de types piles, files, FIFO, LIFO, et autres pipos avec efficacité. Traduit en langage de tous les jours, c’est comme une liste, mais:

  • ajouter un élément à la fin ou au début est une opération super rapide;
  • les retirer aussi;
  • on peut itérer dessus à la même vitesse que la liste;
  • mais accéder à un élement qui n’est pas au début ou à la fin juste par son index est très lent;
  • on peut limiter le nombre d’éléments que ça contient automatiquement.

On va donc l’utiliser comme acumulateur ou pour faire des files d’attentes.

Démonstration:

>>> from collections import deque
>>> ze_deck = deque(maxlen=3) # on limite le deque à 3 éléments
>>> ze_deck.append(1)
>>> ze_deck
deque([1], maxlen=3)
>>> ze_deck.append(2)
>>> ze_deck
deque([1, 2], maxlen=3)
>>> ze_deck.append(3)
>>> ze_deck
deque([1, 2, 3], maxlen=3)
>>> ze_deck.append(4) # ... et MAGIQUEMENT !
>>> ze_deck
deque([2, 3, 4], maxlen=3)
>>> for x in ze_deck:
...     print x
...     
2
3
4

Bam le dernier élément est squizzé, et tout ça rapidement et efficacement. Vous voyez où je veux subtilement en venir ?

Et finalement: comment on utilise l’un pour faire l’autre ?

>>> from collections import deque
>>> from itertools import islice
>>> 
>>> def window(iterable, size=2):
...         iterable = iter(iterable)
...         d = deque(islice(iterable, size), size)
...         yield d
...         for x in iterable:
...                 d.append(x)
...                 yield d
...         
>>> for x in window('azertyuiop'):
...         print x
...     
deque(['a', 'z'], maxlen=2)
deque(['z', 'e'], maxlen=2)
deque(['e', 'r'], maxlen=2)
deque(['r', 't'], maxlen=2)
deque(['t', 'y'], maxlen=2)
deque(['y', 'u'], maxlen=2)
deque(['u', 'i'], maxlen=2)
deque(['i', 'o'], maxlen=2)
deque(['o', 'p'], maxlen=2)

Vous noterez au passage que nous faisons ici un usage malin des générateurs, ce qui nous permet de retourner un itérable sans tout charger en mémoire et de slider sur notre window avec classe.

Ainsi on peut se la peter en faisant des trucs compliqués en une ligne comme:

>>> {tuple(d): i for i, d in enumerate(window(xrange(10), 3))}
{(5, 6, 7): 5, (0, 1, 2): 0, (7, 8, 9): 7, (6, 7, 8): 6, (1, 2, 3): 1, (3, 4, 5): 3, (2, 3, 4): 2, (4, 5, 6): 4}

Ce qui n’a absolument aucun intérêt, mais si vous comprenez ce que ça fait, alors vous avec bien masterisé Python.

]]>
http://sametmax.com/implementer-une-fenetre-glissante-en-python-avec-un-deque/feed/ 7
Comment utiliser yield et les générateurs en Python ? 14   Recently updated ! http://sametmax.com/comment-utiliser-yield-et-les-generateurs-en-python/ http://sametmax.com/comment-utiliser-yield-et-les-generateurs-en-python/#comments Tue, 02 Oct 2012 12:09:42 +0000 http://sametmax.com/?p=2416 Les générateurs sont une fonctionalité fabuleuse de Python, et une étape indispensable dans la maîtrise du langage. Une fois compris, vous ne pourrez plus vous en passer.

Rappel sur les itérables

Quand vous lisez des éléments un par un d’une liste, on appelle cela l’itération:

lst = [1, 2, 3]
>>> for i in lst :
...     print(i)
1
2
3

Et quand on utilise une liste en intention, on créé une liste, donc un itérable. Encore une fois, avec une boucle for, on prend ses éléments un par un, donc on itère dessus:

lst = [x*x for x in range(3)]
>>> for i in lst :
...     print(i)
0
1
4

À chaque fois qu’on peut utiliser “forin…” sur quelque chose, c’est un itérable : lists, strings, files…

Ces itérables sont pratiques car on peut les lire autant qu’on veut, mais ce n’est pas toujours idéal car on doit stocker tous les éléments en mémoire.

Les générateurs

Si vous vous souvenez de l’article sur les comprehension lists, on peut également créer des expressions génératrices:

generateur = (x*x for x in range(3))
>>> for i in generateur :
...     print(i)
0
1
4

La seule différence avec précédemment, c’est qu’on utilise () au lieu de []. Mais on ne peut pas lire generateur une seconde fois car le principe des générateurs, c’est justement qu’ils génèrent tout à la volée: ici il calcule 0, puis l’oublie, puis calcule 1, et l’oublie, et calcule 4. Tout ça un par un.

Le mot clé yield

yield est un mot clé utilisé en lieu et place de return, à la différence près qu’on va récupérer un générateur.

>>> def creerGenerateur() :
...     mylist = range(3)
...     for i in mylist:
...         yield i*i
...
>>> generateur = creerGenerateur() # crée un générateur
>>> print(generateur) # generateur est un objet !
< generator object creerGenerateur at 0x2b484b9addc0>
>>> for i in generateur:
...     print(i)
0
1
4

Ici c’est un exemple inutile, mais dans la vraie vie vivante, c’est pratique quand on sait que la fonction va retourner de nombreuses valeurs qu’on ne souhaite lire qu’une seule fois.

Le secret des maîtres Zen qui ont acquis la compréhension transcendantale de yield, c’est de savoir que quand on appelle la fonction, le code de la fonction n’est pas exécute. A la place, la fonction va retourner un objet générateur.

C’est pas évident à comprendre, alors relisez plusieurs fois cette partie.

creerGenerateur() n’exécute pas le code de creerGenerateur.

creerGenerateur() retourne un objet générateur.

En fait, tant qu’on ne touche pas au générateur, il ne se passe rien. Puis, dès qu’on commence à itérer sur le générateur, le code de la fonction s’exécute.

La première fois que le code s’éxécute, il va partir du début de la fonction, arriver jusqu’à yield, et retourner la première valeur. Ensuite, à chaque nouveau tour de boucle, le code va reprendre de la où il s’est arrêté (oui, Python sauvegarde l’état du code du générateur entre chaque appel), et exécuter le code à nouveau jusqu’à ce qu’il rencontre yield. Donc dans notre cas, il va faire un tour de boucle.

Il va continuer comme ça jusqu’à ce que le code ne rencontre plus yield, et donc qu’il n’y a plus de valeur à retourner. Le générateur est alors considéré comme définitivement vide. Il ne peut pas être “rembobiné”, il faut en créer un autre.

La raison pour laquelle le code ne rencontre plus yield est celle de votre choix: condition if/else, boucle, recursion… Vous pouvez même yielder à l’infini.

Un exemple concret et un café, plz

yield permet non seulement d’économiser de la mémoire, mais surtout de masquer la complexité d’un algo derrière une API classique d’itération.

Supposez que vous ayez une fonction qui – tada ! – extrait les mots de plus de 3 caractères de tous les fichiers d’un dossier.

Elle pourrait ressembler à ça:

import os
 
def extraire_mots(dossier):
    for fichier in os.listdir(dossier):
        with open(os.path.join(dossier, fichier)) as f:
            for ligne in f:
                for mot in ligne.split():
                    if len(mot) > 3:
                        yield mot

Vous avez là un algo dont on masque complètement la complexité, car du point de vue de l’utilisateur, il fait juste ça:

for mot in extraire_mots(dossier):
    print mot

Et pour lui c’est transparent. En plus, il peut utiliser tous les outils qu’on utilise sur les itérables d’habitude. Toutes les fonctions qui acceptent les itérables acceptent donc le résultat de la fonction en paramètre grâce à la magie du duck typing. On créé ainsi une merveilleuse toolbox.

Controller yield

>>> class DistributeurDeCapote():
    stock = True
    def allumer(self):
        while self.stock:
            yield "capote"
...

Tant qu’il y a du stock, on peut récupérer autant de capotes que l’on veut.

>>> distributeur_en_bas_de_la_rue = DistributeurDeCapote()
>>> distribuer = distributeur_en_bas_de_la_rue.allumer()
>>> print distribuer.next()
capote
>>> print distribuer.next()
capote
>>> print([distribuer.next() for c in range(4)])
['capote', 'capote', 'capote', 'capote']

Dès qu’il n’y a plus de stock…

>>> distributeur_en_bas_de_la_rue.stock = False
>>> distribuer.next()
Traceback (most recent call last):
  File "<ipython-input-22-389e61418395>", line 1, in <module>
    distribuer.next()
StopIteration
< type 'exceptions.StopIteration'>

Et c’est vrai pour tout nouveau générateur:

>>> distribuer = distributeur_en_bas_de_la_rue.allumer()
>>> distribuer.next()
Traceback (most recent call last):
  File "<ipython-input-24-389e61418395>", line 1, in <module>
    distribuer.next()
StopIteration

Allumer une machine vide n’a jamais permis de remplir le stock ;-) Mais il suffit de remplir le stock pour repartir comme en 40:

>>> distributeur_en_bas_de_la_rue.stock = True
>>> distribuer = distributeur_en_bas_de_la_rue.allumer()
>>> for c in distribuer :
...     print c
capote
capote
capote
capote
capote
capote
capote
capote
capote
capote
capote
capote
...

itertools: votre nouveau module favori

Le truc avec les générateurs, c’est qu’il faut les manipuler en prenant en compte leur nature: on ne peut les lire qu’une fois, et on ne peut pas déterminer leur longeur à l’avance. itertools est un module spécialisé là-dedans: map, zip, slice… Il contient des fonctions qui marchent sur tous les itérables, y compris les générateurs.

Et rappelez-vous, les strings, les listes, les sets et même les fichiers sont itérables.

Chaîner deux itérables, et prendre les 10 premiers caractères ? Piece of cake !

>>> import itertools
>>> d = DistributeurDeCapote().allumer()
>>> generateur = itertools.chain("12345", d)
>>> generateur = itertools.islice(generateur, 0, 10)
>>> for x in generateur:
...     print x
...     
1
2
3
4
5
capote
capote
capote
capote
capote

Les dessous de l’itération

Sous le capot, tous les itérables utilisent un générateur appelé “itérateur”. On peut récupérer l’itérateur en utiliser la fonction iter() sur un itérable:

>>> iter([1, 2, 3])
< listiterator object at 0x7f58b9735dd0>
>>> iter((1, 2, 3))
< tupleiterator object at 0x7f58b9735e10>
>>> iter(x*x for x in (1, 2, 3))
< generator object  at 0x7f58b9723820>

Les itérateurs ont une méthode next() qui retourne une valeur pour chaque appel de la méthode. Quand il n’y a plus de valeur, ils lèvent l’exception StopIteration:

>>> gen = iter([1, 2, 3])
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
3
>>> gen.next()
Traceback (most recent call last):
  File "< stdin>", line 1, in < module>
StopIteration

Message à tous ceux qui pensent que je fabule quand je dis qu’en Python on utilise les exceptions pour contrôler le flux d’un programme (sacrilège !): ceci est le mécanisme des boucles internes en Python. Les boucles for utilisent iter() pour créer un générateur, puis attrappent une exception pour s’arrêter. À chaque boucle for, vous levez une exception sans le savoir.

Pour la petite histoire, l’implémentation actuelle est que iter() appelle la méthode __iter__() sur l’objet passé en paramètre. Donc ça veut dire que vous pouvez créer vos propres itérables:

>>> class MonIterableRienQuaMoi(object):
...     def __iter__(self):
...         yield 'Python'
...         yield "ça"
...         yield 'déchire'
...
>>> gen = iter(MonIterableRienQuaMoi())
>>> gen.next()
'Python'
>>> gen.next()
'ça'
>>> gen.next()
'déchire'
>>> gen.next()
Traceback (most recent call last):
  File "< stdin>", line 1, in < module>
StopIteration
>>> for x in MonIterableRienQuaMoi():
...     print x
...
Python
ça
déchire
]]>
http://sametmax.com/comment-utiliser-yield-et-les-generateurs-en-python/feed/ 14
Les context managers et le mot clé with en Python 13 http://sametmax.com/les-context-managers-et-le-mot-cle-with-en-python/ http://sametmax.com/les-context-managers-et-le-mot-cle-with-en-python/#comments Mon, 03 Sep 2012 17:56:43 +0000 http://sametmax.com/?p=1987 with est utilisé comme dans aucun autre langage en Python. Au premier abord mystérieux, il agit en fait comme les décorateurs en permettant d'exécuter du code automatiquement avant et après un autre code. Mais à l'image des décorateurs, tout ce qu'il fait pourrait être écrit à la main sans utiliser le mot clé with. Utiliser with est une question de style. ]]> Le mot clé with est utilisé comme dans aucun autre langage en Python. Au premier abord mystérieux, il agit en fait comme les décorateurs en permettant d’exécuter du code automatiquement avant et après un autre code. Mais à l’image des décorateurs, tout ce qu’il fait pourrait être écrit à la main sans utiliser le mot clé with. Utiliser with est une question de style.

Supposons que vous vouliez afficher quelque chose avant un bout de code, et après un bout de code, même si celui-ci rate. Vous feriez quelque chose comme ça:

def truc():
    print "machin"
 
print "Avant"
try:
    truc()
finally:
    print "Après"

Et ça va afficher:

Avant
machin
Après

Et avec:

def truc():
    print "machin"
    raise Exception('Fail !')

‘Après’ sera quand même affiché. Ça plantera, mais la dernière action sera toujours faite.

Si vous le faites souvent, vous voudrez factoriser du code. Un des moyens de le faire est d’utiliser les context managers.

Créer son propre context manager

Un context manager est une classe ordinaire en Python. Sa seule spécificité est de déclarer une méthode __enter__() et une méthode __exit__(). Ces méthodes sont des méthodes ordinaires, leur nom spécial est juste là par convention, et en les nommant ainsi on s’assure qu’elles seront détectées et utilisées automatiquement.

Notre code là haut peut donc se réécrire ainsi:

class MonSuperContextManager(object):
    def __enter__(self):
        print "Avant"
    def __exit__(self, type, value, traceback):
        # faites pas attention aux paramètres, ce sont toutes les infos
        # automatiquement passées à __exit__ et qui servent pour inspecter
        # une éventuelle exception
        print "Après"
 
with MonSuperContextManager():
    truc()

L’avantage de with est multiple:

  • Il permet de visualiser très précisément où on entre dans l’action et où on en sort (c’est un seul block)
  • Il permet de réutiliser les actions faite à l’entrée et à la sortie de l’action.
  • Même si une exception est levée, l’action de sortie sera exécutée juste avant le plantage. __exit__ est en effet garantie d’être appelée quoiqu’il arrive. Bon, évidement, si il y a une coupure de courant…

En gros, créer un context manager, c’est faire un raccourci lisible pour try/finally. Point.

Un exemple utile de context manager

Supposons que vous ayez beaucoup de travail à faire dans plein de dossiers. Vous voulez vous assurer que vous allez dans le dossier de travail, puis que vous retournez au dossier initial à chaque fois.

import os
 
class Cd(objet):
    def __init__(dirname):
        self.dirname = dirname
    def __enter__(self):
        self.curdir = os.getcwd()
        os.chdir(self.dirname)
    def __exit__(self, type, value, traceback):
        os.chdir(self.curdir)

On l’utilise comme ça:

# ici on est dans /home/moi
 
with Cd('/'):
 
    # faire un truc dans /
 
    with Cd('/opt'):
 
        # faire un truc dans /opt
 
    # ici on est dans /
 
# ici on est dans /home/moi

C’est d’ailleurs ce que fait fabric.

Le mot clé as

Tout ce qu’on retourne dans __enter__ peut être récupéré grâce au mot clé as. Imaginons un context manager qui permette d’ouvrir un fichier et de le fermer automatiquement:

class OpenFile(objet):
    def __init__(filename, mode='r'):
        self.filename = filename
        self.mode = mode
    def __enter__(self):
        self.file = open(self.filename, self.mode)
        # ici on retourne l'objet fichier, il sera accessible avec "as"
        return self.file
    def __exit__(self, type, value, traceback):
        self.file.close()

On l’utilise comme ceci:

with OpenFile('/etc/fstab') as f:
    for line in f:
        print line

f va contenir ici l’objet fichier, car nous l’avons retourné dans __enter__. A la fin du bloc with, le fichier sera fermé automatiquement.

Et devinez quoi, Python possède déjà un context manager qui fait ça:.

with open(vot_fichier_msieu_dames) as f:
   # faire un truc

Context managers sous forme de fonctions

Faire les choses sous forme de classes, c’est pratique quand on a beaucoup de logique à encapsuler. Mais la plupart des context managers sont très simples. Pour cette raison, Python vient avec plein d’outils pour se simplifier la vie avec with dans un module judicieusement nommé contextlib.

Pour l’utiliser, il faut avoir des notions sur les décorateurs, et le mot clé yield. Si ce n’est pas votre cas, restez sur la version sous forme de classe :-)

Supposons que l’on veuille recréer le context manager open:

from contextlib import contextmanager
 
@contextmanager
def open(filename, mode):
    try:
        f = open(filename, mode)
        yield f
    finally:
        f.close()

Bon, c’est simplifié, hein, le vrai est plus robuste que ça.

Comment ça marche ?

D’abord, on utilise le décorateur @contextmanager pour dire à Python que la fonction sera un context manager.

Ensuite, on fait un try/finally (il est pas automatique comme avec __enter__ et __exit__).

yield sépare le code en deux: tout ce qui est avant est l’équivalent de __enter__, tout ce qui est après est l’équivalent de __exit__. Ce qui est “yieldé” est ce que l’on récupère avec le mot clé as.

Context manager et décorateur, le shampoing deux en un

Ces deux fonctionnalités se ressemblent beaucoup: elles permettent toutes les deux de lancer du code automatiquement avant et après un code tiers. La seule différence est que le context manager le fait à la demande, alors que le décorateur s’applique à la définition d’une fonction.

Quand on sait comment ils marchent, il est facile de faire un context manager utilisable également en tant que décorateur.

from functools import wraps
 
class ContextDecorator(object):
    # __call__ est une méthode magique appelée quand on utilise () sur un objet
    def __call__(self, f):
        # bon, cette partie là suppose que vous savez comment marche un
        # décorateur, si c'est pas le cas, retournez lire l'article sur S&amp;M
        # linké dans le premier paragraphe
        @wraps(f)
        def decorated(*args, **kwds):
            # notez le with appelé sur soi-même, c'est y pas mignon !
            with self:
                return f(*args, **kwds)
        return decorated

Et voilà, il suffit d’hériter de ça, et on a un décorateur + context manager. Par exemple, si on veut timer un truc:

import datetime
 
class TimeIt(ContextDecorator):
 
    def __enter__(self):
        self.start = datetime.datetime.now()
        print self.start
 
    def __exit__(self, type, value, traceback):
        print (datetime.datetime.now() -self.start).total_seconds()

Timer juste un appel:

def foo():
    # faire un truc
 
with TimeIt():
    foo()

Timer tous les appels:

@TimeIt()
def foo():
   # faire un truc

Notez que ContextDecorator est présent par défaut dans le module contextlib sous Python 3.2.

]]>
http://sametmax.com/les-context-managers-et-le-mot-cle-with-en-python/feed/ 13
Astuces Python en vrac 8 http://sametmax.com/astuces-python-en-vrac/ http://sametmax.com/astuces-python-en-vrac/#comments Fri, 03 Aug 2012 13:15:35 +0000 http://sametmax.com/?p=1463 Je n’arrive pas à trouver un lien entre tous ces trucs, alors un bon vrac fera l’affaire.

Float accepte de parser l’infini

>>> infini = float('inf')
>>> infini
inf
>>> infini + 1
inf
>>> infini - 1
inf
>>> float('-inf')
-inf
>>> float('-inf') + 1
-inf
>>> float('inf') + float('-inf')
nan

Attention, il est très probable que ce soit dépendant de l’implémentation CPython.

Iter() peut prendre un callable en argument

iter(), c’est la fonction qui créé un générateur à partir d’un itérable:

>>> generateur = iter([1, 2, 3])
>>> generateur.next()
1
>>> generateur.next()
2
>>> generateur.next()
3

Il se trouve qu’il a aussi une autre forme: iter(callable, sentinel).

Sous cette forme, il va créer une générateur qui appelle callable jusqu’à ce que la valeur sentinel apparaisse.

>>> import datetime
>>> datetime.datetime.now().strftime("%S")
'45'
>>> generateur = iter(lambda: datetime.datetime.now().strftime("%S"), "59")
>>> generateur.next()
'56'
>>> generateur.next()
'57'
>>> generateur.next()
'58'
>>> generateur.next()
Traceback (most recent call last):
  File "<ipython-input-45-6c9f9efdd35c>", line 1, in <module>
    generateur.next()
StopIteration

Chainer les comparateurs

Histoire de diminuer le nombre de if:

>>> a, b, c, d = 1, 2, 3, 4
>>> a < b < c < d
True
>>> a == 1 > b -2
True

Le mot clé else, en dehors de if

Else ne s’applique pas qu’aux conditions, mais aussi aux exceptions et aux boucles.

Dans une boucle for, la clause else est exécutée à la fin de l’itération si il n’y a pas eu de break.

>>> import random
>>> lst = [random.randint(1, 5) for x in xrange(5)]
... for x in lst:
...     if x == 5:
...         break
... else:
...     print "5 n'a jamais été trouvé"

Dans la gestion des exceptions, else est éxécuté si catch n’est jamais appelé:

try:
    open('fichier')
except IOError:
    print "Une IO Error est arrivée"
else:
    print "Tout s'est bien passé"
finally:
    print "Toujours éxécuté"
 
print "Exécuté si on est rentré dans except ou else"

Continuation de lignes avec les parenthèses

\ sur les longues lignes, ça va 5 minutes.

from force import (rouge, bleu, jaune, vert, noir, blanc, rose, fushia,
                   vermeille, vers_a_pois_jaune, call)
 
# ici le multi line édit de sublime text m'a vachement aidé
if (rouge.transformation and bleu.transformation and jaune.transformation
        and vert.transformation and noir.transformation and blanc.transformation
        and rose.transformation and fushia.transformation
        and  vermeille.transformation and vers_a_pois_jaune.transformation):
    print ("C'est bon là je crois qu'on a tout le monde sauf erreur de ma part"
           " dans le comptage... Ah non merde il manque jaune devant marron "
           "derrière")
    call(force='jaune_devant_marron_derriere', message="Ramène ton cul tout"
                                                       "de suite !")

Les extensions .pyw, .pyo et .pth

Vous croyiez qu’il n’y avait que .py et .pyc dans la vie ? Ah, jeunes padawans…

.pyw est juste un renommage, il permet, sous Windows, de ne pas ouvrir de terminal quand on lance le script (ce qui est préférable quand on a déjà une UI)

.pyo est l’extension générée quand on lance la commande python avec l’option -o (optimize). Pour le moment il retire juste les assert.

.pth est l’extension qu’on donne à un simple fichier texte qui contient une liste de chemins de dossiers. Posé à la racine d’un site directory, il dit à Python de rajouter automatiquement ces dossiers au Python Path, ce qui évite de manipuler sys.path.

Lever une exception à nouveau

Il suffit d’utiliser raise sans paramètre. Pratique quand on ne veut pas interrompre la remontée d’exception, et insérer un traitement juste avant que l’exception se déclenche (en opposition à finally qui garanti le traitement, pas le moment de celui-ci.)

try:
    open('fichier')
except IOError:
    print "pouet"
    raise

Passer une valeur à un générateur

Vous aimez yield ? Vous en abusez ? Sachez qu’on peut faire plus vicieux encore:

def je_te_yield_tu_me_yield(lst):
    for x in lst:
        nouvelle_liste = yield x
        if nouvelle_liste is not None:
            for z in nouvelle_liste:
                print z

Le truc tordu est qu’ici yield est dans un assignement. Non seulement il retourne une valeur, mais en plus il en récupère une. send() permet de passer une valeur à yield à son prochain retour (sinon la valeur reçue est toujours None):

>>> generateur = je_te_yield_tu_me_yield([1, 2, 3])
>>> generateur.next()
1
>>> generateur.next()
2    
>>> generateur.send(('a', 'b', 'c'))
a
b
c
3

On peut inverser les booléens avec 1/0 et vice-versa

Les booléens ne sont qu’une surcouche des entiers, et même si:

>>> type(1) 
<type 'int'>
>>> type(bool)
<type 'type'>

Dans la pratique:

>>> 1 == True
True
>>> 0 == False
True
>>> 2 == True
False

On peut donc les interchanger:

>>> True + 1 # pas sur que ce soit utile
2
>>> lst = ['a', 'b'] # par contre ça c'est cool pour les binary trees
>>> lst[False]
'a'
>>> lst[True]
'b'
>>> fin_de_phrase = True # et ça c'est TRES pratique pour le formating
>>> print "... d'une longue histoire" + ("." * fin_de_phrase)
... d'une longue histoire.
>>> fin_de_phrase = False
>>> print "... d'une longue histoire" + ("." * fin_de_phrase)
... d'une longue histoire

_ contient la dernière sortie sur le shell

>>> _
'b'
>>> 1 + 1
2
>>> _
2
>>> _ + 1
3
>>> a = [1, 2, 3]
[1, 2, 3]
>>> _.append(4)
>>> a
[1, 2, 3, 4]

Ca ne marche que dans le shell, et uniquement sur ce qui est affiché à l’écran. Si vous n’affichez pas la valeur, _ ne change pas. Attention à ne pas trop compter sur _ car il est très volatile.

Enumerate() accepte un index de départ

>>> for i, elem in enumerate('azerty'):
...     print i, elem
...     
0 a
1 z
2 e
3 r
4 t
5 y
>>> for i, elem in enumerate('azerty', 10):
    print i, elem
...     
10 a
11 z
12 e
13 r
14 t
15 y

On peut assigner et supprimer des slices

>>> a = range(10)
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[:5] = [42]
>>> a
[42, 5, 6, 7, 8, 9]
>>> a[:1] = range(5)
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> del a[::2]
>>> a
[1, 3, 5, 7, 9]
>>> a[::2] = a[::-2]
>>> a
[9, 3, 5, 7, 1]

import braces

Si vous n’aimez pas les espaces pour l’indentation, vous pouvez faire:

from __future__ import braces
]]>
http://sametmax.com/astuces-python-en-vrac/feed/ 8
Concurrence sans threads en python 10 http://sametmax.com/concurrence-sans-threads-en-python/ http://sametmax.com/concurrence-sans-threads-en-python/#comments Sat, 28 Jul 2012 00:51:37 +0000 http://sametmax.com/?p=1373 Ceci est un post invité de poulpe posté sous licence creative common 3.0 unported.

Je parie que là, maintenant, vous êtes en train de ne pas vous demander “Comment pourrais-je exécuter des actions concurrente sans utiliser de threads en python ?”. Et c’est bien dommage pour vous car la seule chose que j’ai à vous écrire c’est un début de réponse à cette question.

Pourquoi faire ?

Ouep, les threads c’est pas toujours la joie. Au rayon des inconvénients, on retrouve souvent complexité de conception et de debuggage, librairies externes pas toujours thread safe, dégradation des perfs, aucun contrôle sur la granularité de l’exécution, risques liés aux locks pour toute la partie “Atomicité” etc.
Bon tout n’est quand même pas noir, et dans la majorité des cas, un petit coup de threads sera le plus pratique pour faire ce que vous voulez. Mais si votre besoin est vraiment particulier (ou que vous vous ennuyez beaucoup pendant les vacances) voici une solution assez élégante, qui vous laisse le contrôle absolu (MOUHAHAHA) et qui nécessite souvent peu de modifications de votre code existant.

Comment faire ?

Pour faire ça, on va utiliser les générateurs que vous connaissez bien.
Quoi de mieux qu’un petit exemple pour commencer :

  • Paul veut afficher de façon régulière le mot “Loutre”.
  • Jacques, lui, voudrait afficher de la même façon le mot “Tarentule”.
  • Un mec bizarre que personne ne connait désire écrire “Musaraigne”.

Comme ils sont tous les trois très cons et qu’ils sont incapables de se mettre d’accord pour savoir qui commence, ils décident de faire ça tous en même temps. Problème, ils veulent tous afficher leur mot selon une temporisation bien précise sans se gêner les uns les autres.

Voici donc la fonction que chacun de nos trois zoophiles veut utiliser. Vous remarquerez que le seul truc qu’elle possède de spécial, c’est le petit yield à la fin de chaque itération. C’est moi qui ai décidé de le rajouter arbitrairement à cet endroit (parce que c’est moi le chef). C’est en effet la seule modification à apporter à la fonction pour la rendre “éclatable”.

def afficher_un_truc_regulierement(truc, delai, nombre):
    """Affiche un "truc" tous les "delais" un certain "nombre" de fois"""
    import time
    derniere_occur = time.time()
    num = 0
    while num < nombre:
        maintenant = time.time()
        if maintenant - derniere_occur > delai:
            derniere_occur = maintenant
            print str(num) + " : " + truc
            num += 1
        yield     # Je rajoute mon(mes) yield(s) où je veux.

Le placement du yield est important, tous les traitements entre deux yields seront exécutés de façon atomique. Dans le reste de ce tuto, j’appellerai ce groupe de traitements atomique une granule (j’aime bien le mot).
Dès l’ajout du mot-clé yield dans le corps, notre fonction retourne un générateur au lieu de s’exécuter normalement.

On crée ensuite une liste d’actions à effectuer de façon concurrente. Chaque action est un générateur retourné par l’appel à la fonction.

liste_des_actions = []
#Paul :
liste_des_actions.append(afficher_un_truc_regulierement("Loutre", 4, 4))
#Jacques :
liste_des_actions.append(afficher_un_truc_regulierement("Tarentule", 5, 3))
#Le mec bizarre
liste_des_actions.append(afficher_un_truc_regulierement("Musaraigne", 3, 3))

Voici enfin le mécanisme qui permet d’exécuter tout ce beau bordel. Il est assez générique et le code parle de lui même :)

while True:     # Boucle infinie
    if len(liste_des_actions):     # Si il reste des actions
        #On itère sur une copie de la liste (avec [:])
        #pour pouvoir modifier la liste pendant la boucle
        for action in liste_des_actions[:]:
            try:
                action.next()     # On execute une granule
            except StopIteration:
                #Il n'y a plus de granule dans cette action
                #On enlève donc l'action de la liste
                liste_des_actions.remove(action)
    else:
        #Plus aucune action, on finit la boucle infinie
        break
print "Tout est bien qui finit bien."

Ici, l’exemple est simpliste mais on peut l’adapter à des fonctions beaucoup plus complexes et nombreuses, qui ne se présentent pas forcement sous forme de boucle.

Comment faire mieux ?

Je vous laisse avec une piste d’évolution possible qui est assez amusante à implémenter (on rigole avec ce qu’on peut, hein). On peut facilement imaginer un système de priorité dynamique entre les actions. En effet, ici, on ne yield aucune valeur, mais on peut décider d’utiliser le nombre X yieldé (et donc retourné par action.next()) pour sauter les X prochains appels à cette action, ce qui aura pour effet de réduire la priorité de celle-çi par rapport aux autres.

Voilou, j’espère que vous n’utiliserez jamais ça dans du code collaboratif (ou alors si vous n’aimez pas vos collaborateurs à la limite) mais que le jour où vous aurez ce besoin particulier, vous saurez quoi faire.

]]>
http://sametmax.com/concurrence-sans-threads-en-python/feed/ 10