PROJET AUTOBLOG


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

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

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

En attendant asyncio

vendredi 17 janvier 2014 à 15:09

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.

Sans programmation asynchrone

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…

Avec programmation asynchrone

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.

flattr this!

Error happened! 0 - count(): Argument #1 ($value) must be of type Countable|array, null given In: /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php:428 http://www.ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/?En-attendant-asyncio #0 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(999): VroumVroum_Blog->update() #1 /var/www/ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/index.php(1): require_once('...') #2 {main}