PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Les interpréteurs alternatifs de python 20

jeudi 3 mars 2016 à 09:17

Ceci est un post invité de deronnax posté sous licence creative common 3.0 unported.

Bonjour, tous.
Vu qu’un des derniers posts de Sam&Max parlait de vitesse et des implémentations alternatives de Python, je propose un article sur l’état de ces différentes implémentations justement, et leur rapport à la vitesse.

Je commence par un petit rappel, pour nos lecteurs les moins aguerris avec l’écosystème Python : Python est un langage et aussi un interpréteur de référence, dont le vrai nom est CPython, écrit en C, qui livre également la bibliothèque standard Python (les modules datetime, urllib, collections, etc). CPython a des performances typiques de langage de script comme PHP ou Ruby, c’est à dire pas terribles, et a une conception assez simple, qui utilise des solutions simples pour répondre aux problèmes de langages de script. Celles qui font le plus parler d’elles (en mal évidement) étant la gestion de la mémoire par comptage de références (references counting), considérée moins performant qu’un vrai ramasse-miette (garbage collector) et assez limitée, et un verrou global de l’interpréteur, dont vous avez déjà dû entendre parler, le très mal-aimé GIL (Global Interpreter Lock), qui empêche entre autre à CPython d’avoir de vrai threads, c’est à dire des portions de code s’exécutant réellement en même temps, concurremment, sur des cœurs processeur différents. Néanmoins, rappelons-le encore : ces solutions permettent d’avoir un interpréteur simple et clair, pour lequel écrire des modules d’extensions en C est simple, ce qui est considéré actuellement comme le plus important. De plus, CPython lui-même n’ayant pas des performances significatives, du vrai multithread n’est pas un besoin pertinent. Si vous avez besoin de faire des traitements lourds, 9 fois sur 10 une bibliothèque spécialisée en C s’en occupe et fait elle même le traitement lourd en C dans ses threads, tel qu’OpenCV ou NumPy. En général vous avez très rarement besoin de vrai thread Python, et donc le GIL est relativement un faux problème.
La communauté Python s’est adaptée à cet absence de vrai concurrence, et différentes solutions sont apparues pour contourner le problème, je pense par exemple à l’excellent module multiprocessing, qui vous permet d’instancier différents interpréteurs et de les faire communiquer entre eux de manière transparente comme si c’était des threads.
D’une manière générale, quand il sera question d’un interpréteur alternatif, quatre points intéresseront la communauté :

Je ne parlerai ici que des interpréteurs se voulant concurrents de CPython et remplissant la même niche. Les interpréteurs remplissant une tache un peu différente, et/ou nécessitant des modifications significatives de la manière d’écrire du code Python, ou n’ayant pas un niveau d’aboutissement significatif ne seront pas abordés, comme Cython ou Stackless Python. Voici une liste complète des implémentations de python.

Déjà, niveau implémentations alternatives, parlons des historiques Jython et IronPython ; ces deux projets sont des interpréteurs python tournant dans la VM existante d’un autre langage, respectivement la JVM Java d’Oracle (Sun, avant son rachat) et le CLR/.Net C# de Microsoft, apportant ainsi différents bénéfices, entre autre un vrai ramasse-miette et aussi des vrai threads qui carburent. Ils ont aussi en commun d’avoir été peu adoptés (très peu dans le cas d’IronPython) et qu’ils ne sont plus très vivants (très peu dans le cas d’IronPython, déjà lâché par Microsoft en 2010) : dernière release de Jython: mai 2015 ; IronPython: juin 2014. Ça fait mal. De plus, si à leur sortie ils donnaient par moment de meilleures performances que CPython, ce dernier s’est beaucoup amélioré et maintenant ils se valent au mieux. De plus, tous les deux sont très lourds, avec une empreinte mémoire beaucoup plus grosse et un temps de démarrage sans comparaison avec CPython. À déployer, c’est complètement la mort aussi, rien à voir avec apt-get install python/pypy. Très beaucoup la mort pour obtenir un IronPython qui tourne sous Mono sous Ubuntu. Ces projets sont bien partis pour disparaître s’ils ne changent pas. Et le support de python3 est inexistant pour tous les deux. Et vos modules codés en C, vous pouvez vous asseoir dessus.

Pour la petite histoire (anecdote à recaser en soirée pour briller), Jim Hugunin, le créateur de Jython, a commencé à écrire IronPython pour démontrer aux gens que la plateforme .Net était mauvaise et biens moins bonne que la JVM pour écrire une implémentation de langage. Jim a commencé à écrire une implémentation de Python en .Net, et à sa grande surprise a réussi à faire une implémentation très aboutie en peu de temps et a trouvé la plateforme .Net tellement géniale qu’il a quitté le projet Jython pour lancer IronPython.

Ensuite vient Pypy, l’interpréteur Python en python orienté vitesse. Pypy existe depuis une dizaine d’année et vise à être un drop-in remplacement de CPython, c’est à dire qu’ils suffit de l’installer et de changer les commandes “python truc” par “pypy truc” et hop ça marche. Ça fonctionne, à condition de ne pas utiliser de modules compilés en C. Si c’est néanmoins le cas, Pypy peut utiliser les modules en C de CPython moyennant recompilation, mais attention, les perfs sont moins bonne qu’avec CPython, dû au fait que l’API vient de CPython et impose à Pypy, un interfaçage/fonctionnement qui n’est pas le sien et ne supporte pas toute l’API. Pypy délivre actuellement des performances en moyenne neuf fois supérieures à CPython sur leur suite de tests, ce qui est assez bluffant. Après, à chacun de considérer si la suite de test de Pypy est représentative d’une utilisation réelle, ce qui est le problème existentielle de chaque implémentation Python alternative qui vous met sous le nez un graphe montrant qu’elle est X fois plus rapide que CPython sur sa suite de tests. Pypy utilise également un GIL, donc ne fourni pas de vrais threads, mais a dans un coin un projet de passer à un modèle de concurrence qui permettrait de s’affranchir du GIL, et dispose par contre d’un vrai ramasse-miette. Le support de Python 3 est minimum, en juin 2014 est sorti Pypy 2.4 supportant python version 3.2, l’équipe informant qu’il est néanmoins plus lent que Pypy visant python 2.7, autant vous dire qu’on est pas en avance.

