Как я могу сделать набор функций, которые могут использоваться как синхронно, так и асинхронно? - PullRequest
0 голосов
/ 03 ноября 2018

Представьте, что у меня есть такой набор функций:

def func1():
    func2()

def func2():
    time.sleep(1)  # simulate I/O operation
    print('done')

Я хочу, чтобы их можно было использовать синхронно:

# this would take two seconds to complete
func1()
func1()

а также асинхронно, например, вот так:

# this would take 1 second to complete
future = asyncio.gather(func1.run_async(), func1.run_async())
loop = asyncio.get_event_loop()
loop.run_until_complete(future)

Проблема, конечно, в том, что func1 каким-то образом должен распространять "контекст", в котором он работает (синхронно или асинхронно), на func2.

Я хочу избежать написания асинхронного варианта каждой из моих функций, потому что это приведет к большому количеству дублирующегося кода:

def func1():
    func2()

def func2():
    time.sleep(1)  # simulate I/O operation
    print('done')

# duplicate code below...
async def func1_async():
    await func2_async()

async def func2_async():
    await asyncio.sleep(1)  # simulate I/O operation
    print('done')

Есть ли способ сделать это без необходимости выполнения асинхронной копии всех моих функций?

Ответы [ 2 ]

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

Итак, я нашел способ достичь этого, но, поскольку это буквально первый раз, когда я что-то сделал с async, я не могу гарантировать, что в нем нет ошибок или что это не ужасная идея.

Концепция на самом деле довольно проста: определите ваши функции как обычные асинхронные функции, используя async def и await, где это необходимо, а затем добавьте обертку вокруг них, которая автоматически ожидает функцию , если без цикла обработки событий бежит. Подтверждение концепции:

import asyncio
import functools
import time


class Hybrid:
    def __init__(self, func):
        self._func = func

        functools.update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        coro = self._func(*args, **kwargs)

        loop = asyncio.get_event_loop()

        if loop.is_running():
            # if the loop is running, we must've been called from a
            # coroutine - so we'll return a future
            return loop.create_task(coro)
        else:
            # if the loop isn't running, we must've been called synchronously,
            # so we'll start the loop and let it execute the coroutine
            return loop.run_until_complete(coro)

    def run_async(self, *args, **kwargs):
        return self._func(*args, **kwargs)


@Hybrid
async def func1():
    await func2()

@Hybrid
async def func2():
    await asyncio.sleep(0.1)


def twice_sync():
    func1()
    func1()

def twice_async():
    future = asyncio.gather(func1.run_async(), func1.run_async())
    loop = asyncio.get_event_loop()
    loop.run_until_complete(future)


for func in [twice_sync, twice_async]:
    start = time.time()
    func()
    end = time.time()
    print('{:>11}: {} sec'.format(func.__name__, end-start))

# output:
#  twice_sync: 0.20142340660095215 sec
# twice_async: 0.10088586807250977 sec

Однако этот подход имеет свои ограничения. Если у вас есть синхронная функция, вызывающая гибридную функцию, вызов синхронной функции из асинхронной функции изменит ее поведение:

@hybrid
async def hybrid_function():
    return "Success!"

def sync_function():
    print('hybrid returned:', hybrid_function())

async def async_function():
    sync_function()

sync_function()  # this prints "Success!" as expected

loop = asyncio.get_event_loop()
loop.run_until_complete(async_function())  # but this prints a coroutine

Позаботьтесь об этом!

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

Вот мой "не ответ на вопрос", который я знаю, что переполнение стека любит ...

Есть ли способ сделать это без реализации асинхронной копии всех моих функций?

Я не думаю, что есть. Создание «общего переводчика» для преобразования функций в собственные сопрограммы кажется практически невозможным. Это потому, что сделать синхронную функцию асинхронной - это больше, чем бросить перед ней ключевое слово async и пару операторов await. Имейте в виду, что все, что вы await должно быть awaitable .

Ваш def func2(): time.sleep(1) иллюстрирует этот момент. Синхронные функции будут выполнять блокирующие вызовы, такие как time.sleep(); асинхронные (нативные сопрограммы) будут ожидать неблокирующих сопрограмм. Как вы указали, для того, чтобы сделать эту функцию асинхронной, нужно не просто использовать async def func(), но ожидать asyncio.sleep(). Допустим, вместо time.sleep() вы вызываете более сложную блокирующую функцию. Вы создаете какой-то необычный декоратор, который накладывает атрибут функции , называемый run_async, который вызывается, на декорированную функцию. Но как этот декоратор узнает, как "преобразовать" блокирующие вызовы в func2() в их эквиваленты сопрограмм, если они даже определены? Я не могу придумать какой-нибудь магии, которая была бы достаточно умна, чтобы преобразовать все вызовы в синхронной функции в их await способных аналогов.

В ваших комментариях вы упоминаете, что это для HTTP-запросов. В качестве примера можно привести различия в сигнатурах вызовов и API-интерфейсах между пакетами requests и aiohttp. В aiohttp, .text() - это экземпляр , метод ; в requests, .text является свойством . Как вы могли бы создать что-то достаточно умное, чтобы знать такие различия?

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

...