Выход из генератора Async в Python AsyncIO

107
11

У меня есть простой класс, который использует асинхронный генератор для получения списка URL-адресов:

import aiohttp
import asyncio
import logging
import sys

LOOP = asyncio.get_event_loop()
N_SEMAPHORE = 3

FORMAT = '[%(asctime)s] - %(message)s'
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format=FORMAT)
logger = logging.getLogger(__name__)

class ASYNC_GENERATOR(object):
def __init__(self, n_semaphore=N_SEMAPHORE, loop=LOOP):
self.loop = loop
self.semaphore = asyncio.Semaphore(n_semaphore)
self.session = aiohttp.ClientSession(loop=self.loop)

async def _get_url(self, url):
"""
Sends an http GET request to an API endpoint
"""

async with self.semaphore:
async with self.session.get(url) as response:
logger.info(f'Request URL: {url} [{response.status}]')
read_response = await response.read()

return {
'read': read_response,
'status': response.status,
}

def get_routes(self, urls):
"""
Wrapper around _get_url (multiple urls asynchronously)

This returns an async generator
"""

# Asynchronous http GET requests
coros = [self._get_url(url) for url in urls]
futures = asyncio.as_completed(coros)
for future in futures:
yield self.loop.run_until_complete(future)

def close(self):
self.session._connector.close()

Когда я выполняю эту основную часть кода:

if __name__ == '__main__':
ag = ASYNC_GENERATOR()
urls = [f'https://httpbin.org/get?x={i}' for i in range(10)]
responses = ag.get_routes(urls)
for response in responses:
response = next(ag.get_routes(['https://httpbin.org/get']))
ag.close()

Журнал распечатывает:

[2018-05-15 12:59:49,228] - Request URL: https://httpbin.org/get?x=3 [200]
[2018-05-15 12:59:49,235] - Request URL: https://httpbin.org/get?x=2 [200]
[2018-05-15 12:59:49,242] - Request URL: https://httpbin.org/get?x=6 [200]
[2018-05-15 12:59:49,285] - Request URL: https://httpbin.org/get?x=5 [200]
[2018-05-15 12:59:49,290] - Request URL: https://httpbin.org/get?x=0 [200]
[2018-05-15 12:59:49,295] - Request URL: https://httpbin.org/get?x=7 [200]
[2018-05-15 12:59:49,335] - Request URL: https://httpbin.org/get?x=8 [200]
[2018-05-15 12:59:49,340] - Request URL: https://httpbin.org/get?x=4 [200]
[2018-05-15 12:59:49,347] - Request URL: https://httpbin.org/get?x=1 [200]
[2018-05-15 12:59:49,388] - Request URL: https://httpbin.org/get?x=9 [200]
[2018-05-15 12:59:49,394] - Request URL: https://httpbin.org/get [200]
[2018-05-15 12:59:49,444] - Request URL: https://httpbin.org/get [200]
[2018-05-15 12:59:49,503] - Request URL: https://httpbin.org/get [200]
[2018-05-15 12:59:49,553] - Request URL: https://httpbin.org/get [200]
[2018-05-15 12:59:49,603] - Request URL: https://httpbin.org/get [200]
[2018-05-15 12:59:49,650] - Request URL: https://httpbin.org/get [200]
[2018-05-15 12:59:49,700] - Request URL: https://httpbin.org/get [200]
[2018-05-15 12:59:49,825] - Request URL: https://httpbin.org/get [200]
[2018-05-15 12:59:49,875] - Request URL: https://httpbin.org/get [200]
[2018-05-15 12:59:49,922] - Request URL: https://httpbin.org/get [200]

Поскольку responses являются асинхронным генератором, я ожидаю, что он даст один ответ от асинхронного генератора (который должен отправлять только запрос при фактическом уступке), отправить отдельный запрос конечной точке без параметра x, а затем дать следующий ответ от асинхронный генератор. Это должно переворачиваться назад и вперед между запросом с параметром x и запросом без параметров. Вместо этого он дает все ответы от асинхронного генератора с параметром x а затем все запросы https, которые не имеют параметров.

Что-то подобное происходит, когда я делаю:

ag = ASYNC_GENERATOR()
urls = [f'https://httpbin.org/get?x={i}' for i in range(10)]
responses = ag.get_routes(urls)
next(responses)
response = next(ag.get_routes(['https://httpbin.org/get']))
ag.close()

И лог-гравюры:

