Sam & Max » async http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Jouons un peu avec Python 3.5 27 http://sametmax.com/jouons-un-peu-avec-python-3-5/ http://sametmax.com/jouons-un-peu-avec-python-3-5/#comments Wed, 16 Sep 2015 16:31:38 +0000 http://sametmax.com/?p=16918 fantastique article présentant Python 3.5, je ne vais donc pas pas répéter inutilement ce qu’ils ont dit. Le but de ce post est plutôt de faire mumuse avec le nouveau joujou.]]> Zeste de savoir a fait un fantastique article présentant Python 3.5, je ne vais donc pas répéter inutilement ce qu’ils ont dit. Le but de ce post est plutôt de faire mumuse avec le nouveau joujou.

La release est récente, mais fort heureusement on peut facilement l’installer. Sous Windows et Mac, il y a des builds tout chauds.

Pour linux, en attendant un repo tierce partie ou l’upgrade du système, on peut l’installer quelques commandes depuis les sources. Par exemple pour les distros basées sur Debian comme Ubuntu, ça ressemble à :

$ # dependances pour compiler python
$ sudo apt-get install build-essential libreadline-dev tk8.4-dev libsqlite3-dev libgdbm-dev libreadline6-dev liblzma-dev libbz2-dev libncurses5-dev libssl-dev python3-dev tk-dev
$ sudo apt-get build-dep python3 # juste pour etre sur :)
 
$ # téléchargement des sources
$ cd /tmp
$ wget https://www.python.org/ftp/python/3.5.0/Python-3.5.0.tar.xz
$ tar -xvf Python-3.5.0.tar.xz
$ cd Python-3.5.0
 
$ # et on build
$ ./configure
$ make
$ sudo make altinstall 
# pas 'make install' qui écrase le python du système !
 
$ python3.5 # ahhhhhh
Python 3.5.0 (default, Sep 16 2015, 10:44:14) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Sur les centos-likes, c’est grosso merdo la même chose, sans le build-dep (mais plutôt un truc genre sudo yum groupinstall 'Development Tools'), et en remplaçant les -dev par -devel.

Nouvel opérateur

@ est maintenant le nouvel opérateur de produit matriciel, mais il ne fait officiellement rien.

Comprenez par là que Python implémente l’opérateur, mais pas le produit en lui-même, la feature ayant été spécialement incluse pour faire plaisir aux utilisateurs de libs scientifiques type numpy.

On va donc tester ça sur le terrain. On se fait un petit env temporaire avec pew et on s’installe numpy :

pew mktmpenv -p python3.5
pip install pip setuptools --upgrade
pip install numpy 
# encore un peu de compilation

Testons mon bon. L’ancienne manière de faire :

>>> a = np.array([[1, 0], [0, 1]])
>>> b = np.array([[4, 1], [2, 2]])
>>> np.dot(a, b)
array([[4, 1],
       [2, 2]])

Et la nouvelle :

>>> a @ b
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-3d41f06f59bb> in <module>()
----> 1 a @ b
 
TypeError: unsupported operand type(s) for @: 'numpy.ndarray' and 'numpy.ndarray'

Woops, apparemment numpy n’a pas encore implémenté le truc.

Bon. Bon, bon, bon. Comment on va tester alors… Ah, oui, y a une magic method :

class Array(np.ndarray):
    def __matmul__(self, other):
        return np.dot(self, other)
 
>>> a = a.view(Array)
>>> b = b.view(Array)
>>> a @ b
Array([[4, 1],
       [2, 2]])

Bon, voilà ce que ça donnera quand les devs de numpy auront implémenté le bouzin (la dernière ligne hein, pas tout le bordel avant).

Apparemment ça fait bander les matheux, donc je suppose que c’est une super nouvelle.

% is back on bytes

En python 2, on pouvait faire "truc %s" % "bidule" et u"truc %s" % u"bidule" et b"truc %s" % u"bidule" et ça a été viré en python 3 qui ne garde % que pour str et pas pour bytes.

Ca n’aurait pas été un problème si ce n’est que Python est très utilisé pour le réseau, et que construire un paquet qui mélange de la sémantique binaire et textuelle devient soudainement une grosse soupe de decode() et encode().

Jour 1, test 3, suspense…

>>> bytearray([66, 108, 117, 101, 32, 112, 114, 105, 101, 115, 116, 32, 115, 97, 121, 115, 58, 32, 37, 115]) % b"wololo"
bytearray(b'Blue priest says: wololo')