Vous avez pu tomber sur des noms genre Unladden Swallow et Stackless Python : Unladen Swallow était un Pypy-like antérieur par des ingénieurs de Google qui voulait générer du JIT avec LLVM en promettant des performances 5 fois supérieurs à CPython. Le projet a échoué à tenir ses promesses et a fini par mourir doucement lors que Google a retiré son financement. Ce qui a pu être sauvé a été intégré dans Pypy (des améliorations au module Pickle). Stackless  est un interpréteur modifié qui intègre les coroutines de base et d’autres trucs parallèles/asynchrone, il n’est pas un drop-in remplacement et demande d’écrire du python qui ne tournera que sur et pour lui.

Ensuite est venu il y a un an le “Pypy” de Dropbox, Pyston. Tout le monde a tapé sur Dropbox pour avoir réinventé sa roue carrée avec Pyston au lieu de contribuer à Pypy, Dropbox a répondu qu’ils utilisent une approche différente de celle de Pypy, ils utilisent un method-at-a-time JIT au lieu d’un tracing JIT, méthode qui a donné de si bons résultats avec le moteur javascript de Google, V8, en s’appuyant sur la célèbre LLVM, la “machine virtuelle pour le bas-niveau”. LLVM étant par ailleurs soutenue par des grands noms comme Apple et Intel, si Dropbox ne merde pas, ça pourrait donner un truc intéressant. De plus, contrairement à Pypy qui a choisi de peu supportter l’API C de CPython pour les modules, Pyston vise une compatibilité absolue avec l’API C, c’est une de ses priorité fondamentale . Nous pouvons en déduire que Dropbox a beaucoup de modules d’extension en C auxquels ils tiennent beaucoup. Pypy utilise un ramasse miette et un GIL. Un point fait néanmoins tousser avec Pyston, c’est que les créateurs déclarent ne vouloir que viser Python 2.7 et ne pas envisager de supporter python3. On a du mal à croire que Guido van Rossum travaille pour cette boite. Toutes ces informations viennent de la FAQ de Pyston. Le blog de Pyston, intéressant, explique leurs choix technique et rend compte de leur avancée : http://blog.pyston.org/.

Pyston a fait plusieurs chose intelligemment, notamment au lieu de réécrire un interpréteur, ils ont forké CPython et branché leurs systèmes de JIT dedans, ce qui leur a permis de sortir en moins d’un an une release de Pyston plutôt viable qui fait tourner beaucoup de choses et passe beaucoup de tests. Leur interpréteur est déjà 25% plus rapide que CPython sur leur suite de test, face à un Pypy 50% plus performant. C’est un petit tour de force.

Un énorme écueil d’écrire un interpréteur alternatif à CPython étaient les modules en C de la bibliothèque standard ; une partie significative des modules de la bibliothèque standard étaient écrits en C, donc si vous faisiez un interpréteur alternatif, il fallait aussi vous recoder dans le langage de votre interpréteur tous les modules de la bibliothèque standard que vous ne pouviez utiliser, ce qui implique de faire 10.000 tests pour s’assurer qu’ils ont exactement le même comportement que ceux de la bibliothèque standard, ce qui est extrêmement fastidieux et pénible. Depuis Python 3.3 en septembre 2012 , CPython fourni un recode en Python de tous ses module en C qui passent exactement la même suite de test et qui sont donc “garantis” (modulo erreur humaine, c’est une vaste et dure tâche) de se comporter exactement comme leurs congénères en C. C’est une énorme charge de travail en moins pour les développeurs d’interpréteurs alternatifs. Également, depuis Python 3.1 , l’import de modules, qui auparavant était une machinerie interne un peu obscure de l’interpréteur CPython, et donc qu’il fallait recoder en croisant les doigts pour que ça reproduise exactement le comportement de CPython sous peine de tout voir exploser, a été recodé en Python et est livré avec CPython, donc fini de redevoir recoder un système d’import, vous pouvez juste le reprendre et l’utiliser, ouf.

De tous, Pypy est le seul qui s’approche actuellement vaguement d’un “concurrent” sérieux à CPython, talonné par Pyston pour un futur proche. Et encore, ce n’est pas encore le drop-in remplacement parfait. Et tous ont un support de Python3 inexistant, sauf Pypy qui en a un insuffisant :). Et tous, en tant que VM complète (donc lourde) ou interpréteur-optimiseur visant à optimiser le code (et donc analyser), ont des temps de démarrage et d’atteinte de l’efficacité sans commune mesure avec CPython. Ils ne sont intéressants qu’en cas de long-running (genre, vos sites Django). Et tous utilisent un ramasse-miette, ce qui donne un comportement différents sur les objets “finalisés”. Alors que CPython a un comportement déterministe et détruit immédiatement les objets qui ne sont plus utilisés, les ramasse-miettes reportent leur destruction à un moment où il sera plus opportun de les détruire, potentiellement très longtemps ou jamais au pire des cas. C’est pour ça que par hygiène, il faut toujours libérer explicitement les ressources des objets représentants des ressources tel que les files-like objets représentants fichiers ou socket en utilisant leur méthode “close()” ou le manager “with” même si ça fonctionne très bien sans sous CPython. Pour les objets représentant des ressources limitées comme les fichiers, les sockets ou n’importe quel lien vers une ressource limitée procurée par l’OS, ne pas le faire signifie se prendre rapidement un stop de l’OS. D’une manière générale, CPython applique des solutions simples et déterministes qui offrent pas mal de garanties sympathiques (gestion des ressources, atomicité, alignement mémoire, adresse des objets en mémoire fixe, etc) qui sont maintenant considérées comme acquise et immuables par beaucoup de programmes python, malheureusement gênantes pour faire un interpréteur optimisant vraiment agressivement (là j’ai pas de sources mais c’est une complainte qui revient régulièrement dans les discussions des autres interpréteurs, surtout Pypy).

