我在Python 3.5+中有一个模块,提供了一个从远程Web API读取一些数据并将其返回的功能。该函数依赖于包装器函数,后者使用库requests
进行HTTP调用。
在这里(故意删除所有数据验证逻辑和异常处理):
# module fetcher.py
import requests
# high-level module API
def read(some_params):
resp = requests.get('http://example.com', params=some_params)
return resp.json()
# wrapper for the actual remote API call
def get_data(some_params):
return call_web_api(some_params)
该模块当前已导入并由多个客户端使用。
从今天开始,对get_data的调用本质上是 synchronous :这意味着使用函数fetcher.read()
的人都知道这将阻塞执行该函数的线程。< / p>
我想允许fetcher.read()
以同步和异步方式(例如通过事件循环)两者运行。
这是为了保持与使用该模块的现有呼叫者的兼容性,同时提供了可能性
利用非阻塞调用为希望异步调用该函数的调用者提供更好的吞吐量。
这就是说,我的合法愿望是尽可能少地修改原始代码...
从今天开始,我唯一了解的是请求不支持开箱即用的异步操作,因此我应该切换到异步友好的HTTP客户端(例如aiohttp
)以提供一个无阻塞行为
如何修改上面的代码来满足我的需求?这也使我问:是否有将同步软件API增强到异步上下文的最佳实践? strong>
答案 0 :(得分:1)
我想允许
fetcher.read()
以同步和异步方式(例如,通过事件循环)运行。
我认为通过同步和异步API使用同一功能是不可行的,因为使用模式非常不同。即使您能以某种方式使其工作,将事情弄乱也太容易了,特别是考虑到Python的动态键入特性。 (例如,用户可能不小心忘记了用异步代码await
来执行其功能,并且同步代码会启动,从而阻塞了事件循环。)
相反,我建议实际的API是异步的,并创建一个简单的同步包装器,该包装器仅使用run_until_complete
调用入口点。遵循以下原则:
# new module afetcher.py (or fetcher_async, or however you like it)
import aiohttp
# high-level module API
async def read(some_params):
async with aiohttp.request('GET', 'http://example.com', params=some_params) as resp:
return await resp.json()
# wrapper for the actual remote API call
async def get_data(some_params):
return call_web_api(some_params)
是的,您从使用requests
切换到aiohttp
,但是更改是机械的,因为API的本质非常相似。
存在同步模块是为了向后兼容和方便,并且将包装异步功能:
# module fetcher.py
import afetcher
def read(some_params):
loop = asyncio.get_event_loop()
return loop.run_until_complete(afetcher.read(some_params))
...
这种方法同时提供了API的同步版本和异步版本,而无需重复代码,因为同步版本由简单的蹦床组成,可以使用适当的装饰器进一步压缩其定义。
异步提取器模块应该有一个不错的简称,这样用户就不会因为使用异步功能而受到惩罚。它应该易于使用,并且与sync API相比,它实际上提供了许多新功能,其中最值得一提的是开销较低的并行化和可靠的取消。
不建议的路由是使用run_in_executor
或类似的基于线程的工具在引擎盖下的线程池中运行requests
。该实现不提供使用asyncio的实际好处,但会产生所有费用。在这种情况下,最好继续提供同步API,并让用户使用concurrent.futures
或类似的工具进行并行执行,以便他们至少知道自己在使用线程。