PROJET AUTOBLOG


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

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

⇐ retour index

async / await, la feature de dernière minute de Python 3.5 9

vendredi 24 avril 2015 à 20:47

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 :

Entre temps j’y ai réfléchi et avec le recul, je commence à voir de sérieuses qualités à cette proposition :

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.

Python-like n’a aucun intérêt 17

mardi 21 avril 2015 à 08:57

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.

Les slices, c’est rapide et c’est beau 19

jeudi 16 avril 2015 à 10:35

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 :

Minitip pew et OSX

mercredi 15 avril 2015 à 13:27

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.

Le problème

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.

La solution

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é.

Un header d’encoding plus simple pour Python 11

mardi 14 avril 2015 à 14:15

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.