Parmi les news relatives à la vitesse de Python que l’article de S&M ne mentionne pas, la nouvelle release de la JVM vient avec un meilleur découplage des éléments la composant, et du coup il devient facile de l’utiliser pour un autre langage. IBM a fait un proof-of-concept en faisant un interpréteur python, qu’ils vont open-sourcer (et faire pareil avec Ruby). Pas d’altruisme là-dedans, c’est pour montrer au monde que leur VM est bien fichue. Ça pourrait être le nouveau Jython : http://www.infoworld.com/article/3014128/open-source-tools/ibms-open-source-jvm-project-could-also-speed-ruby-python.html.

Aussi, Microsoft s’étant visiblement, dernièrement, un peu amouraché de Python, on peut s’attendre que leur coopération à Python aille un peu plus loin. Ou pas.

Après, la triste vérité est que les langages dynamiques sont très durs à optimiser, parce qu’ils sont justement dynamiques, surtout Python où tout est modifiable, et donc interpréteurs/VMs ne peuvent être sûrs de rien et ne peuvent pas supprimer pleins de tests/résolutions/recherches, et que Python, malgré toutes ses implémentations pleines de bonne volonté, ne sera jamais aussi rapide que le C, ni même que Java. La dernière fois que j’ai regardé (~3 ans), le benchmark game donnait python 20x plus lent que le C, contre 2x pour le Java. Ça fait réfléchir. Tous les efforts poussés à fond, on tapera peut être le 5x plus lent que le C :)

Actuellement, un core-contributeur de CPython (français, cocorico !) travaille sur différents sujets visant à améliorer significativement les performances de CPython. C’est la première fois qu’un chantier aussi vaste est lancé visant à sérieusement améliorer les performances de CPython. Le site sur le sujet : http://faster-cpython.readthedocs.org/

Ces différents projets acceptent les dons. Je vous encourage évidemment vivement à donner. La vitesse de Python est l’un dernier écueil qui pourraient faire choisir à un décideur technique pressé un autre langage plutôt que Python. Des meilleurs interpréteurs Python, c’est plus de tâches accomplies en Python, donc plus de postes pour les développeurs Python, donc plus de demande, et donc de meilleurs salaires. Quand vous donnez à des projets Python, vous vous versez de l’argent à vous-même dans le futur. De plus, ça aide à faire reculer les parts de marché de cette horreur qu’est PHP et de la concurrence de Ruby et Go, et ça, c’est toujours ça de pris :)
Même une petite somme est significative. Avec l’euro fort, vous avez un grand pouvoir. Et un grand pouvoir implique de grandes responsabilités.
Donner à Cpython : https://www.python.org/psf/donations/
Donner à Pypy : http://pypy.org/

Voila, vous savez tout sur l’histoire des différents interpréteurs Python. Avec ça, vous pourrez vous la pétez en soirée et niquer des tonnes de meufs. Ou pas.

Il n’y a pas de mauvais script… 11

mardi 1 mars 2016 à 12:22

Ceci est un post invité de atrament posté sous licence creative common 3.0 unported.

… il n’y  a que des scripts en passe de devenir bons. Avis aux débutants, croyez-y, vous ne le resterez pas.

C’est l’histoire d’un des premiers “vrais scripts” que j’ai écrits. Je venais de finir Learning Python et Programming Python de Mark Lutz (à l’époque la seconde édition “couvre python 2 !”).

J’étais même pas un script kiddie, et je ne programmais que pour m’amuser. J’étais fan de Sinfest, un web comic gavé de gros blasphèmes qui tâchent.

Je me suis mis en tête de télécharger tout ces comics d’un coup. Voilà ce que ce script était, et ce qu’il est devenu.

Script originel avec formatage encore moins lisible, sur 0bin.

#~ Script to DL all sinfest webcomics
#~ one can edit the START and END parameters to DL a subset
STARTDAY = 17  # 17
STARTMONTH = 01  # 01
STARTYEAR = 2011  # 2000
ENDDAY = 31
ENDMONTH = 01
ENDYEAR = 2012
ROOTDIR = 'Sinfest'
ROOTURL = "http://www.sinfest.net/comikaze/comics/"
IMGEXT = ".gif"
#~ Code start - no edition from here unless you know what you're doing
#~ --------------------
from distutils.file_util import copy_file
from urllib import urlopen
import os.path
import os
#~ ROOTDIR=os.getcwd()+ROOTDIR
#~ Check if date is out of range
 
 
def checkdate(d, m, y, ed, em, ey, sd, sm, sy):  # end date is ed, em, ey
    if y > ey or (y == ey and m > em) or (y == ey and m == em and d > ed):
        return 0  # date over date range
    elif y < sy or (y == sy and m < sm) or (y == sy and m == sm and d < sd):
        return 0
    else:
        return 1  # date below end date and above start date
#~ make the file name teh sae format than sinfest server
 
 
def getfilename(y, m, d, IMGEXT):
    M =`m`
    D =`d`
    Y =`y`
    if len(M) < 2:
        M = '0' + M
    if len(D) < 2:
        D = '0' + D
    filename = Y + '-' + M + '-' + D + IMGEXT
    return filename
