web-dev-qa-db-fra.com

Comment créer un générateur asynchrone en Python?

J'essaie de réécrire ce code Python2.7 dans le nouvel ordre mondial asynchrone:

def get_api_results(func, iterable):
    pool = multiprocessing.Pool(5)
    for res in pool.map(func, iterable):
        yield res

map() bloque jusqu'à ce que tous les résultats aient été calculés, j'essaie donc de réécrire ceci en tant qu'implémentation asynchrone qui produira des résultats dès qu'ils seront prêts. Comme map(), les valeurs de retour doivent être retournées dans le même ordre que iterable. J'ai essayé ceci (j'ai besoin de requests en raison des exigences d'authentification héritées):

import requests

def get(i):
    r = requests.get('https://example.com/api/items/%s' % i)
    return i, r.json()

async def get_api_results():
    loop = asyncio.get_event_loop()
    futures = []
    for n in range(1, 11):
        futures.append(loop.run_in_executor(None, get, n))
    async for f in futures:
        k, v = await f
        yield k, v

for r in get_api_results():
    print(r)

mais avec Python 3.6 je reçois:

  File "scratch.py", line 16, in <module>
    for r in get_api_results():
TypeError: 'async_generator' object is not iterable

Comment puis-je accomplir cela?

13
Erik Cederstrand

En ce qui concerne votre ancien code (2.7) - le multitraitement est considéré comme un puissant remplacement pour le module de threading beaucoup plus simple pour le traitement simultané de tâches gourmandes en processeur, où le threading ne fonctionne pas si bien. Votre code n'est probablement pas lié au processeur - car il suffit de faire des requêtes HTTP - et le threading aurait peut-être suffi pour résoudre votre problème.

Cependant, au lieu d'utiliser threading directement, Python 3+ a un module Nice appelé concurrent.futures celui avec une API plus propre via cool Executor classes. Ce module est également disponible pour python 2.7 en tant que package externe .

Le code suivant fonctionne sur python 2 et python 3:

# For python 2, first run:
#
#    pip install futures
#
from __future__ import print_function

import requests
from concurrent import futures

URLS = [
    'http://httpbin.org/delay/1',
    'http://httpbin.org/delay/3',
    'http://httpbin.org/delay/6',
    'http://www.foxnews.com/',
    'http://www.cnn.com/',
    'http://europe.wsj.com/',
    'http://www.bbc.co.uk/',
    'http://some-made-up-domain.coooom/',
]


def fetch(url):
    r = requests.get(url)
    r.raise_for_status()
    return r.content


def fetch_all(urls):
    with futures.ThreadPoolExecutor(max_workers=5) as executor:
        future_to_url = {executor.submit(fetch, url): url for url in urls}
        print("All URLs submitted.")
        for future in futures.as_completed(future_to_url):
            url = future_to_url[future]
            if future.exception() is None:
                yield url, future.result()
            else:
                # print('%r generated an exception: %s' % (
                # url, future.exception()))
                yield url, None


for url, s in fetch_all(URLS):
    status = "{:,.0f} bytes".format(len(s)) if s is not None else "Failed"
    print('{}: {}'.format(url, status))

Ce code utilise futures.ThreadPoolExecutor, Basé sur le filetage. Une grande partie de la magie est dans as_completed() utilisée ici.

Votre code python 3.6 ci-dessus, utilise run_in_executor() qui crée une futures.ProcessPoolExecutor(), et n'utilise pas vraiment d'E/S asynchrone !!

Si vous voulez vraiment continuer avec asyncio, vous devrez utiliser un client HTTP qui prend en charge asyncio, tel que aiohttp . Voici un exemple de code:

import asyncio

import aiohttp


async def fetch(session, url):
    print("Getting {}...".format(url))
    async with session.get(url) as resp:
        text = await resp.text()
    return "{}: Got {} bytes".format(url, len(text))


async def fetch_all():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, "http://httpbin.org/delay/{}".format(delay))
                 for delay in (1, 1, 2, 3, 3)]
        for task in asyncio.as_completed(tasks):
            print(await task)
    return "Done."


loop = asyncio.get_event_loop()
resp = loop.run_until_complete(fetch_all())
print(resp)
loop.close()

Comme vous pouvez le voir, asyncio possède également une as_completed(), qui utilise désormais une véritable E/S asynchrone, en utilisant un seul thread sur un processus.

12
Udi

Vous mettez votre boucle d'événement dans une autre co-routine. Ne fais pas ça. La boucle d'événements est le "pilote" le plus externe du code asynchrone et doit être exécutée de manière synchrone.

Si vous devez traiter les résultats récupérés, écrivez plus de coroutines qui le font. Ils pourraient extraire les données d'une file d'attente ou conduire directement la récupération.

Vous pouvez avoir une fonction principale qui récupère et traite les résultats, par exemple:

async def main(loop): 
    for n in range(1, 11):
        future = loop.run_in_executor(None, get, n)
        k, v = await future
        # do something with the result

loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))

Je ferais aussi de la fonction get() correctement asynchrone en utilisant une bibliothèque asynchrone comme aiohttp afin que vous n'ayez pas du tout à utiliser l'exécuteur.

6
Martijn Pieters