将异步生成器转换为同步生成器。

3

想象一下,我们有一个返回生成器的原始API(实际上它是一种从服务器获取页面/结果块的机制,同时提供一个简单的生成器给用户,并让他逐个迭代这些结果。为了简单起见:

# Original sync generator
def get_results():
     # fetch from server
     yield 1
     yield 2
     # fetch next page
     yield 3
     yield 4
     # ....

现在有一个需要实现API的asyncio版本,但我们仍需要保持旧API的操作性。这就是事情变得复杂的地方,我们想要将async generator转换为sync one,但我找不到一种优雅的方法来实现这个目标。迄今为止,我能做到的最好的方法是“首先将所有结果获取到列表中,然后在该列表上提供一个虚假的sync generator”。但这有点违背初衷:

# Async generator
async def get_results_async():
     # await fetch from server
     yield 1
     yield 2
     # await fetch next page
     yield 3
     yield 4
     # ....


# Backward compatible sync generator
def get_results():

    async def gather_all_results():
        res = []
        async for i in get_results_async():
            res.append(i)
        return res

    res = asyncio.run(gather_all_results())
    for i in res:
        yield i

有没有更好、更优雅的方法,在不获取所有结果的情况下返回它们?

谢谢


我知道这个问题几天前已经被问过了,但我想知道为什么你要编写 get_results() 的新版本,而不是只使用旧版本。新版本创建、运行和停止事件循环,因此它的性能比旧版本差。在任何已经运行事件循环的上下文中都无法工作。你的方法似乎是一种不必要的劳动,而且结果很差。 - Paul Cornelius
@PaulCornelius 在 get results 函数内部的实际代码会获取一堆分页数据。原来的代码使用 urllib,而新的异步代码则使用 aiohttp。主要原因是不想维护两个并行执行几乎相同任务的代码路径。 - Dmitry Fink
1个回答

2

由于asyncio是具有传染性的,因此很难编写优雅的代码来将asyncio代码集成到旧代码中。对于上述情况,下面的代码略微好一些,但我认为它还不够优雅。

async def get_results_async():
    # await fetch from server
    yield 1
    yield 2
    # await fetch next page
    yield 3
    yield 4
    # ....


# Backward compatible sync generator
def get_results():
    gen = get_results_async()
    while True:
        try:
            yield asyncio.run(gen.__anext__())
        except StopAsyncIteration:
            break 

您可以重复使用您的事件循环,而不需要创建一个新的。

async def get_results_async():
    # await fetch from server
    yield 1
    yield 2
    # await fetch next page
    yield 3
    yield 4
    # ....

# loop that you save in somewhere.
loop = asyncio.get_event_loop()

# Backward compatible sync generator
def get_results():
    gen = get_results_async()
    while True:
        try:
            yield loop.run_until_complete(gen.__anext__())
        except StopAsyncIteration:
            break 

1
这段代码可以工作,但是当里面有更复杂的异步操作时,它只会返回 get_results_async 的第一个项目 - 不明白为什么。 - sonium
请注意,您正在热循环中放置try/except。这是一种高度低效且不可取的模式。在try内部放置while是一个更好的想法。 - pt12lol
@sonium 我也遇到了同样的问题,并在另一个线程中提出了这个问题。你解决了这个问题吗?这与嵌套异步生成器有关吗? - Guibod

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接