同步和异步实现的代码重复

时间:2019-03-14 00:04:25

标签: python asynchronous async-await python-asyncio coroutine

在实现在同步和异步应用程序中都可以使用的类时,我发现自己在这两种用例中维护着几乎相同的代码。

仅作为示例,请考虑:

from time import sleep
import asyncio


class UselessExample:
    def __init__(self, delay):
        self.delay = delay

    async def a_ticker(self, to):
        for i in range(to):
            yield i
            await asyncio.sleep(self.delay)

    def ticker(self, to):
        for i in range(to):
            yield i
            sleep(self.delay)


def func(ue):
    for value in ue.ticker(5):
        print(value)


async def a_func(ue):
    async for value in ue.a_ticker(5):
        print(value)


def main():
    ue = UselessExample(1)
    func(ue)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(a_func(ue))


if __name__ == '__main__':
    main()

在此示例中,还不错,ticker的{​​{1}}方法易于串联维护,但是您可以想象异常处理和更复杂的功能可以快速地扩展方法并使即使这两种方法实际上都可以保持相同(仅将某些元素替换为其异步对应元素),这仍然是一个问题。

假设没有什么实质性差异值得完全实现,那么维护这样的类并避免不必要的重复的最佳(也是最Pythonic)方法是什么?

2 个答案:

答案 0 :(得分:10)

要使基于异步协程的异步代码库可用于传统同步代码库,没有一条千篇一律的方法。您必须根据代码路径进行选择。

选择并从一系列工具中进行选择:

使用async.run()的同步版本

在协程周围提供同步包装程序,这些程序会阻塞直到协程完成。

甚至ticker()之类的异步生成器函数也可以在循环中以这种方式处理:

class UselessExample:
    def __init__(self, delay):
        self.delay = delay

    async def a_ticker(self, to):
        for i in range(to):
            yield i
            await asyncio.sleep(self.delay)

    def ticker(self, to):
        agen = self.a_ticker(to)
        try:
            while True:
                yield asyncio.run(agen.__anext__())
        except StopAsyncIteration:
            return

这些同步包装器可以使用辅助函数生成:

from functools import wraps

def sync_agen_method(agen_method):
    @wraps(agen_method)
    def wrapper(self, *args, **kwargs):
        agen = agen_method(self, *args, **kwargs)   
        try:
            while True:
                yield asyncio.run(agen.__anext__())
        except StopAsyncIteration:
            return
    if wrapper.__name__[:2] == 'a_':
        wrapper.__name__ = wrapper.__name__[2:]
    return wrapper

然后只需在类定义中使用ticker = sync_agen_method(a_ticker)

直接协程方法(不是生成协程)可以用以下方法包装:

def sync_method(async_method):
    @wraps(async_method)
    def wrapper(self, *args, **kwargs):
        return async.run(async_method(self, *args, **kwargs))
    if wrapper.__name__[:2] == 'a_':
        wrapper.__name__ = wrapper.__name__[2:]
    return wrapper

找出常见组件

将同步部分重构为生成器,上下文管理器,实用程序功能等。

对于您的特定示例,将for循环拉到一个单独的生成器中可以将重复的代码最小化为两个版本休眠的方式:

class UselessExample:
    def __init__(self, delay):
        self.delay = delay

    def _ticker_gen(self, to):
        yield from range(to)

    async def a_ticker(self, to):
        for i in self._ticker_gen(to):
            yield i
            await asyncio.sleep(self.delay)

    def ticker(self, to):
        for i in self._ticker_gen(to):
            yield i
            sleep(self.delay)

虽然这没什么区别,但在其他情况下也可以。

抽象语法树转换

使用AST重写和映射将协程转换为同步代码。如果您不小心识别asyncio.sleep()time.sleep()之类的实用程序功能,则这可能非常脆弱:

import inspect
import ast
import copy
import textwrap
import time

asynciomap = {
    # asyncio function to (additional globals, replacement source) tuples
    "sleep": ({"time": time}, "time.sleep")
}


class AsyncToSync(ast.NodeTransformer):
    def __init__(self):
        self.globals = {}

    def visit_AsyncFunctionDef(self, node):
        return ast.copy_location(
            ast.FunctionDef(
                node.name,
                self.visit(node.args),
                [self.visit(stmt) for stmt in node.body],
                [self.visit(stmt) for stmt in node.decorator_list],
                node.returns and ast.visit(node.returns),
            ),
            node,
        )

    def visit_Await(self, node):
        return self.visit(node.value)

    def visit_Attribute(self, node):
        if (
            isinstance(node.value, ast.Name)
            and isinstance(node.value.ctx, ast.Load)
            and node.value.id == "asyncio"
            and node.attr in asynciomap
        ):
            g, replacement = asynciomap[node.attr]
            self.globals.update(g)
            return ast.copy_location(
                ast.parse(replacement, mode="eval").body,
                node
            )
        return node


def transform_sync(f):
    filename = inspect.getfile(f)
    lines, lineno = inspect.getsourcelines(f)
    ast_tree = ast.parse(textwrap.dedent(''.join(lines)), filename)
    ast.increment_lineno(ast_tree, lineno - 1)

    transformer = AsyncToSync()
    transformer.visit(ast_tree)
    tranformed_globals = {**f.__globals__, **transformer.globals}
    exec(compile(ast_tree, filename, 'exec'), tranformed_globals)
    return tranformed_globals[f.__name__]

尽管上述内容可能还远远不够满足所有需求,并且转换AST树 可能令人生畏,但上述内容将让您仅维护异步版本并将该版本直接映射到同步版本:

>>> import example
>>> del example.UselessExample.ticker
>>> example.main()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../example.py", line 32, in main
    func(ue)
  File "/.../example.py", line 21, in func
    for value in ue.ticker(5):
AttributeError: 'UselessExample' object has no attribute 'ticker'
>>> example.UselessExample.ticker = transform_sync(example.UselessExample.a_ticker)
>>> example.main()
0
1
2
3
4
0
1
2
3
4

答案 1 :(得分:0)

async/await具有传染性。

接受您的代码将具有不同的用户(同步用户和异步用户),并且这些用户将有不同的要求,随着时间的推移,实现会有所不同。

发布单独的库

例如,比较aiohttpaiohttp-requestsrequests

同样,比较asyncpgpsycopg2

如何到达那里

Opt1。 (轻松)克隆实现,让他们有所分歧。

Opt2。 (明智的)部分重构,例如异步库依赖并导入同步库。

Opt3。 (激进)创建一个可在同步程序和异步程序中使用的“纯”库。例如,请参见https://github.com/python-hyper/hyper-h2

从好的方面来说,测试更容易,更彻底。考虑一下迫使测试框架评估异步程序中所有可能的并发执行顺序有多困难(或不可能)。纯图书馆不需要那:)

不利的一面是,这种编程风格需要不同的思维,并不总是那么简单,而且可能不是最优的。例如,您可以写await socket.read(2**20)而不是for event in fsm.push(data): ...,而是依靠您的库用户为您提供大小合适的数据块。

有关上下文,请参见https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/中的backpressure参数