Site original : Sam & Max: Python, Django, Git et du cul
La programmation asynchrone arrive en force avec la version 3.4, mais celle-ci n’est pas encore en version stable. En attendant, Python 3 possède déjà de quoi faire de la programmation asynchrone, et même parallèle, avec une bien plus grande facilité qu’en Python 2.
Si vous avez oublié le principe ou l’intérêt de la programmation asynchrone, il y a un article pour ça ©.
Pour montrer l’intérêt de la chose, nous allons utiliser un bout de code pour télécharger le code HTML de pages Web.
Le code est simple et sans chichi :
# -*- coding: utf-8 -*- import datetime from urllib.request import urlopen start_time = datetime.datetime.now() URLS = ['http://sebsauvage.net/', 'http://github.com/', 'http://sametmax.com/', 'http://duckduckgo.com/', 'http://0bin.net/', 'http://bitstamp.net/'] for url in URLS: try: # j'ignore volontairement toute gestion d'erreur évoluée result = urlopen(url).read() print('%s page: %s bytes' % (url, len(result))) except Exception as e: print('%s generated an exception: %s' % (url, e)) elsapsed_time = datetime.datetime.now() - start_time print("Elapsed time: %ss" % elsapsed_time.total_seconds())
Ce qui nous donne:
python sans_future.py http://sebsauvage.net/ page: 9036 bytes http://github.com/ page: 12582 bytes http://sametmax.com/ generated an exception: HTTP Error 502: Bad Gateway http://duckduckgo.com/ page: 8826 bytes http://0bin.net/ page: 5551 bytes http://bitstamp.net/ page: 51996 bytes Elapsed time: 25.536095s
Erreur 500 sur S&M… Mon script qui se fout de ma gueule en plus…
On utilise le module future, qui, comme sont nom l’indique, implémente des outils pour manipuler des “futures” en Python. Il inclut notamment un context manager pour créer, lancer et arrêter des workers automatiquement, et leur envoyer des tâches, puis récupérer les résultats de ces tâches sous forme de “futures”.
Pour rappel, une “future” est juste un objet qui représente le résultat d’une opération asynchrone (puisqu’on ne sait pas quand elle se termine). Cet objet contient des méthodes pour vérifier si le résultat est disponible à un instant t, et obtenir ce résultat si c’est le cas.
# -*- coding: utf-8 -*- import datetime import concurrent.futures from urllib.request import urlopen from concurrent.futures import ProcessPoolExecutor, as_completed start_time = datetime.datetime.now() URLS = ['http://sebsauvage.net/', 'http://github.com/', 'http://sametmax.com/', 'http://duckduckgo.com/', 'http://0bin.net/', 'http://bitstamp.net/'] def load_url(url): """ Le callback que vont appeler les workers pour télécharger le contenu d'un site. On peut appeler cela une 'tâche' """ return urlopen(url).read() # Un pool executor est un context manager qui va automatiquement créer des # processus Python séparés et répartir les tâches qu'on va lui envoyer entre # ces processus (appelés workers, ici on en utilise 5). with ProcessPoolExecutor(max_workers=5) as e: # On e.submit() envoie les tâches à l'executor qui les dispatch aux # workers. Ces derniers appelleront "load_url(url)". "e.submit()" retourne # une structure de données appelées "future", qui représente un accès au # résultat asynchrone, qu'il soit résolu ou non. futures_and_url = {e.submit(load_url, url): url for url in URLS} # "as_completed()" prend un iterable de future, et retourne un générateur # qui itère sur les futures au fur et à mesures que celles # ci sont résolues. Les premiers résultats sont donc les premiers arrivés, # donc on récupère le contenu des sites qui ont été les premiers à répondre # en premier, et non dans l'ordre des URLS. for future in as_completed(futures_and_url): # Une future est hashable, et peut donc être une clé de dictionnaire. # On s'en sert ici pour récupérer l'URL correspondant à cette future. url = futures_and_url[future] # On affiche le résultats contenu des sites si les futures le contienne. # Si elles contiennent une exception, on affiche l'exception. if future.exception() is not None: print('%s generated an exception: %s' % (url, future.exception())) else: print('%s page: %s bytes' % (url, len(future.result()))) elsapsed_time = datetime.datetime.now() - start_time print("Elapsed time: %ss" % elsapsed_time.total_seconds())
Et c’est quand même vachement plus rapide :
python3 avec_future.py # notez qu'on utilise Python 3 cette fois http://duckduckgo.com/ page: 8826 bytes http://sebsauvage.net/ page: 9036 bytes http://github.com/ page: 12582 bytes http://sametmax.com/ page: 50998 bytes http://0bin.net/ page: 5551 bytes http://bitstamp.net/ page: 52001 bytes Elapsed time: 3.480596s
Même si vous retirez les commentaires, le code est encore très verbeux, ce qui explique pourquoi j’attends avec impatience asyncio qui, grâce à yield from
, va intégrer l’asynchrone de manière plus naturelle au langage.
Mais ça reste beaucoup plus simple que de créer son process à la main, créer une queue, envoyer les tâches dans la queue, s’assurer que le process est arrêté, gérer les erreurs et le clean up, etc.
Notez qu’on peut remplacer ProcessPoolExecutor
par ThreadPoolExecutor
si vous n’avez pas besoin d’un process séparé mais juste de l’IO non bloquant.
Télécharger le code de larticle : avec future / sans future.