Site original : Sam & Max: Python, Django, Git et du cul
Ces mots clés vont-ils être introduits pour Python 3.5 alors qu’elle est déjà en alpha 4, et que la feature freeze est pour la prochaine version ? La release finale est prévue pour le 22 mai, ce qui est à peine un mois, pour une nouveauté formalisée début avril et toujours en draft.
J’avoue qu’à première vue, l’idée ne m’a pas enthousiasmé.
D’abord, parce que ça n’apporte pas grand chose de nouveau. Les coroutines, ça existe depuis un bail maintenant, et asyncio les exploite avec yield from
.
Pour résumer, la proposition est d’introduire deux mots clés tels que :
import asyncio @asyncio.coroutine def truc(): bidule() yield from machine_asynchrone() chose() |
Puisse être écrit :
async def truc(): bidule() await machine_asynchrone() chose() |
(Notez que la coloration syntaxique ne les prends pas en compte :))
Le PEP propose également l’introduction de deux syntaxes pour avoir des context managers et des boucles asynchrones: async with
et async for
Et ça ne m’a pas chauffé parce que :
async
avec un autre mot clé et ça pête toutes les attentes qu’on a eu sur la syntaxe du langage jusqu’ici : maintenant on a des qualificateurs, ce qu’on avait jamais eu avant. Ca ajouté aux type hintings, qui sont déjà très laids, c’est charger la mule.Entre temps j’y ai réfléchi et avec le recul, je commence à voir de sérieuses qualités à cette proposition :
yield from
pour les deux rendait tout ça bien confus.await
c’est moins chiant à tapper que yield from
et plus joli.yield from
d’une coroutine, c’est la merde, ce qui n’est pas le cas pour les await
.yield
qu’il faut assimiler, et apprendre à distinguer dans ses deux formes.Bref, async
et await
sont plus explicites, amènent moins de confusion, évitent des bugs et d’une manière générale permettent une compréhension plus rapide du code, mais également du concept de l’asynchrone en général, particulièrement pour les débutants.
Car j’avais en effet vu pas mal de gens ne rien piger aux coroutines.
Du coup, même si j’aimerais que le truc ne soit pas baclé et inséré à la va vite avant la bêta, je trouve finalement que c’est pas mal. La syntaxe n’est pas fantastique, mais je n’arrive pas à imaginer mieux (alors que clairement, les types hinting seraient 100x mieux dans la docstring avec le format existant pour sphynx), donc inutile de critiquer si on apporte pas de solution.
On voit passer des technos ici et là qui annoncent “un langage qui a un goût de Python”, “une syntaxe Python like”, “un preprocesseur qui converti un code compatible Python en JS”, etc.
Ça n’a aucun intérêt.
Oui, la syntaxe de Python fait parti de ses points forts, d’une des raisons qu’on aime le langage. Mais la syntaxe n’est PAS le langage.
Python, c’est un standard qui signifie que mon code Python tourne sur CPython, Pypy, IronPython et Jython. Ce n’est pas qu’une syntaxe. C’est aussi l’accès à une gigantesque stdlib, à des milliers de paquet sur pypi, à de la métaprogrammation, à la capacité de créer des threads, du multiprocessing, des coroutines, d’accéder au système de fichier, d’attaquer une base de données, de traiter les dates, etc.
Ce n’est pas juste la possibilité de faire :
[x * 2 for x in iterable if condition] |
Au passage, généralement les Python-like, ne font même pas ça correctement. Oubliant qu’en python on peut faire aussi :
[x * 2 for x in iterable][2:6:3] (x * 2 for x in iterable) next(x * 2 for x in iterable) next((x * 2 for x in iterable), 0) {x * 2 for x in iterable} {x: x * 2 for x in iterable} [x + y for x, y in iterable] [y * 2 for x in iterable for y in x] |
Bref, un truc qui fait vaguement comme Python n’est pas intéressant. Si je veux quelque chose de différent, je vais utiliser un autre langage. Si je veux Python, je vais utiliser Python. Pas un ersatz qui ne me donnera pas un dixième du potentiel d’un outil que je maîtrise très bien.
Si vous faites de l’analyse de gros jeux de données en Python, vous vous êtes vite rendu compte que c’est là que le langage montre sa lenteur.
Les boucles pour manipuler des arrays d’entiers, qui prennent quelque nanosecondes en C, prennent des microsecondes en Python. C’est pour ça que les scientifiques utilisent SciPy et les analystes Pandas, qui sont tous deux basés sur la lib C Numpy bien enrobée dans du joli Python.
Parfois néanmoins, garder le code en pur Python est un objectif, comme dans le cas du projet mss, un screenshotter qui ne souhaites avoir une extension compilée attachée à la patte.
Or la manipulation d’images, ça bouffe des grosses matrices de pixels, et l’auteur nous a posé une jolie colle sur IndexError : ayant un array de pixels en notation BGR (Blue, Green Red), comment le transposer en RGB de manière performante ?
Sa proposition était :
pixels = range(300) # je réduis le nombre de pixel pour le benchmark buffer_len = len(pixels) def version1(): for idx in range(0, buffer_len - 2, 3): pixels[idx + 2], pixels[idx] = pixels[idx], pixels[idx + 2] |
J’ai tenté :
from array import array xrange = getattr(__builtins__, 'xrange', range) def version2(): def to_rgb(pixels, buffer_len): for i in xrange(0, buffer_len - 2, 3): yield pixels[i + 2] yield pixels[i + 1] yield pixels[i] return array('H', to_rgb(pixels, buffer_len)) |
Mais mesurer révèle que cette solution est au final bien plus lente que celle de l’auteur :
import timeit print(timeit.timeit('version1()', setup='from __main__ import version1')) #16.9543120861 print(timeit.timeit('version2()', setup='from __main__ import version2')) #42.7808477879 |
C’est bubulle qui va nous pondre une solution originale, élégante, mais surtout 5 fois plus rapide, ce qui est complètement inattendu :
def version3(): pixels[2:buffer_len:3], pixels[0:buffer_len:3] = pixels[0:buffer_len:3], pixels[2:buffer_len:3] |
Il utilise 3 astuces en une :
Le gain de perf est assez bluffant :
print(timeit.timeit('version3()', setup='from __main__ import version3')) #1.84227705002 |
Pourquoi est-ce aussi rapide ? Je ne peux que faire des suppositions.
Je pense que faisant cette opération sans boucler explicitement, une bonne partie reste dans l’implémentation en C sous-jacente. On évite notamment pas mal d’appels à __getitem__
et __setitem__
qui sont toujours coûteux en Python.
Par curiosité, j’ai lancé le même test sur pypy :
0.359977960587 15.5387830734 0.750488996506 |
On voit l’effet de la compilation JIT puisque la boucle explicite redevient plus performante.
Mais du coup je me pose une question : pourquoi ma version, qui utilise une boucle explicite également, reste autant à la traine ?
Puis j’ai réalisé : l’array stock des entier C, mais quand on les sort, il faut les rewrapper dans des entiers Python. Donc ma boucle se tape cette conversion à chaque tour, et ça bouffe.
Je vire l’array :
def version4(): def to_rgb(pixels, buffer_len): for i in xrange(0, buffer_len - 2, 3): yield pixels[i + 2] yield pixels[i + 1] yield pixels[i] return list(to_rgb(pixels, buffer_len)) |
Et pouf, le temps devient plus raisonnable.
Avec CPython : 1.84687900543
Avec Pypy : 0.274104118347
Sur CPython, la vitesse est du même ordre de grandeur que la version avec les slices.
Sur PyPy, la version est 2X plus rapide que les slices, est 25% plus rapide que la version originale.
Moralités :
array
sert à économiser de la place en mémoire, pas à gagner du CPU.Ceci est un post invité de coyote posté sous licence creative common 3.0 unported.
Comme tout bon lecteur de S&M, je me fais un devoir d’adopter tous les outils conseillés par nos maîtres (oui bon sauf crossbar.io – j’ai une vie…)
Donc, j’étais très heureux de découvrir pew mais soyons honnête, ce n’est pas exactement un drop-in replacement de virtualenv-wrapper. Bien sûr, c’est très proche et c’est beaucoup mieux mais… deux-trois petites bricoles n’emmerdaient toujours.
bash sous OSX et Linux ont des configurations et conventions un poil différentes ce qui fait que par défaut, si vous n’êtes pas déjà au fait de ces différences, vous vous rendez compte que dès que vous activez un environnement avec pew, tout est cassé : votre prompt, vos raccourcis, etc.
Tout ça est dû au fait que (/!\ simplification inside) :
* Sous linux, au lancement de tout shell, le contenu du bashrc (~/.bashrc, /etc/bashrc) est exécuté.
* Sous OSX, au lancement d’un terminal, le contenu du ~/.bash_login est exécuté puis au lancement de subshell, c’est le ~/bashrc.
C’est con, mais ça veut dire qu’à chaque pew workon toto
, seul .bashrc est exécuté. Et généralement, sous OSX, il est vide ou non existant.
C’est très simple, mais ça prends quand même 5mn…
On ne peut pas simplement tout déplacer du .bash_login vers le .bashrc, sinon hors virtualenv ça ne marchera pas. Voici donc comment je fais moi, mais c’est juste parce que je suis paresseux.
~/.bash_login:
source ~/.bashrc |
Et c’est tout. En gros, on court-circuite le comportement OSX pour revenir vers du plus standard.
Extrait du ~/.bashrc:
# modification de diverses variables que je veux PARTOUT (hors env et dans env) export PATH=/usr/local/mongodb-osx-x86_64-2.6.3/bin:$PATH # modifications que je veux uniquement HORS env # si vous avez d'autres versions de python ou pip dans votre path, # comme le .bashrc est lancé après l'inclusion du path de l'environ par pew # vous devez bricoler sinon vous aurez pas la bonne version. if [ "${VIRTUAL_ENV}a" = "a" ] then export PATH="/Library/Frameworks/Python.framework/Versions/3.4/bin:${PATH}" else # modification voulues uniquement dans l'env ? fi # raccourcis pour les vieilles habitudes. c'est ce qui me derange # le plus avec pew :) function workon() { pew-workon $@; } # completion pew sur le raccourci. vous connaissez le nom de vos envs vous ? _pew() { local cur=${COMP_WORDS[COMP_CWORD]} COMPREPLY=( $(compgen -W "$(pew-ls)" -- ${cur}) ) } complete -F _pew workon # renvoie le path d'un projet depuis le nom de l'env pour faire d'une pierre deux coups (optionnel) # utilise pyp function pwdof() { path=`pwd` env_name=`echo $@| pyp "p.split('/')[-1]"` if [ "${env_name}" = "toto" ] then path=~/src/toto-project; fi echo $path; } # coloration du prompt a l’intérieur du subshell # j'aime avoir les branches git et le nom de l'env dans mon prompt. # donc pour que votre prompt marche dehors ET dedans (exemple) function color_prompt { local __user_and_host="\[\033[01;32m\]\u@\h" local __cur_location="\[\033[01;34m\]\w" local __git_branch_color="\[\033[31m\]" local __git_branch='`git branch 2> /dev/null | grep -e ^* | sed -E s/^\\\\\*\ \(.+\)$/\(\\\\\1\)\ /`' local __prompt_tail="\[\033[35m\]$" local __last_color="\[\033[00m\]" export PS1="$__user_and_host $__cur_location $__git_branch_color$__git_branch$__prompt_tail$__last_color " if [ "${VIRTUAL_ENV}a" != "a" ] then export PS1="(\$(basename '$VIRTUAL_ENV'))$PS1"; # facultatif: permet de faire un cd au lancement du subshell cd `pwdof ${VIRTUAL_ENV}` fi } color_prompt # completion de la commande pew qui n'est pas installé par pip # mais qui reste pratique. # adapté depuis: https://github.com/berdario/pew/blob/master/pew/complete_scripts/complete.bash _pew() { local cur=${COMP_WORDS[COMP_CWORD]} local prev=${COMP_WORDS[COMP_CWORD-1]} args="--help --python -i -a -r" commands="ls add mkproject rm lssitepackages cp workon new mktmpenv setproject show wipeenv sitepackages_dir inall toggleglobalsitepackages" case $prev in ls|show|rm|workon|cp|setproject) COMPREPLY=( $(compgen -W "$(pew-ls)" -- ${cur}) ) return 0 ;; inall) _command_offset 2 return 0 ;; mktmpenv|new) COMPREPLY=( $(compgen -W "${args}" -- ${cur}) ) return 0 ;; mkproject) COMPREPLY=( $(compgen -W "${args} -t --list" -- ${cur}) ) return 0 ;; add) COMPREPLY=( $(compgen -W "--help -d" -- ${cur}) ) return 0 ;; esac COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) } complete -F _pew pew |
Et voilà !
Ça casse pas trois pattes à un canard mais si vous êtes sous OSX, que vous avez installé, vu que c’était pas parfait puis vous êtes dit «on verra plus tard» et bien le moment est arrivé.
Je sais pas pour vous, mais moi je me souviens jamais de :
# -*- coding: utf-8 -*- |
Je le copie/colle à chaque fois, et sur sublime j’ai un snippet pour le taper.
Je viens d’apprendre, après 10 putain d’années à coder en python, que le header suivant était parfaitement valide :
# coding: utf-8 |
Bordel de merde, pourquoi c’est pas écrit dans tous les tutos ? Pourquoi on se tape encore l’ancien ?
Évidement avec Python 3, y a souvent plus besoin d’en-tête du tout, mais shit, ce genre détail c’est super con. Vous imaginez pas le nombre d’élèves à qui j’ai fait apprendre la première version toute pourrie alors que la seconde est si simple.
Grr, dirais-je.