#~ run the loop
if not os.path.isdir(ROOTDIR):
    os.mkdir(ROOTDIR)
for y in range(STARTYEAR, ENDYEAR + 1):
    for m in range(1, 13):
        for d in range(1, 32):
            filename = getfilename(y, m, d, IMGEXT)
            if checkdate(d, m, y, ENDDAY, ENDMONTH, ENDYEAR,
                         STARTDAY, STARTMONTH, STARTYEAR):
                if os.path.isfile(ROOTDIR + '/' + filename):
                    print '[' + filename + '] already exists'
                else:
                    src = urlopen(ROOTURL + filename)
                    if src.info().gettype() == 'image/gif':
                        dst = open(ROOTDIR + '/' + filename, 'wb')
                        dst.write(src.read())
                        dst.close()
                        print '[' + filename + ']' + ' copied'
                    else:
                        print "MIMETYPE ERROR (probably ERROR 404) ignored"
                    src.close()

On remarquera l’arrogance du jeune codeur fier de lui dans certains commentaires. Ils étaient à mon intention, en vrai, et montraient surtout que je ne savais pas trop ce que je faisais.

En vrac, parmi les choses qui me font sourire aujourd’hui :

Bon, il y a prescription, et puis je suis pas mal autodidacte, c’était pas si mal : ça fonctionnait ! Ça m’a quand même servi, de temps en temps, jusque 2012, pour remettre à jour mes dossiers d’images.


Et puis je l’ai relu et j’ai décidé de le reprendre avec ce que je savais de nouveau :

et ça a donné ça :

# -*- coding: utf-8 -*-
#! /usr/bin/python
# Script to DL all sinfest webcomics
# one can edit the START and END parameters to DL a subset
#-------------------------------------------------------------------------------
# Name:        getSinfest
# Purpose:
#
# Author:      Atrament
#
# Created:     12/04/2013
# Copyright:   (c) Atrament 2013
# Licence:     
#-------------------------------------------------------------------------------
 
import datetime
from urllib.request import urlopen
import os.path
import os
from urllib.error import HTTPError
 
 
def main():
    debut=datetime.date(2000,1,17)
    curGif=debut
    if not os.path.isdir("Sinfest"):
        os.mkdir("Sinfest")
    while curGif<=datetime.date.today():
        if not os.path.isfile("Sinfest/"+curGif.isoformat()+".gif"):
            try:
                src=urlopen("http://www.sinfest.net/comikaze/comics/"+curGif.isoformat()+".gif")
                dst=open("Sinfest/"+curGif.isoformat()+".gif",'wb')
                dst.write(src.read())
                dst.close()
                print("    "+curGif.isoformat()+".gif : fetched.")
                src.close()
            except HTTPError as e:
                print("on "+curGif.isoformat()+" error happened. So is life.")
        else:
            print(curGif.isoformat()+" : ok")
        curGif+=datetime.timedelta(days=1)
 
if __name__ == '__main__':
    main()

Et honnêtement ça s’améliore (un peu): un shebang (pas tout à fait en haut du fichier, mais j’apprendrai plus tard), bien que je code à l’époque uniquement sous windows avec pyscripter, qui me fournit les commentaires de début de fichier, qui ne servent à rien, mais à l’époque m’éclatent. Ben oui, à ce moment là, je suis à peine adulte, et avec les heures que j’ai passé sur IDLE à tâtonnner, c’est une victoire pour moi.


C’est un peu plus tard encore que j’apprends que les requêtes internet qui prennent des heures, c’est pas obligatoire, parce qu’on peut faire du multi thread, et continuer de tourner pendant qu’on attend qu’une autre tâche s’accomplisse. Voilà de quoi faire rêver un jeune programmeur : la puissance du multi-coeurs à la portée de mon code ! (non, je ne savais pas que c’est faux). En conséquence, mon script évolue, pour devenir… ça. Attention, ça pique les yeux.

# -*- coding: UTF-8 -*-
# ---------------------------------------------------------------------
# Author:      Atrament
# Licence:     CC-BY-SA https://creativecommons.org/licenses/by-sa/4.0/
# ---------------------------------------------------------------------
# All libs are part of standard distribution for python 3.4
import imghdr
import os
import queue
from threading import Thread
import datetime
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
import zipfile
import sys
 
 
# Useful functions
def make_cbz(directory):
    for year in range(2000, datetime.date.today().year + 1):
        with zipfile.ZipFile("Sinfest-{}.cbz".format(year), "w") as archive:
            for gif_file in [x for x in os.listdir(directory) if x.split("-")[0] == str(year)]:
                archive.write(directory + '/' + gif_file, arcname=gif_file)
        print("Sinfest-{}.cbz has been generated".format(year))
 
 
def confirm(prompt):
    if input(prompt + " (y/n)") in "yY":
        return True
    else:
        return False
 
 
def file_needs_download(filename):
    """Checks whether a file exists, is corrupt, so has to be downloaded again
    also cleans garbage if detected"""
    if not os.path.isfile(filename):
        # many comics are *supposed* to be missing,
        # no need to output for these (uncomment for debug)
        # print("IS NOT FILE :", filename)
        return True
    elif os.path.getsize(filename) == 0:
        print("WRONG SIZE for", filename)
        return True
    elif filename.split(".")[-1] != "gif":
        print(filename, "IS NOT GIF")
        return True
    elif filename.split(".")[-1] in {"jpg", "gif", "png"} and imghdr.what(filename) is None:
        # Encoding error...
        print("WRONG FILE STRUCTURE for", filename)
        return True
    else:
        return False
 
 
