python - 同步和异步实现的代码重复
问题描述
在实现同时用于同步和异步应用程序的类时,我发现自己为这两个用例维护了几乎相同的代码。
举个例子,考虑:
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
方法UselessExample
很容易串联维护,但是您可以想象异常处理和更复杂的功能可以快速扩展方法并使其成为更多问题,即使这两种方法实际上都可以保留相同(仅将某些元素替换为其异步对应元素)。
假设没有实质性的区别使得两者都值得完全实现,那么维护这样的类并避免不必要的重复的最佳(也是最 Pythonic)的方法是什么?
解决方案
从传统的同步代码库中使用基于异步协程的代码库,没有一种万能的方法。您必须为每个代码路径做出选择。
从一系列工具中挑选:
同步版本使用asyncio.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()
vs )不小心,这可能会非常脆弱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
推荐阅读
- bash - Linux:关闭 ssh 会话时结束运行 bash 脚本
- testing - CakePHP3:集成测试中的模拟方法?
- java - eclipse java photon中的INSTALL_PARSE_FAILED_NO_CERTIFICATES
- antd - blur 和 focusIn 的输入验证消息切换
- javascript - 在 Javascript 中解构对象
- android - 针对 Kotlin JVM 项目的最低 Android API 级别
- java - SonarQube 代码覆盖率不提供正确的值
- python-3.x - Pycharm,opencv加载dll失败,提示工作
- firebase - SHA-1 - 您的操作被禁止。Firebase SHA-1 错误
- android - 环氧树脂视图中的谷歌地图片段