[2018-05-15 13:08:38,643] - Request URL: https://httpbin.org/get?x=8 [200]
[2018-05-15 13:08:38,656] - Request URL: https://httpbin.org/get?x=1 [200]
[2018-05-15 13:08:38,681] - Request URL: https://httpbin.org/get?x=3 [200]
[2018-05-15 13:08:38,695] - Request URL: https://httpbin.org/get?x=4 [200]
[2018-05-15 13:08:38,717] - Request URL: https://httpbin.org/get?x=6 [200]
[2018-05-15 13:08:38,741] - Request URL: https://httpbin.org/get?x=2 [200]
[2018-05-15 13:08:38,750] - Request URL: https://httpbin.org/get?x=0 [200]
[2018-05-15 13:08:38,773] - Request URL: https://httpbin.org/get?x=9 [200]
[2018-05-15 13:08:38,792] - Request URL: https://httpbin.org/get?x=7 [200]
[2018-05-15 13:08:38,803] - Request URL: https://httpbin.org/get?x=5 [200]
[2018-05-15 13:08:38,826] - Request URL: https://httpbin.org/get [200]

Вместо этого я хочу:

[2018-05-15 13:08:38,643] - Request URL: https://httpbin.org/get?x=8 [200]
[2018-05-15 13:08:38,826] - Request URL: https://httpbin.org/get [200]

Есть моменты, когда я хочу сначала получить все ответы, прежде чем делать что-либо еще. Тем не менее, есть также моменты, когда я хочу вставлять и делать промежуточные запросы до получения следующего элемента из генератора (т.е. Генератор возвращает результаты из результатов постраничного поиска, и я хочу обработать дальнейшие ссылки с каждой страницы, прежде чем переходить на следующую страницу).

Что мне нужно изменить для достижения требуемого результата?

спросил(а) 2021-01-19T12:44:10+03:00 3 месяца, 2 недели назад
1
Решение
87

Оставляя в стороне технический вопрос, являются ли responses асинхронным генератором (это не так, поскольку Python использует этот термин), ваша проблема кроется в as_completed. as_completed запускает кучу сопрограмм параллельно и предоставляет средства для получения своих результатов по мере их завершения. То, что фьючерсы, выполняемые параллельно, не совсем очевидны из документации, но имеет смысл, если учесть, что исходные concurrent.futures.as_completed работают с потоковыми фьючерсами, у которых нет выбора, кроме как запускаться параллельно. Концептуально то же самое относится к фьючерсам асинчо.

Ваш код получает только первый (быстрый результат), а затем начинает делать что-то еще, также используя asyncio. Остальные сопрограммы, as_completed как as_completed, не застывают только потому, что никто не собирает их результаты - они выполняют свои задания в фоновом режиме, и после того, как они сделаны, готовы await (в вашем случае кодом внутри as_completed, к которому вы as_completed используя loop.run_until_complete()). Я бы рискнул догадаться, что URL-адрес без параметров занимает больше времени, чем URL-адрес только с параметром x, поэтому он печатается после всех других сопрограмм.

Другими словами, эти печатаемые строки журнала означают, что asyncio выполняет свою работу и обеспечивает параллельное выполнение, которое вы запросили! Если вы не хотите выполнять параллельное выполнение, тогда не запрашивайте его, выполните их серийно:

def get_routes(self, urls):
for url in urls:
yield loop.run_until_complete(self._get_url(url))

Но это плохой способ использования asyncio - его основной цикл не является реентерабельным, поэтому для обеспечения возможности компоновки вы почти наверняка хотите, чтобы цикл был развернут только один раз на верхнем уровне. Обычно это делается с конструкцией типа loop.run_until_complete(main()) или loop.run_forever(). Как отметил Мартийн, вы можете добиться этого, сохранив при этом хороший API-интерфейс генератора, сделав get_routes реальным асинхронным генератором:

async def get_routes(self, urls):
for url in urls:
result = await self._get_url(url)
yield result

Теперь вы можете иметь main() сопрограмму, которая выглядит так:

async def main():
ag = ASYNC_GENERATOR()
urls = [f'https://httpbin.org/get?x={i}' for i in range(10)]
responses = ag.get_routes(urls)
async for response in responses:
# simulate 'next' with async iteration
async for other_response in ag.get_routes(['https://httpbin.org/get']):
break
ag.close()

loop.run_until_complete(main())

ответил(а) 2021-01-19T12:44:10+03:00 3 месяца, 2 недели назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема