当实现同时在同步和异步应用程序中使用的类时,我发现自己为这两个用例维护了几乎相同的代码。在
举个例子,考虑一下:
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()
在这个例子中,UselessExample
的ticker
方法很容易串联起来维护,但是可以想象,异常处理和更复杂的功能可以快速地扩展一个方法,并使其成为一个更大的问题,尽管这两种方法实际上可以保持相同(只是用异步对应的元素替换某些元素)。在
假设没有实质性的区别,这两种方法都值得完全实现,那么维护这样一个类并避免不必要的重复的最好(也是最具Pythonic)方法是什么?在
async/await
通过设计具有感染力。在接受你的代码将有不同的用户-同步和异步,并且这些用户将有不同的需求,随着时间的推移,实现会有所不同。在
发布单独的库
例如,比较
aiohttp
与aiohttp-requests
与requests
。在同样,比较
asyncpg
与psycopg2
。在如何到达那里
选项1。(简单)克隆实现,允许它们发散。在
选项2。(合理)部分重构,例如,让异步库依赖并导入同步库。在
选项3。(radical)创建一个可以在同步和异步程序中使用的“纯”库。例如,请参见https://github.com/python-hyper/hyper-h2。在
从好的方面来说,测试更容易、更彻底。考虑一下强制测试框架评估异步程序中所有可能的并发执行顺序有多困难(或不可能)。Pure library不需要它:)
从负面来看,这种风格的编程需要不同的思考,并不总是直接的,而且可能是次优的。例如,您可以编写
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
参数要使基于异步协同程序的代码库从传统的同步代码库中可用,没有一条万能的道路。你必须根据代码路径做出选择。在
从一系列工具中选择:
使用
async.run()
的同步版本为协程提供同步包装,在协程完成之前会阻塞。在
即使是异步生成器函数,如
ticker()
,也可以这样在循环中处理:可以使用helper函数生成这些同步包装:
^{pr2}$然后在类定义中使用
ticker = sync_agen_method(a_ticker)
。在直接向上的协同程序方法(不是生成器协同程序)可以用以下方式包装:
排除常见成分
将同步部分重构成生成器、上下文管理器、实用程序函数等
对于您的特定示例,将
for
循环拉出到单独的生成器中,可以将重复代码减到两个版本休眠的状态:虽然这没什么区别,但它可以在其他环境下工作。在
抽象语法树转换
使用AST重写和映射将协程转换为同步代码。如果您不小心识别诸如
asyncio.sleep()
vstime.sleep()
之类的实用函数,这可能会非常脆弱:虽然上述内容可能还远远不够完整,无法满足所有需求,而且转换AST树可能会使望而生畏,但是上面的内容可以让您只维护异步版本并将该版本直接映射到同步版本:
相关问题 更多 >
编程相关推荐