Voilà ça c’est fait !

os.scandir()

os.path.walk() est dans mon top 10 des APIs que je déteste le plus en Python, juste à côté de la gestion timezone. Avoir os.walk() en Python 3 qui retourne un générateur me ravit. Avoir une version 10 X plus rapide avec scandir, n’est-ce pas choupinet ?

>>> import os
>>> list(os.scandir('/tmp/'))
                       [<DirEntry 'systemd-private-316509818ceb41488a4721c78dabb603-colord.service-eXUfPo'>,
 <DirEntry 'unity_support_test.0'>,
 <DirEntry 'config-err-7UpWeO'>,
 <DirEntry '.ICE-unix'>,
 <DirEntry 'pip-rw_63q0_-unpack'>,
 <DirEntry 'systemd-private-316509818ceb41488a4721c78dabb603-systemd-timesyncd.service-eZumpq'>]

C’est très dommage que ça ne retourne pas des objets Path de pathlib, mais bon, les perfs, tout ça…

Zipapp, le grand inaperçu

Le saviez-vous ? Python peut exécuter un zip, ce qui permet de créer un script en plusieurs fichiers et de le partager comme un seul fichier. Non vous ne le saviez-vous-te-pas car personne n’en parle jamais.

La 3.5 vient avec un outil en ligne de commande pour faciliter la création de tels zip et une nouvelle extension (que l’installeur fera reconnaitre à Windows) pour cesdits fichiers : .pyz.

Je fais mon script :

foo
├── bar.py
├── __init__.py
└── __main__.py

__main__.py est obligatoire, c’est ce qui sera lancé quand on exécutera notre script. Dedans je mets import bar et dans bar print('wololo again').

Ensuite je fusionne tout ça :

python -m zipapp foo

Et pouf, j’ai mon fichier foo.pyz :

$ python3.5  foo.pyz
wololo again

Attention aux imports dedans, ils sont assez chiants à gérer.

L’unpacking généralisé

J’adore cette feature. J’adore toutes les features de la 3.5. Cette release est fantastique. Depuis la 3.3 chaque release est fantastique.

Mais bon, zeste de savoir l’a traité en long et en large donc rien à dire de plus, si ce n’est que j’avais raté un GROS truc :

  • On peut faire de l’unpacking sur n’importe quel itérable.
  • On peut faire de l’unpacking dans les tuples.
  • Les parenthèses des tuples sont facultatives.

Donc ces syntaxes sont valides :

>>> *range(2), *[1, 3], *'ea'
(0, 1, 1, 3, 'e', 'a')
>>> *[x * x for x in range(3)], *{"a": 1}.values()
(0, 1, 4, 1)

Ce qui peut être très chouette et aussi la porte ouverte à l’implémentation d’un sous-ensemble de Perl en Python. C’est selon l’abruti qui code.

Type hints

Ce qu’il faut bien comprendre avec les types hints, c’est que Python ne s’en sert pas. Il n’en fait rien. Que dalle. Nada. Peau de balle. Zob. Niet. Zero. La bulle. Néant. Null. None. Réforme gouvernementale.

Les types hints sont disponibles, mais Python ne va pas les traiter différemment d’autres annotations. Le but est de permettre à des outils externes (linter, IDE, etc) de se baser sur ces informations pour ajouter des fonctionnalités.

Pour l’instant, un seul le fait : mypy.

Et là on sent bien que tout ça est tout neuf car si on fait pip install mypy-lang, on tombe sur une version buggée. Il faut donc l’installer directement depuis le repo, soit :

pip install https://github.com/JukkaL/mypy/archive/master.zip

Puis écriture d’une fonction annotée avec des types hints :

 
from typing import Iterable, Tuple
 
PixelArray = Iterable[Tuple[int, int, int]]
 
def rgb2hex(pixels: PixelArray) -> list:
    pattern = "#{0:02x}{1:02x}{2:02x}"
    return [pattern.format(r, g, b) for r, g, b in pixels]
 
 
# ça marche :
rgb2hex([(1, 2, 3), (1, 2, 3)])
# ['#010203', '#010203']

La preuve que Python n’en fait rien :

>>> hex("fjdkls")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-be62b8f062fe> in <module>()
----> 1 hex("fjdkls")
 
TypeError: 'str' object cannot be interpreted as an integer

Même la doc profite peu du typage :

Help on function rgb2hex in module __main__:

rgb2hex(pixels:typing.Iterable) -> list

Mais si on met ça dans un fichier foo.py :

