如何制作一组可以同时使用和异步使用的功能?

时间:2018-11-02 21:25:42

标签: python python-3.x asynchronous async-await python-asyncio

想象一下,我有一组这样的功能:

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 :(得分:3)

这是我的“不回答”,我知道Stack Overflow喜欢...

  

有没有办法实现我所有功能的异步副本?

我不认为有。制作“空白翻译器”以将功能转换为本地协程似乎是不可能的。这是因为使同步函数异步,不仅仅在其前面加上一个async关键字和其中的几个await语句。请记住,您所做的任何事情await必须为awaitable

您的def func2(): time.sleep(1)说明了这一点。同步函数将进行阻塞调用,例如time.sleep();异步(本机协程)将等待非阻塞协程。如您所指出的,使此函数异步不仅需要使用async def func(),而且还需要等待asyncio.sleep()。现在,让我们而不是time.sleep()来调用一个更复杂的阻塞函数。您构建了某种精美的装饰器,该装饰器将名为run_async的{​​{3}}拍打到装饰函数上。但是,即使修饰符已经定义,装饰者如何知道如何将func2()中的阻塞调用“转换”为它们的协程等效项?我想不出任何足以将同步功能中的所有调用转换为await对应对象的魔术。

在您的评论中,您提到这是针对HTTP请求的。对于一个实际示例,requestsaiohttp包之间的呼叫签名和API的差异。在aiohttp中,.text()是实例function attribute;在requests中,.textmethod。您如何构建足够聪明的东西来了解诸如此类的差异?

我并不是要劝阻-但我认为使用线程会更现实。

答案 1 :(得分:0)

因此,我找到了一种方法来实现这一目标,但是由于这实际上是我第一次使用async做任何事情,所以我不能保证它没有任何错误,也不是很糟糕。想法。

这个概念实际上非常简单:在必要时使用async defawait定义函数,例如普通的异步函数,然后在它们周围添加一个包装器,以自动等待功能 if 没有事件循环正在运行。概念证明:

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

请注意这一点!