def conditional_download(filename, base_url):
    if file_needs_download(filename):
        try:
            src = urlopen(base_url + filename)
            dst = open(filename, 'wb')
            dst.write(src.read())
            # gracefully close theses accesses.
            dst.close()
            src.close()
            print("\t" + filename + " : fetched.")
        except HTTPError:
            # many days do not have a comic published.
            # no need to flood the console for this.
            pass
        except URLError:
            pass
            print("network error on " + filename)
        finally:
            # clean garbage on disk, useful if failure occurred.
            file_needs_download(filename)
 
 
class ThreadedWorker():
    def __init__(self, function=None, number_of_threads=8):
        self.queue = queue.Queue()
 
        def func():
            while True:
                item = self.queue.get()
                if function:
                    function(item)
                else:
                    print(item, "is being processed.")
                self.queue.task_done()
 
        self.function = func
 
        for i in range(number_of_threads):
            t = Thread(target=self.function, name="Thread-{:03}".format(i))
            t.daemon = True
            t.start()
 
    def put(self, object_to_queue):
        self.queue.put(object_to_queue)
 
    def join(self):
        self.queue.join()
 
    def feed(self, iterator):
        for task in iterator:
            self.queue.put(task)
 
 
def download_sinfest(target_folder):
    """
    Creates a directory and fetches Sinfest comics to populate it in full.
    """
    if not os.path.isdir(target_folder):
        os.makedirs(target_folder)
    os.chdir(target_folder)
 
    f = lambda filename: conditional_download(filename, "http://www.sinfest.net/btphp/comics/")
    # Make a worker with this function and run it
    t = ThreadedWorker(function=f, number_of_threads=20)
    # structure of comprehended list is a bit complex to generate all file names
    files = [(datetime.date(2000, 1, 17) + datetime.timedelta(days=x)).isoformat() + ".gif"
             for x in range((datetime.date.today() - datetime.date(2000, 1, 17)).days + 1)]
    t.feed(files)
    t.join()
 
 
if __name__ == "__main__":
    if any(("y", "Y", "-y", "-Y" in sys.argv)):
        folder = os.path.expanduser("~/Pictures/Sinfest/").replace("\\", "/")
        print("\nproceeding to download...")
        download_sinfest(folder)
        print("\ngenerating comic book files (.cbz)...")
        os.chdir(folder)
        os.chdir("..")
        make_cbz(folder)
    else:
        folder = os.path.expanduser("~/Pictures/Sinfest/").replace("\\", "/")
        while not confirm("Target to downloads is {} ?".format(folder)):
            folder = input("Please enter new folder (N to abort) :")
            if folder in "nN":
                exit(0)
        if confirm("Proceed to download ?"):
            download_sinfest(folder)
        if confirm("Do you want to generate cbz (comic books) files ?"):
            os.chdir(folder)
            os.chdir("..")
            make_cbz(folder)
        input("Finished. Please press Enter")

C’est une horreur. Si vous êtes de ces perfectionnistes qui lisent le code et font la code review par habitude, je suis désolé, vous avez du pleurer.  Le commentaire sur la lib standard “en intro” montre que à ce moment là, j’ai un peu conscience qu’on peut accéder à d’autres modules, mais on est loin de pip pour moi. Je tente maladroitement d’exploiter sys.argv (avec des erreurs qui pourraient être catastrophiques),  et l’input à défaut. Une fonctionnalité neuve est apparue : faire des archives comic book en zip. Je suis fier.

Mais le côté comique de ce code, c’est la classe ThreadedWorker. C’est ptêt bien ma première ‘vraie’ classe, mais j’implémente moi-même un dispatcheur de jobs sur des threads. Il y a de quoi être fier, mais on n’est pas du tout dans du python propre, là. C’est ballot, la version précédente était pas si mal, niveau clarté.

Et dire que la lib standard en fournit un, de dispacheur de jobs…


Je passe quelques itérations sur des détails, aujourd’hui il en est là, ce script.

#! /usr/bin/env python3.5
import imghdr
import os
import datetime
import zipfile
from concurrent.futures import ThreadPoolExecutor
 
import requests
import begin
 
 
# Useful functions
def make_cbz(dst_directory, src_directory):
    for year in range(2000, datetime.date.today().year + 1):
        with zipfile.ZipFile("Sinfest-{}.cbz".format(year), "w") as archive:
            for filename in os.listdir(src_directory):
                if filename.startswith(str(year)):
                    archive.write(src_directory + "/" + filename, arcname=filename)
        print("Sinfest-{}.cbz has been generated".format(year))
 
 
def file_needs_download(filename):
    """Checks whether a file exists, is corrupt, so has to be downloaded again
    also cleans garbage if detected"""
    if not os.path.isfile(filename):
        # many comics are *supposed* to be missing,
        # no need to output for these (uncomment for debug)
        # print("IS NOT FILE :", filename)
        return True
    elif os.path.getsize(filename) == 0:
        print("WRONG SIZE for", filename)
        return True
    elif filename.split(".")[-1] != "gif":
        print(filename, "IS NOT GIF")
        return True
    elif filename.split(".")[-1] in {"jpg", "gif", "png"} and imghdr.what(filename) is None:
        # Encoding error...
        print("WRONG FILE STRUCTURE for", filename)
        return True
    else:
        return False
 
 
def conditional_download(filename, base_url, caller=None):
    if file_needs_download(filename):
        src = requests.get(base_url + filename)
        # manage failure to download
        if src.status_code == 404:
            src.close()
            return  # ignore it, that file is simply missing.
        if src.status_code != 200:  # an error other than 404 occurred
            # print("Error {} on {}".format(src.status_code, filename))
            src.close()
            if caller:  # retry that file later
                caller.submit(conditional_download, filename, base_url, caller)
        # actually copy that file
        dst = open(filename, 'wb')
        dst.write(src.content)
        # gracefully close theses accesses.
        dst.close()
        src.close()
        print("\t{} : fetched.".format(filename))
 
 
