Un peu de fun avec les décorateurs


Puisque la programmation asynchrone est au goût du jour, on se mange des callbacks un peu partout. Et ça alourdit toujours le code. Chaque langage, lib ou framework a essayé de trouver des astuces pour rendre tout ça plus digeste, et on a vu la naissance des Futures, Deferred, Promises, coroutines, yield from et autres joyeusetés.

Prenons par exemple un script Twisted. Déjà, Twisted, c’est pas vraiment l’exemple de la syntaxe Weight Watcher, ou alors si, mais avant le début du régime.

# -*- coding: utf-8 -*-
 
""" Télécharge des pages et affiche leur, de manière asynchrone """
 
import re
 
# Ceci doit être pip installé
import treq
from twisted.internet.task import react
from twisted.internet.defer import inlineCallbacks, returnValue
 
# Soit on utilise la syntaxe 'inlineCallbacks', c'est à dire avec des yields
# qui marquent les appels asynchrones.
@inlineCallbacks
def get_title(url):
    res = yield treq.get(url) # Ceci est asynchrone et non bloquant
    html = yield res.content() # Ça aussi
    try:
        val = re.search(r'', html.decode('utf8')).groups()[0]
    except:
        val = ''
 
    returnValue(val)
 
# Soit on récupère un objet defer et on ajoute un callback manuellement
def main(reactor):
 
    # Ceci est asynchrone et non bloquant
    defer = get_title('http://sametmax.com/quest-ce-quun-callback/')
 
    # Ceci arrive une fois que get_title est terminé
    def cb(title):
        print(title.upper() + '!')
 
    defer.addCallback(cb)
 
    # Pareil
    autre_defer = get_title('https://github.com/sametmax/django-quicky')
 
    def cb(title):
        print(title.upper() + '!!!')
 
    autre_defer.addCallback(cb)
 
    return defer
 
react(main)

D’une manière générale, je préfère la syntaxe à base de yields, même si elle oblige à se trimbaler le décorateur inlineCallbacks partout, à parsemer sa fonction de yields et à utiliser returnValue à la place de return puisque le mot clé est interdit dans les générateurs en Python 2.7.

Mais bon, ça reste facile à lire. On sait que les lignes avec yield, sont les appels bloquant qu’on demande à la boucle d’événements de traiter de manière asynchrone.

La syntaxe à base de callbacks est plus lourde, en revanche elle donne le contrôle sur la concurrence des callbacks puisqu’ils sont explicites au lieu d’être automatiquement ajoutés par magie. Elle parlera aussi plus aux dev Javascript qui ont l’habitude d’ajouter des callbacks manuellement.

Néanmoins, en JS, on a des fonctions anonymes plus flexibles, et on ferait donc plutôt une truc du genre :

get_title(url).then(function(title){
    # faire un truc avec le résultat
})

Et bien il se trouve qu’avec Python, bien qu’on ne le voit pas souvent, on peut avoir cette idée de la déclaration de son appel asynchrone juste au dessus de son callback, en utilisant des décorateurs.

En effet, les décorateurs ne sont que du sucre syntaxique :

@truc
def bidule():
    chose

N’est en fait qu’un raccourci pour écrire :

def bidule():
    chose
 
bidule = truc(bidule)

Du coup, on peut prendre n’importe quelle fonction, ou méthode, et l’utiliser comme décorateur :

@react
def main(reactor):
 
    then = get_title('http://sametmax.com/quest-ce-quun-callback/').addCallback
    @then
    def cb(title):
        print(title.upper() + '!')
 
    then = get_title('https://github.com/sametmax/django-quicky').addCallback
    @then
    def cb(title):
        print(title.upper() + '!!!')
 
    return cb

Et en jouant avec functools.partial, on peut faire aussi des trucs rigolos.

Non pas que cette syntaxe soit le truc indispensable à connaître et à utiliser. Mais les gens n’y pensent jamais. On utilise pas assez les décorateurs.

Par exemple, combien de fois vous avez vu :

def main():
    print('Doh')
 
if __name__ == '__main__':
    main()

Certaines libs, comme begin, font des décorateurs pour ça :

def main(func):
    if __name__ == '__main__':
        func()

Et du coup, dans son prog:

@main
def _():
    print('Doh')

Comme souvent, c’est le genre de feature qui peut être abusée, mais c’est parfois sympa de rapprocher une action juste au dessus de la fonction qui va être dans ce contexte.

J’espère ainsi vous avoir inspiré pour mettre un hack ou deux en production détournant complètement l’usage des décorateurs et ajoutant quelques gouttes de plus dans le vase de la sécurité de votre emploi, ou votre licenciement.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.