Site original : Sam & Max: Python, Django, Git et du cul
Si vous vous souvenez, on ne peut pas mettre un antislash à la fin d’une raw string :
>>> print(r'Moi pouvoir\\') Moi pouvoir\\ >>> r'Moi pouvoir\\' 'Moi pouvoir\\\\' >>> r'Moi pouvoir\' File "<stdin>", line 1 r'Moi pouvoir\' ^ SyntaxError: EOL while scanning string literal
La plupart du temps on s’en branle.
Mais supposons que vous vouliez créer un path pour disons, pauvre de vous, une commande DOS…
>>> print("\chemin\vers\dossier\dos\\") \chemin ers\dossier\dos\ >>> print(r"\chemin\vers\dossier\dos\\") \chemin\vers\dossier\dos\\
Ah, ah, te voilà bien baisé, cher programmeur !
Sauf que non, car une raw string n’est pas un type de string particulier, comme on l’a vu ici, donc on peut faire ça :
>>> print(r"\chemin\vers\dossier\dos" + "\\") \chemin\vers\dossier\dos\
Zooooo !
Et même si on se sent chaud, on peut utiliser la concaténation implicite pour se la jouer mega chaud du slip :
>>> print(r"\chemin\vers\dossier\dos" "\\") \chemin\vers\dossier\dos\
Qu’il en faut peu pour qu’un dev se sente le roi du monde…
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 :
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…
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 :
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 :
x[2]
, x['cle']
…)int()
, str()
, tuple()
, etcMais 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 :
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 à …
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 :
__eq__
.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 ?
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.
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.
Hier, j’ai dû pusher du Mac Do. Parce que je suis dans un pli d’une circonvolution du sphincter du trou du cul du monde, à savoir chez mes grands-parents. Inutile de dire que j’avais un meilleur accès Internet à Kisumu, au Kenya. Si, si.
Bien entendu, le Mac wifi est annoncé comme “Internet gratuit Illimité”, ce qui en langage commercial ignorant signifie une partie du Web censurée et les ports de emails ouverts si vous nous achetez un maxi best of.
Donc pas de SSH.
Heurement Github est grand, Github est beau, et Github accepte les push via HTTPS.
Modifiez juste une ligne dans votre fichier .git/config de :
[remote "origin"] url = git@github.com/votre_pseudo/votre_repo.git
Vers :
[remote "origin"] url = https://github.com/votre_pseudo/votre_repo.git
La commande git devrait vous prompter pour username (mettez votre email) et mot de passe, et hop : le monde du télétravail à la campagne s’ouvre à vous.
Ouaaa, j’ai passé la journée sur ce truc. Il y avait beaucoup de texte pour une fois, alors la traduction anglais => français a pris pas mal de temps. C’est fou ce qu’on peut faire comme trucs chiants quand on a pas Internet…
Bref, je vous poste la nouvelle app de Django, une app à la fois depuis le Mac Do, parce que c’est le seul truc à avoir un Wifi dans le coin paumé où je me trouve.
Machez bien, c’est assez dense.
P.S: ouinnnn, ils font plus le wrap chèvre.
Cet article s’adresse à des développeurs qui commencent à être bien dans leurs chaussettes en Python et qui se sentent de relever le défi d’écrire du code pour d’autres personnes.
Car quand vous allez publier du code, un script, une lib voire, Dieu ait pitié de vous, un framework, les gens qui vont les utiliser vont avoir des besoins auxquels vous n’aviez pas pensé. Or, ils ne voudront pas modifier votre code. L’interêt d’utiliser le code d’un autre, c’est justement de s’éviter la maintenance. Ils voudront aussi prendre en main rapidement votre outil pour faire des choses simples sans avoir à tout comprendre.
Ainsi, il va vous falloir intégrer divers moyens d’étendre les fonctionnalités de votre code, afin que les autres dev puissent l’utiliser dans le cadre de leurs besoins.
Nous allons donc construire un projet bidon : une barre de progression en ASCII dont la sortie va ressembler à ceci :
Starting [========================================= ] 93%
L’idée est de pouvoir l’utiliser ainsi :
with ProgressBar() as pb: for progres in faire_un_truc(): pb.progress = progres
Nous allons le proposer ce projet sous forme de classe importable, et nous allons voir plusieurs manières de rendre ce code facile à prendre en main, configurable et extensible :
J’aurais pu ajouter l’injection de dépendance, mais j’ai déjà traité ce sujet dans un autre article, et je ne voulais pas charger celui-ci. Il est déjà bien gras.
J’ai aussi volontairement omis la docstring et les comments. Écrire une bonne doc est un article à part entière.
Pré-requis : être parfaitement à l’aise avec le POO et les références, comprendre le principe des context managers, avoir pigé les callbacks.
Et roulez jeunesse !
# -*- coding: utf-8 -*- import sys from io import IOBase # On hérite d'IOBase pour permettre le détournement de stdout. Voir plus bas. class ProgressBar(IOBase): # On met les paramètres par ordre de fréquence d'utilisation. Il est # plus probable que les programmeurs changent le total que la sortie # du programme. # Mettre des valeurs par défaut saines permet à l'utilisateur de faire # des essais sans trop regarder la doc. Par ailleurs, des valeurs par # défaut servent de documentation en soit, et apparaitront si help() # est appelé. def __init__(self, total=100, percent_per_sign=1, progress_sign='=', callbacks=(), template='Starting [{bar}] {progress}%', output=sys.stdout, intercept_stdout=True): # Par défaut le total est 100, afin que l'utilisateur passe un # pourcentage, ce qui est le plus naturel. Néanmoins, si il souhaite # s'affranchir de calculs, on lui permet de passer une autre valeur # qui sera ramenée à un pourcentage automatiquement à l'affichage # de toute façon. self.total = total # Customisation de base : changer le symbole de progrès. Par défaut # un égal, mais certains aiment les ., les - ou autre. self.progress_sign = progress_sign # Idem, si la personne veut une barre plus ou moins longue. self.percent_per_sign = percent_per_sign # la longueur de la barre de progression self.bar_len = 100 / percent_per_sign * len(progress_sign) # Le formatage de la progress bar se fait à l'aide d'une chaîne # ordinaire qui sert de template (avec le langage de formatage de Python) # et peut facilement être changée pour obtenir plus de contrôle # sur l'aspect de la bar. self.template = template # On peut aussi passer un tuple de fonctions qui permettent de réagir # à chaque fois que le progrès change. On utilise nous-même notre # système de callback pour que la methode print_progress() soit # appelée à chaque fois, mettant à jour l'affichage de la bar. self.callbacks = callbacks + (self.print_progress,) # Initialisation de 3 variables à vide. 'buffer' va contenir # tout ce qui va être écrit avec write() pour éviter de perturber # l'affichage de la bar, et '_progress' va contenir le progrès, mais # le '_' signale que c'est une variable à usage interne. Nous allons # en effet l'enrober dans une propriété. Enfin cursor_shift est # le nombre de caractères à effacer pour redessiner la bar à chaque # mise à jour. self.buffer = '' self._progress = 0 self.cursor_shift = 0 # Par défaut, on s'attend à ce que l'utilisateur affiche cette # bar directement dans le terminal, sur la sortie standard. Mais # il peut vouloir déporter l'affichage ailleurs (par exemple stderr). # Pour cette raison, on permet de passer le stream sur lequel # l'utilisateur va écrire, même si par défaut on prend sys.stdout, # donc la sortie standard. # Si l'utilisateur ne souhaite rien de tout cela, il peut également # retirer 'self.print_progress' de la liste des callbacks puisque # l'attribut est public. self.out = output # Comme on utilise la sortie standard par défaut, la barre peut # facilement être cassée par un autre code écrivant aussi sur la sortie # standard. Puisqu'un débutant ne comprendra pas rour se suite ce qui se # passe, par défaut on va intercepter stdout et mettre tout ce qui est # écrit dessus dans un buffer. Si jamais il y a des prints fait durant # l'affichage, il seront cachés, et stockés. Ce comportement n'est pas # forcément désirable, et peut donc être désactivé par un paramètre pour # l'utilisateur qui sait ce qu'il fait notamment dans le cadre # d'utilisation des threads. self._intercept_stdout = intercept_stdout and self.out == sys.stdout if self._intercept_stdout: # L'interception de la sortie standard se fait en remplaçant # sys.stdout par soi-même. C'est pour cette raison que ProgressBar # hérite de IOBase. En effet, de cette manière, on possède # l'interface d'un objet stream et tout écriture sur 'self' semblera # fonctionner comme sur sys.stdout. Cela permet de faire des prints # ou de lancer un shell et d'utiliser la barre, bien que # dans un shell cela monopolisera le prompt. sys.stdout = self # Afficher la barre à la création de l'objet retire de la flexibilité à # l'utilisateur qui peut vouloir le créer d'un côté, le stocker, et # démarrer l'affichage plus tard. L'affichage est donc conditionné # par cette méthode. def show(self): bar = self.format() self.out.write(bar) self.cursor_shift = len(bar) return self # Comme le plus souvent on voudra tout de même afficher la barre juste après # la création, on transforme cette classe en context manager. Pour cela on # créer un alias de la method show() qu'on appelle __enter__, puisque tout # objet qui a une méthode nommée __enter__ peut être utilisé comme context # manager. Si vous ne vous souvenez pas de ce que c'est, il y a un article # sur le blog à ce sujet. Mais en résumé, ce sont les objets utilisables # avec "with". # Notez qu'on ne fait pas __enter__ = show, ce qui serait un moyen plus # court d'aliaser, car il empêcherait __enter__ d'appeler le bon show() # en cas d'héritage, si show() est écrasé. def __enter__(self): return self.show() # Une petit méthode de nettoyage, qui ici va simplement remettre sys.stdout # à sa place. def stop(self, *args, **kwargs): if self._intercept_stdout: sys.stdout = self.out # Même topo, on alias stop en __exit__ pour avoir la sortie du context # manager. On a pas appelé directement ces méthodes __enter__ et __exit__ # car l'utilisateur peut vouloir les appeler manuellement. def __exit__(self, *args, **kwargs): return self.stop() # Une simple propriété qui donne accès au progres en lecture... @property def progress(self): return self._progress # ... et en écriture. La différence étant qu'à l'écriture, on vérifie # que la valeur est bien comprise entre 0 et le total. On appelle aussi # les callbacks, et donc l'affichage se mettra à jour. @progress.setter def progress(self, value): if not 0 <= value <= self.total: raise ValueError("'value' is %s and should be set between 0 " "and 'total' (%s)" % (value, self.total)) # On le fait avant car les callbacks doivent être appelés # avec un état propre. previous_progress = self._progress self._progress = value # Ce que l'on va passer en paramètres aux callbacks est un choix # important. Déjà en premier paramètre, on passe self. Ainsi le callback # aura accès à presque tout, et on est certain qu'il ne se trouvera # pas dépourvu d'informations. Ensuite on passe le progrès. Normalement # il peut le récupérer à travers self.progress, mais comme on sait # que c'est une information très utilisée, on la passe par politesse, # pour faciliter la vie de l'utilisateur. Enfin, une information qu'il # ne pourrait pas avoir autrement est le progrès précédent. Bien que # nous ne l'utilisons pas dans notre callback, on peut imaginer que # c'est le genre d'info qui peut être utile, et qui ne peut être trouvée # dans self. for callback in self.callbacks: callback(self, self._progress, previous_progress) # Si on arrive au bout, on appelle stop() automatiquement. Stop() # étant idempotente, ce n'est pas grave si elle est appelée plusieurs # fois. if value == self.total: self.stop() # Un cas intéressant : pourquoi on ne fait pas self.template directement au # lieu de créer une property à vide ? Tout simplement parce qu'un template # est typiquement quelque chose que quelqu'un peut vouloir créer # dynamiquement (par exemple pour y ajouter une notion de temps qui passe). # Donc, on propose de passer le template en paramètre dans __init__ car la # plupart du temps, c'est juste ce qu'on voudra faire : un simple changement # cosmétique statique. Mais afin de permettre plus d'extensibilité, on en # fait une propriété, qui, comme toute méthode, peut être écrasée avec de # l'héritage et permettrait à l'utilisateur de vraiment personnaliser son # affichage de manière poussée. @property def template(self): return self._template @template.setter def template(self, value): self._template = value # C'est cette méthode qui est appelée si on a détourné sys.stdout quand # l'utilisateur va faire un print. Elle doit s'appeler write(), car c'est # l'interface connue des objets stream. Grosso modo, on va juste # tout stocker dans une variable plutôt que d'afficher quoique ce soit def write(self, value): self.buffer += value # Pour une progrès donné, retourne la barre de progrès sous forme de # string ASCII. Elle est mise à part car elle peut ainsi être facilement # utilisée dans un callback. def format(self, progress=0): progress = progress * 100 / self.total bar = progress / self.percent_per_sign * self.progress_sign bar += (self.bar_len - len(bar)) * ' ' return self.template.format(bar=bar, progress=progress) # Notre callback que l'on utilise pour afficher la barre. C'est une méthode # statique car cela nous permet de nous mettre dans les conditions exacte # d'un callback d'un utilisateur, qui ne sera qu'une fonction, sans self. @staticmethod def print_progress(progress_bar, progress, previous_progress): # '\b' permet de reculer le curseur dans le terminal, ce qui va # nous permettre de réécrire par dessus l'ancienne bar, et la mettre # à jour. Si vous tenez à faire un display multi ligne, sachez que '\b' # ne permet pas de reculer sur un saut de ligne, pour ça il faut # utiliser '\033[1A' progress_bar.out.write(progress_bar.cursor_shift * '\b') bar = progress_bar.format(progress) progress_bar.out.write(bar) # flush est nécessaire pour vider le buffer et obtenir un affichage # immédiat quand on écrit en direct sur un stream. progress_bar.out.flush() # on met à jour l'avancement du curseur, qui nous permettra de reculer # d'autant au prochain print_progress() progress_bar.cursor_shift = len(bar) if __name__ == "__main__": # voici une utilisation standard de la barre : import time total = 1000 # L'utilisation du context manager permet de ne pas se soucier d'appeler # show() ou stop() et autre détails d'implémentation. # On note aussi qu'avoir des valeurs par défaut pour l'initialisation de la # barre la rend très facile à utiliser sans trop se prendre la tête, et # ce malgré un code derrière assez complexe. On aurait même pu se passer # de "total". with ProgressBar(total) as pb: for i in range(total): time.sleep(0.001) # on voir l'interêt d'utiliser une property ici, ça rend la mise # à jour du progrès très simple pb.progress = i if i == total / 2: print("\nHalf the work already !") half = True if i == total * 0.7: print("Almost done") almost = True pb.progress = total # On peut afficher tout ce qui a été printé durant la progression de la # barre si besoin. print(pb.buffer) from datetime import datetime # Et une utilisation custo où on compte les secondes depuis le départ # et on les affiches dans le template class MaProgressBar(ProgressBar): def show(self): self.start = datetime.now() return super(MaProgressBar, self).show() # Comme on a template en tant que méthode, on peut l'overrider et # obtenir un comportement @property def template(self): seconds = (datetime.now() - self.start).seconds return '{bar} (running for %s seconds)' % seconds @template.setter def template(self, value): pass # Notre système de callback va se trouver utile : # on met un callback de plus qui va écrire dans un fichier (mais # il pourrait faire n'importe quoi, envoyer un post sur un site Web, # un mail, formatter le disque...) def update_progress(progress_bar, progress, previous_progress): with open('/tmp/progress.log', 'w') as log: log.write(str(progress)) # On change aussi le signe de progrès pour quelque chose de plus pro pb = MaProgressBar(progress_sign=':-) ', percent_per_sign=10, callbacks=(update_progress,)) # Au besoin, on peut passer à l'affichage en manuel. pb.show() for i in range(100): time.sleep(.1) pb.progress = i pb.progress = 100 pb.stop()