def download_sinfest(target_folder):
    """
    Source function for the process
    Creates a directory and fetches Sinfest comics to populate it in full.
    """
    if not os.path.isdir(target_folder):
        os.makedirs(target_folder)
    os.chdir(target_folder)
 
    with ThreadPoolExecutor(max_workers=64) as executor:
        for file in ("".join([(datetime.date(2000, 1, 17) + datetime.timedelta(days=x)).isoformat(), ".gif"])
                     for x in range((datetime.date.today() - datetime.date(2000, 1, 17)).days + 1)):
            executor.submit(conditional_download, file, "http://www.sinfest.net/btphp/comics/", executor)
 
 
@begin.start
def run(path: "folder in which the comics must be downloaded" = os.path.expanduser("~/Sinfest/"),
        makecbz: "Compile CBZ comic book archives" = False):
    """Download the Sinfest WebComics"""
    download_sinfest(path)
    if makecbz:
        dst = path[:-1] if path.endswith('/') else path
        dst = "/".join(dst.split("/")[:-1])
        make_cbz(dst, path)
    print("Finished. Goodbye.")
    exit()

Enfin je m’autorise à utiliser des modules tierces. Alors, ça marche, même plutôt vite et bien. Mais c’est largement perfectible : si c’est mieux que par le passé, ça ne correspond pas à ce que je code aujourd’hui : il y a beaucoup de code hérité des anciennes versions et la prochaine évolution serait de le réécrire entièrement. Mais je ne lis plus vraiment Sinfest.


 

Mais à écrire ce texte, j’ai pris les nerfs, et j’ai refait tout ça. J’ai fait un effort, et j’ai tout jeté à github.

Encore un pas de mieux : les fichiers ont du sens, il y a une classe abstraite pour faire une pseudo-API, c’est devenu pratiquement évolutif.

 

La conclusion à l’attention du débutant : chacun voit et juge son code et celui des autres selon son niveau de compétence à l’instant T. On a tous été fiers comme Artaban de bouts de code laids à nos yeux d’aujourd’hui. Débutant qui m’a lu (merci, t’as bien du courage), garde tes scripts à travers les années, reprends les, tu te rendras compte de tous les progrès que tu fais au fil du temps. Quand un type te dis sur internet que ce que tu fais n’est pas “pythonique”, il a sûrement raison, de son point de vue, et il y a sans doute un autre gars qui lui dira la même chose demain. La courbe d’apprentissage est longue comme la vie.

Open bar sur asyncio 5

lundi 29 février 2016 à 14:04

Plus je fais joujou avec asyncio, plus j’apprécie la lib. Mais je tombe aussi sur des tas de petits trucs qui me font dire qu’il va falloir créer quelques couches d’abstraction pour rendre tout ça plus miam.

Par exemple, lire de manière asynchrone les données pipées sur stdin:

async def main(loop):
 
    reader = asyncio.StreamReader()
    def get_reader():
        return asyncio.StreamReaderProtocol(reader)
 
    await loop.connect_read_pipe(get_reader, sys.stdin)
 
    while True:
        line = await reader.readline()
        if not line:
            break
        print(line)
 
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))

C’est assez verbeux, pas très Pythonique (une factory qui instancie un classe a qui on passe une autre instance, c’est très java), ça n’exploite pas toute la force de async/await (le while qui pourrait être un async for), et il faut se soucier de lancer la boucle.

En prime, c’est pas encore évident de trouver qu’il faut faire ça dans la doc.

Pareil, pour lire de manière asynchrone les données écrites par un utilisateur sur stdin:

def on_stdin(*args):
    print("Somebody wrote:", sys.stdin.readline())
 
loop = asyncio.get_event_loop()
loop.add_reader(sys.stdin.fileno(), on_stdin)
loop.run_forever()

Bon, là on a du callback, clairement on peut faire mieux.

La bonne nouvelle, c’est que ça veut dire qu’on a un champ entier où on peut être le premier à écrire une lib, et donc devenir une implémentation de référence, un outil connu, etc. Si vous avez envie de faire votre trou, c’est une opportunité, et en plus, c’est rigolo :)

En effet, avec un peu d’enrobage, on peut rapidement faire des trucs choupinets:

class AioStdinPipe:
 
    def __init__(self, loop=None):
 
        self.loop = loop or asyncio.get_event_loop()
        self.reader = asyncio.StreamReader()
 
    def get_reader(self):
        return asyncio.StreamReaderProtocol(self.reader)
 
    async def __aiter__(self):
        await self.loop.connect_read_pipe(self.get_reader, sys.stdin)
        return self
 
    async def __anext__(self):
        while True:
            val = await self.reader.readline()
            if val == b'':
                raise StopAsyncIteration
            return val
 
 
def run_in_loop(coro):
    loop = asyncio.get_event_loop()
    loop.run_until_complete(coro())

Et ainsi réduire le premier code à un truc très simple, très clair, très Python:

@run_in_loop
async def main():
    async for line in AioStdinPipe():
        print(line)

J’aime 2016, c’est une année pleine de possibilités.

Mais qui donc set ce signal handler ? 6

jeudi 25 février 2016 à 08:59

Le module signal permet de réagir aux signaux envoyés par l’OS, par exemple celui qui vous annonce qu’il va vous défoncer la gueule (SIGTERM) ou que vous votre mémoire n’est plus ce qu’elle était (SIGSEGV).

Mais le problème, c’est que rien n’annonce qu’un signal est attrapé. Pire, quand on enregistre un handler pour un signal, ça écrase le précédent. Yep, c’est très con.

