Усовершенствовать API синхронного программного обеспечения, чтобы разрешить асинхронное использование - PullRequest
0 голосов
/ 28 ноября 2018

У меня есть модуль в Python 3.5+, предоставляющий функцию, которая считывает некоторые данные из удаленного веб-API и возвращает их.Функция опирается на функцию-оболочку, которая, в свою очередь, использует библиотеку requests для выполнения HTTP-вызова.

Вот она (специально исключая всю логику проверки данных и обработку исключений):

# module fetcher.py

import requests

# high-level module API
def read(some_params):
    resp = requests.get('http://example.com', params=some_params)
    return resp.json()

# wrapper for the actual remote API call
def get_data(some_params):
    return call_web_api(some_params)

Модуль в настоящее время импортируется и используется несколькими клиентами.

На сегодняшний день вызов get_data по своей сути синхронный : это означает, что тот, кто использует функцию fetcher.read(), знаетчто это заблокирует поток, в котором выполняется функция.

Чего я хотел бы добиться

Я хочу разрешить запуск fetcher.read() обоих синхронно и асинхронно (например, через цикл обработки событий).Это сделано для того, чтобы сохранить совместимость с существующими абонентами, использующими модуль, и в то же время предложить возможность использовать неблокирующие вызовы, чтобы обеспечить лучшую пропускную способность для абонентов, которые хотят вызывать функцию асинхронно.

Тем не менее, мое законное желание состоит в том, чтобы как можно меньше модифицировать исходный код ...

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

Каким образом необходимо изменить вышеуказанный код, чтобы соответствовать моим пожеланиям? Что также заставляет меня спросить: Есть ли лучшая практика по улучшению программных API синхронизации для асинхронных контекстов?

1 Ответ

0 голосов
/ 28 ноября 2018

Я хочу разрешить запуск fetcher.read() как синхронно, так и асинхронно (например, через цикл обработки событий).

Не думаю, что этовозможно использование одной и той же функции через API синхронизации и асинхронного API, потому что шаблоны использования очень разные.Даже если бы вы могли как-то заставить это работать, было бы слишком легко все испортить, особенно учитывая динамическую типизацию Python.(Например, пользователи могут случайно забыть await свои функции в асинхронном коде, и код синхронизации будет срабатывать, блокируя таким образом их цикл событий.)

Вместо этого я бы рекомендовал фактический API быть асинхронными создать тривиальную оболочку синхронизации, которая просто вызывает точки входа, используя run_until_complete.Что-то вроде этого:

# new module afetcher.py (or fetcher_async, or however you like it)

import aiohttp

# high-level module API
async def read(some_params):
    async with aiohttp.request('GET', 'http://example.com', params=some_params) as resp:
        return await resp.json()

# wrapper for the actual remote API call
async def get_data(some_params):
    return call_web_api(some_params)

Да, вы переключаетесь с использования requests на aiohttp, но изменение является механическим, поскольку API очень похожи по духу.

Синхронизациямодуль будет существовать для обратной совместимости и удобства и будет тривиально оборачивать асинхронную функциональность:

# module fetcher.py

import afetcher

def read(some_params):
    loop = asyncio.get_event_loop()
    return loop.run_until_complete(afetcher.read(some_params))

...

Этот подход обеспечивает как синхронизацию, так и асинхронную версию API, без дублирования кода, потому что версия синхронизации состоит из тривиальных трамплинов,чье определение может быть дополнительно сжато с помощью соответствующих декораторов.

Модуль асинхронного извлечения должен иметь красивое короткое имя, чтобы пользователи не чувствовали себя наказанными за использование асинхронных функций.Он должен быть простым в использовании, и на самом деле он предоставляет множество новых функций по сравнению с API-интерфейсом синхронизации, в частности распараллеливание с минимальными издержками и надежное аннулирование.

Маршрут, который не рекомендуетсяиспользует run_in_executor или аналогичный инструмент на основе потоков для запуска requests в пуле потоков под колпаком.Эта реализация не дает реальных преимуществ использования asyncio, но несет все расходы.В этом случае лучше продолжить предоставление синхронного API и предоставить пользователям возможность использовать concurrent.futures или аналогичные инструменты для параллельного выполнения, где они хотя бы знают, что используют потоки.

...