Ожидание обычной функции, которая вызывает асинхронную функцию - PullRequest
0 голосов
/ 06 февраля 2019

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

    .
├── async
│   └── foo.py
├── foo.py
└── main.py
└── requester.py

# requester.py
class Requester():
    def connect():
        return self._connect('some_address')

class AsynRequester():
    async def connect():
        return await self._connect('some_address')

# foo.py                                                                                                                                              
class Foo:
    def __init__(self, requester):
        self._requester = requester

    def connect(self):
        self.connect_info = self._requester.connect('host') # in async version it would be called by an await internally

# async/foo.py                                                                                                                                        
from foo import Foo as RawFoo

class Foo(RawFoo):
    async def connect(self):
        return await super(RawFoo, self).connect()

# main.py                                                                                                                                             
from async.foo import Foo # from foo import Foo                                                                                                       
from requester import AsynRequester # from requester import Requester

def main():
    f = Foo(AsyncRequester()) # or Foo(Requester()) where we use sync requester

    await f.connect() # or f.connect() if we are using sync methods    

Но async connect наконец вызывает соединение синхронизации типа класса синхронизации Foo (который является родителем асинхронного класса), который внутренне вызывает функцию requester.connect.Это невозможно, потому что requester.connect внутренне вызвал await connect, когда он использовался в асинхронном режиме, но вызывает без ожидания.

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

1 Ответ

0 голосов
/ 07 февраля 2019

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

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

Например, логический метод будет выглядеть так:

# common code that doesn't assume it's either sync or async
class FooRaw:
    async def connect(self):
        self.connect_info = await self._to_async(self._requester.connect(ENDPOINT))

    async def hello_logic(self):
        await self._to_async(self.connect())
        self.sock.write('hello %s %s\n' % (USERNAME, PASSWORD))
        resp = await self._to_async(self.sock.readline())
        assert resp.startswith('OK')
        return resp

При работе в asyncio такие методы, как connect и readline являются сопрограммами, поэтому следует ожидать их возвращаемого значения.С другой стороны, в коде блокировки self.connect и sock.readline есть функции синхронизации, которые возвращают конкретные значения.Но await является синтаксической конструкцией, которая присутствует или отсутствует, вы не можете отключить ее во время выполнения без дублирования кода.

Чтобы разрешить одному и тому же коду работать в режимах синхронизации и асинхронности, FooRaw.hello_logic всегда ожидает, оставляя для метода _to_async возможность обернуть результат в ожидаемый при выполнении вне asyncio.В асинхронных классах _asincify ожидает своего аргумента и возвращает результат, в основном это неоперация.В классах синхронизации он возвращает полученный объект, не ожидая его, но все же определяется как async def, поэтому его можно ожидать.В этом случае FooRaw.hello_logic по-прежнему является сопрограммой, но она никогда не приостанавливается (потому что «сопрограммы», которые она ожидает, - это все случаи _to_async, который не приостанавливается вне asyncio.)

С этим на местеасинхронная реализация hello_logic не должна ничего делать, кроме выбора правильного requester и предоставления правильного _to_async;его connect и hello_logic, унаследованные от FooRaw, выполняют правильные действия автоматически:

class FooAsync(FooRaw):
    def __init__(self):
        self._requester = AsyncRequester()

    @staticmethod
    async def _to_async(x):
        # we're running async, await X and return the result
        result = await x
        return result

В версии синхронизации, помимо реализации _to_async, потребуется обернуть логические методы для «запуска»"сопрограмма:

class FooSync(FooRaw):
    def __init__(self):
        self._requester = SyncRequester()

    @staticmethod
    async def _to_async(x):
        # we're running sync, X is the result we want
        return x

    # the following can be easily automated by a decorator

    def connect(self):
        return _run_sync(super().connect())

    def hello_logic(self):
        return _run_sync(super().hello_logic())

Обратите внимание, что сопрограмму можно запускать вне цикла событий только потому, что FooSync.hello_logic является сопрограммой только по имени;базовый запросчик использует блокирующие вызовы, поэтому FooRaw.connect и другие никогда не приостанавливаются, они завершают свое выполнение за один запуск.(Это похоже на генератор, который выполняет некоторую работу, ничего не получая.) Это свойство делает хелпер _run_sync простым:

def _run_sync(coro):
    try:
        # start running the coroutine
        coro.send(None)
    except StopIteration as e:
        # the coroutine has finished; return the result
        # stored in the `StopIteration` exception
        return e.value
    else:
        raise AssertionError("coroutine suspended")
...