ç’aurait été plus malin d’avoir un truc comme propose le DOM, genre addEventListener, qui accumule les handlers pour un event donné, mais non, on override.

Donc parfois, une lib tierce partie se permet un petit handling de signal sur le pouce, comme ça, sans demander la permission, provoquant chaos et moult jurons. Car des signaux, y en a pas mal:

>>> import signal
>>> [s for s in dir(signal) if s.isupper()]
['ITIMER_PROF', 'ITIMER_REAL', 'ITIMER_VIRTUAL', 'NSIG', 'SIGABRT', 'SIGALRM', 'SIGBUS', 'SIGCHLD', 'SIGCLD', 'SIGCONT', 'SIGFPE', 'SIGHUP', 'SIGILL', 'SIGINT', 'SIGIO', 'SIGIOT', 'SIGKILL', 'SIGPIPE', 'SIGPOLL', 'SIGPROF', 'SIGPWR', 'SIGQUIT', 'SIGRTMAX', 'SIGRTMIN', 'SIGSEGV', 'SIGSTOP', 'SIGSYS', 'SIGTERM', 'SIGTRAP', 'SIGTSTP', 'SIGTTIN', 'SIGTTOU', 'SIGURG', 'SIGUSR1', 'SIGUSR2', 'SIGVTALRM', 'SIGWINCH', 'SIGXCPU', 'SIGXFSZ', 'SIG_BLOCK', 'SIG_DFL', 'SIG_IGN', 'SIG_SETMASK', 'SIG_UNBLOCK']

En plus, certains sont Windows only, d’autres Unix only…

Et pour faciliter les choses, les handlers peuvent être des entiers (bon, des enums dont __eq__ accepte les entiers…):

>>> type(signal.getsignal(signal.SIGKILL))
<enum 'Handlers'>
 
>>> type(signal.getsignal(signal.SIGINT) )
<class 'builtin_function_or_method'>

Voici un petit snippet pour checker si un sacripant n’est pas en train de vous brouter le signal:

>>> import signal
>>> handlers = {}
>>> for s in dir(signal):
...     if s.isupper(): # on chope uniquement les constantes
...         try:
...             handler = getattr(signal, s) # quel handler pour ce signal ?
...             if callable(handler): # on ignore SIG_DFL, SIG_IGN, etc.
...                 handlers[s] = handler # ok, on a un truc gégé !
...         except ValueError:
...            pass
>>> print(handlers[s])

On s’en sert pas souvent, mais je me suis dit que je ferai passer le mot.

L’internationalisation, c’est long et dur (et ça racle le fond) 22

vendredi 19 février 2016 à 14:30

L’internationalisation, abrégé i18n car il y a 18 lettres entre le i et le n, est le processus qui consiste à proposer différentes versions d’un contenu ou d’une interface afin de coller au plus proche des attentes culturelles de différents groupes d’humains.

Quand je discute avec un client et qu’il me dit :

Je veux traduire mon site en Anglais, c’est combien ?

Je tique toujours un peu.

C’est que l’i18n, ce n’est pas juste traduire des mots vers des autres. C’est beaucoup, beaucoup plus compliqué que ça. Et il faut avant tout identifier qui on vise, ce qu’on veut leur apporter, et ce qu’on est prêt à investir comme énergie pour le leur offrir.

En générale, les populations anglophones sont assez proches culturellement de nous, donc c’est plus facile de faire la transition que vers des cultures arabes, russes ou chinoises. Et si c’est imparfait, ce n’est pas trop grave, car l’utilisateur s’adaptera assez bien.

Néanmoins, il ne faut pas croire que prendre le texte et le coller dans Google translate va suffire, loin de là.

Et si le travail vise de gens culturellement plus éloignés de vous, alors le travail peu vite de venir énorme.

Le texte

Bien entendu, on pense tout de suite à la traduction, et c’est déjà un gros boulot.

Mais déjà, il faut s’adapter au contexte culturel que l’on vise. En Japonais, les niveaux de respect ne seront pas les mêmes selon les clients. Vous traduisez vers de l’espagnol, mais visez-vous l’Amérique du Sud et ses ustedes partout, ou l’Espagne et son vosotros ? Vous voulez proposer de louer un cab aux Londoniens ou un taxi aux New-Yorkais ?

Au passage, prenez un bon traducteur, car en plus de devoir traduire le sens du texte (et pas juste les mots), il faut qu’il adapte la ponctuation. Ainsi, on colle “:”, “!” et “!” au mot précédent en anglais, mais pas en français.

On a à peine commencé l’article et c’est déjà la merde, mais attendez !

Il y a le sens de lecture aussi !

De gauche à droite, de droite à gauche, de haut en bas. Et si vous croyez que ce n’est que le texte, lol… Car ça influence également toute la mise en page, avec notamment le placement des éléments importants : titres, boutons, etc. afin qu’ils soient mis en évidence.

Après, il y a l’espacement. Vous savez, on peut se permettre de se la jouer cool sur les espacements en français, parce que’on a plein de ponctuation. Donc un bloc de texte est clair, facile à lire en diagonale. En Thaïlandais par contre, il n’y a pas de virgule, de point, ni même d’espace entre les mots et les phrases. Donc si vous avez un truc à bien séparer du reste, il va falloir trouver une astuce visuelle.

Et puis il y a mon favoris, particulièrement parce que Python est très sévère (mais juste:)) avec : l’encodage.

Car dans le meilleur des mondes, on a de l’UTF8 partout. Mais on est pas dans le meilleur des mondes, et si vous visez certains terminaux, il faudra encoder dans un truc couleur locale. Par exemple, si vous envoyez des SMS sur des vieux 3310 en Afrique.

Les nombres

Ah, les nombres. Qu’est-ce qu’il pourrait être plus simple ? C’est universel pas vrai ?

