等待调用异步函数的普通函数

时间:2019-02-06 17:09:20

标签: python-3.x python-asyncio

对于一个项目,我希望能够同时具有一个库的同步和异步版本,该同步版本具有大多数逻辑部分,并且异步必须以异步方式调用该同步版本。例如,我有一个在构造函数中获取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    

但是异步connect最终调用内部调用Foo函数的requester.connect同步类类型的同步连接(是异步类的父级)。这是不可能的,因为requester.connect在异步模式下使用时已在内部调用await connect,但是它没有等待就正在调用。

我所有的测试都是针对同步版本编写的,因为异步测试效率不高,我还必须为一个版本编写测试,并确保两个版本都能正常工作。我该如何同时使用相同的逻辑同时拥有I / O调用的两个版本。

1 个答案:

答案 0 :(得分:3)

  

同步版本具有大多数逻辑部分,异步必须以异步方式调用同步版本

有可能,但这需要大量工作,因为您正在有效地解决function color不匹配的问题。您的逻辑必须以异步方式编写,并且要有一些技巧才能使其在同步模式下工作。

例如,一种逻辑方法将如下所示:

# 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

在异步环境下运行时,connectreadline之类的方法是协程,因此必须等待它们的返回值。另一方面,在阻塞代码self.connectsock.readline中,同步函数返回具体值。但是await是存在或缺失的语法结构,您不能在运行时将其关闭而不复制代码。

要允许相同的代码在同步和异步模式下工作,请FooRaw.hello_logic 始终等待,将其留给_to_async方法以将结果包装为可等待运行的结果外部异步。在异步类_asincify中等待其参数并返回结果,这基本上是一个空操作。在同步类中,它不等待就返回接收到的对象-但仍定义为async def,因此可以等待它。在那种情况下,FooRaw.hello_logic仍然是一个协程,但是永远不会挂起(因为它等待的“协程”是_to_async的所有实例,它们不在asyncio之外挂起。)

有了这个设置,hello_logic的异步实现就不需要做任何事情,只需选择正确的requester并提供正确的_to_async即可;从connect继承的hello_logicFooRaw会自动执行正确的操作:

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")