from essai import rgb2hex
 
print(rgb2hex("fdjksl"))
res = rgb2hex([(1, 2, 3), (3, 4, 5)])
print(res + 1)

Et qu’on le passe à la moulinette :

$ mypy foo.py 
foo.py:3: error: Argument 1 to "rgb2hex" has incompatible type "str"; expected Iterable[...]
foo.py:5: error: Unsupported operand types for + (List[Any] and "int")

Ensuite j’ai essayé de créer un stub file, c’est-à-dire de mettre les hints dans un fichier à part plutôt que directement dans le code. Ma fonction redevient :

def rgb2hex(pixels):
    pattern = "#{0:02x}{1:02x}{2:02x}"
    return [pattern.format(r, g, b) for r, g, b in pixels]

Et mon fichier stub (même nom, mais avec extension .pyi) contient :

from typing import Iterable, Tuple
 
PixelArray = Iterable[Tuple[int, int, int]]
 
def rgb2hex(pixels: PixelArray) -> list:...

Les stubs sont donc bien des fichiers Python valides, mais avec une extension différente, et juste les signatures des fonctions (le corps du bloc est une Ellipsis).

Et poof, ça marche pareil :

$ mypy foo.py 
foo.py:3: error: Argument 1 to "rgb2hex" has incompatible type "str"; expected Iterable[...]
foo.py:5: error: Unsupported operand types for + (List[Any] and "int")

Il y a un repo qui contient des fichiers stubs pour la stdlib. Vous pouvez y participer, c’est un moyen simple de contribuer à Python.

Bref, pour le moment ça demande encore un peu de maturité, mais je pense que d’ici quelques mois on aura des outils bien rodés pour faire tout ça automatiquement.

Async/await

La feature pub. Techniquement le truc qui a fait dire à tous ceux qui voulaient de l’asyncrone que Python en fait, c’était trop cool. Sauf que Python pouvait faire ça avec yield from avant, mais c’est sur que c’était super confusionant.

Maintenant on a un truc propre : pas de décorateur @coroutine, pas de syntaxe semblable aux générateurs, mais des méthodes magiques comme __await__ et de jolis mots-clés async et await.

Vu que Crossbar est maintenant compatible Python 3, et qu’il supporte asyncio pour les clients… Si on s’implémentait un petit wrapper WAMP pour s’amuser à voir ce que ressemblerait une API moderne pour du Websocket en Python ?

pip install crossbar
crossbar init
crossbar start

(Ouhhhh, plein de zolies couleurs apparaissent dans ma console ! Ils ont fait des efforts cosmétiques chez Tavendo)

Bien, voici maintenant l’exemple d’un client WAMP de base codé avec asyncio selon l’ancienne API :

import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
 
class MyComponent(ApplicationSession):
 
    @asyncio.coroutine
    def onJoin(self, details):
 
        # on marque cette fonction comme appelable
        # a distance en RPC
        def add(a, b):
            return a + b
        self.register(add, "add")
 
        # et on triche en l'appelant cash. J'ai
        # la flemme de coder un deuxième client
        # et ça passe quand même par le routeur
        # donc merde
        res = yield from self.call("add", 2, 3)
        print("Got result: {}".format(res))
 
 
if __name__ == '__main__':
    runner = ApplicationRunner("ws://127.0.0.1:8080/ws",
        u"crossbardemo",
        debug_wamp=False,  # optional; log many WAMP details
        debug=False,  # optional; log even more details
    )
    runner.run(MyComponent)

Et ça marche nickel en 3.5. Mais qu’est-ce que c’est moche !

On est en train de bosser sur l’amélioration de l’API, mais je pense que ça va reste plus bas niveau que je le voudrais.

Donc, amusons-nous un peu à coder un truc plus sexy. Je vous préviens, le code du wrapper est velu, j’avais envie de me marrer un peu après les exemples ballots plus haut :

import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
 