Pas vrai ?

Et bien disons que comme d’habitude, la donnée est universelle, et la représentation ne l’est pas.

Déjà, il a les bases. On aime beaucoup la base 10, et quand c’est du technique parfois un peu de binaire, d’octal ou d’hexa.

Mais certaines populations ont des manières de compter étonnantes. Par exemple, si vous faites un système à Bamako (ne rigolez pas, c’est un exemple de ma carrière), sachez qu’en Bambara 1000 se dit 2 fois 500.

À cela se rajoutent les séparateurs et devises. Mille dollars et quarante cents pour un américain, c’est $1,000.40 ou $1000.40, alors qu’en français ce sera 1000,40 $ ou 1 000,40 $.

Et chaque format à ses règles de groupages, ses séparateurs, ses positions de préfixes… Les numéros de téléphone, les numéros de sécurité sociale, les codes postaux…

Attendez, ne fuyez pas, on n’en est pas encore à la moitié !

Les unités

Même la NASA a niqué une mission parce qu’ils se sont embrouillés entre les unités impériales et le système métrique.

Alors vous allez me dire, tant qu’on n’utilise pas des unités pifométriques, il suffit de convertir. Oui, mais encore faut-il savoir de quoi on parle. Il faut donc toujours demander à l’utilisateur en quoi il saisit ses données, et être très précis.

Très précis parce que figurez-vous que:

Les dates

Je ne vous apprends rien en vous disant que gérer les fuseaux horaires c’est chiant. Mettez tout en UTC, et utiliser une bonne lib parce que le temps c’est relou : des zones géographiques alignées n’ont pas les mêmes heures (France et UK), des pays très grands ont plusieurs fuseaux (USA) ou un seul malgré leur gigantisme (Chine), et certains on les heures d’été/hiver, mais pas tous. Et il y a les années bissextiles, et la fameuse leap second. Et le fait que certains pays décident de changer de fuseau horaire comme ça, pouf, parce que ça les arrange.

Du coup il faut bien demander aux utilisateurs où ils sont pour récupérer la date dans le bon fuseau (l’ordinateur peut être réglé pour envoyer la mauvaise info, il peut sortir de l’avion, il peut ne pas vouloir utiliser la timezone actuelle, etc), convertir tout en UTC, et l’afficher dans le bon.

Après, hop, on a le formage des dates. Mais si, vous savez, ce truc qui a 40 000 règles qui changent d’un pays à l’autre, d’un standard à l’autre. 02/03/2015: dd/mm/yyyy ou mm/dd/yyyy ? Timestamp en seconde ou en millisecondes ?

Allez, vous croyez que vous vous en sortirez comme ça ?

N’oubliez pas, tout le monde n’a pas le même calendrier…

Nous utilisons un calendrier basé sur le chrétien, mais vous avez des calendriers bouddhistes, musulmans, des trucs basés sur les phases de la lune, et même il y a quelque temps en France le très rigolo calendrier républicain. Donc maintenant, il faut se soucier de pour où, de pour qui, mais aussi de pour quand on formate. Je plains les gens qui font des apps pour les musées.

Je plains aussi tous les gens qui font des plannings internationaux, qui doivent décider si lundi ou dimanche est le premier jour à afficher et traduire les notations naturelles comme “il y a 3 minutes(s)”.

La loi

Oh, si, oh si !

La loi change d’un pays à l’autre ! Nudité, le copyright, l’accessibilité…

Le DMCA aux USA, la censure en Chine, l’Hadopi en France. Nan pour le dernier je déconne hein, ils ont condamné 5 personnes depuis le début du programme.

Et allez prendre en compte les lecteurs d’écran, mettre du ARIA comme il faut, tester le bouzin pour les personnes en déficience visuelle (problèmes de couleur, aveugles, malvoyants…) ou motrice.

Bon alors là je ne vais pas rentrer dans les détails, car c’est trop, trop compliqué pour ma petite tête.

La culture

Le symbolisme diffère d’un groupe culturel à l’autre.

Vous allez faire un site pour le mariage, en beau blanc !

Mais en Chine, il faut du rouge.

Et dans certaines cultures, c’est la couleur du deuil.

Ce qui rend crédible commercialement aussi est différent. Un beau site bien épuré, avec plein d’espace négatif, c’est classe non ? Mais en Asie, avoir une page saturée est la norme.

En fait toute l’infrastructure, et même le système entier change la manière de voir les choses.

Quand on écrit du contenu pour un pays, il faut se rappeler des choses comme le climat, la monnaie différente, conduite à droite ou à gauche, voltage du courant, jours chômés, situation politique…

Si on fait une application avec beaucoup de carto, on ne mettra pas les mêmes choses en avant. Dans certains pays, les pistes sont importantes, dans d’autres les autoroutes le sont. Le truc le plus marquant qui me vient à l’esprit, c’est le fait que les cartes japonaises définissent un adresse en labélisant les blocs entre les rues, tandis que nous labélisons les rues entre les blocs.

Et je ne parle même pas du markéting

Qui est bien entendu radicalement différent selon la population visée, et non juste en termes d’i18n, mais aussi selon l’appartenance ethnique, sociale, culturelle, démographique, les handicape…

Il serait vraiment intéressant d’avoir un espèce de wiki open source qui, pour chaque groupe visé, liste les choses auxquelles il faut penser.

Finalement tout ça est trop chiant, z’ont qu’à se démerder ces estrangers de merde !

Arg, putain, on est 70 millions seulement, c’est nous les estrangers.

Après, ça ne veut pas dire qu’il faut tout faire, et trouver l’équilibre entre les ressources disponibles et le résultat qu’on vise. Mais il est bon de savoir ce qu’on décide de ne pas faire.