class App:
 
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.procedures = []
        self.subscriptions = []
        self.event_handlers  = {}
 
    def run(self, url="ws://127.0.0.1:8080/ws",
                realm="realm1", debug_wamp=False, debug=False):
        runner = ApplicationRunner(url, realm,
                                                     debug_wamp=debug_wamp,
                                                     debug=debug)
        runner.run(self)
 
    def run_cmd(self, *args, **kwargs):
        # et on pourrait même ici mettre du parsing d'argument
        # et de os.environ, mais j'ai la flemme
        if __name__ == '__main__':
            self.run(*args, **kwargs)
 
    # quelques décorateurs pour faire du déclaratif
    # et remettre les paramètres dans le bon ordre
    def register(self, name, *args, **kwargs):
            def wrapper(proc):
                self.procedures.append([name, proc, args, kwargs])
                return proc
            return wrapper
 
    def subscribe(self, topic, *args, **kwargs):
            def wrapper(callback):
                self.procedures.append([topic, callback, args, kwargs])
                return callback
            return wrapper
 
    # un système d'event interne
    def on(self, event):
            def wrapper(callback):
                self.event_handlers.setdefault(event, []).append(callback)
                return callback
            return wrapper
 
    async def trigger(self, event):
        for callback in self.event_handlers.get(event, ()):
            await callback(self.session)
 
    # un peu de code de compatibilité avec l'API initiale
    def __call__(self, *args):
        class CustomeSession(ApplicationSession):
            async def onJoin(session_self, details):
 
                # on joint on fait tous les registers et tous les
                # subscribes
                for name, proc, args, kwargs in self.procedures:
                     session_self.register(proc, name, *args, **kwargs)
 
                for topic, callback, args, kwargs in self.subscriptions:
                     session_self.subscribe(proc, topic, *args, **kwargs)
 
                # on appelle les handlers de notre event
                await self.trigger('joined')
        self.session = CustomeSession(*args)
        return self.session

Évidement la coloration syntaxique ne suit pas sur nos async/await.

Bon, vous allez me dire, mais ça quoi ça sert tout ça ? Et bien, c’est une version tronquée et codée à l’arrache de l’API Application pour Twisted… mais version asyncio.

C’est-à-dire que c’est une lib qui permet de faire le même exemple que le tout premier qu’on a vu dans cette partie – qui souvenez-vous était fort moche -, mais comme ça :

app = App()
 
@app.register('add')
async def add(a, b):
    return a + b
 
@app.on('joined')
async def _(session):
    res = await session.call("add", 2, 3)
    print("Got result: {}".format(res))
 
app.run_cmd()

Des jolis décorateurs ! Des jolis async ! Des jolis await !

Et tout ça tourne parfaitement sur 3.5 messieurs-dames.

Bref, on peut faire du WAMP avec une syntaxe claire et belle, il faut juste se bouger le cul pour coder une abstraction un peu propre.

Je pense que autobahn restera toujours un peu bas niveau. Donc il va falloir que quelqu’un se colle à faire une lib pour wrapper tout ça.

Des volontaires ?

Arf, je savais bien que ça allait me retomber sur la gueule.

]]>
http://sametmax.com/jouons-un-peu-avec-python-3-5/feed/ 27
async / await, la feature de dernière minute de Python 3.5 9 http://sametmax.com/async-await-la-feature-de-derniere-minute-de-python-3-5/ http://sametmax.com/async-await-la-feature-de-derniere-minute-de-python-3-5/#comments Fri, 24 Apr 2015 18:47:53 +0000 http://sametmax.com/?p=16116 formalisée début avril et toujours en draft.]]> 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 :

  • Je ne voyais pas l’intérêt d’ajouter des mots clés pour faire un truc qu’on peut déjà faire. Le faible nombre de key words dans Python est pour moi une feature.
  • Il n’y a rien que ça permette qu’on ne pouvait pas faire avant.
  • Ça me parait un peu fait à la dernière minute, introduit comme un voleur avant la release.
  • C’est franchement moche le 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 :

  • On distingue clairement le code qui est pour créer des générateurs, et le code asynchrone. Ce sont des choses qui n’ont sémantiquement rien à voir, et utiliser yield from pour les deux rendait tout ça bien confus.
  • C’est moins à taper : pas besoin d’importer un décorateur de 3 km et l’apposer, et quand on a pas mal de point d’arrêt, await c’est moins chiant à tapper que yield from et plus joli.
  • Ca évite pas mal de bug subtiles qui se glissent quand on utilise une coroutine pour la première fois sans comprendre tout le système. Par exemple si on vire tous les yield from d’une coroutine, c’est la merde, ce qui n’est pas le cas pour les await.
  • Quand on fait une boucle sur un generateur on sait tout de suite si on va avoir un point d’arrêt ou non.
  • Les mêmes mots clés sont utilisés dans d’autre langage, ce qui permet aux migrants de comprendre le principe bien plus vite qu’avec 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.

]]>
http://sametmax.com/async-await-la-feature-de-derniere-minute-de-python-3-5/feed/ 9