如何在不修改源码的情况下将异步Python代码整合到同步函数中?
问题:异步改造外部同步HTTP库(不修改源码)
我有一个依赖requests模块执行HTTP请求的外部库,需要在尽量不使用多线程(仅作为最后备选)的前提下异步使用该库,且无法修改其源码。由于该库与requests的交互仅通过单个函数完成,猴子补丁似乎是可行方案,但不确定能否用带async关键字的异步函数替换同步函数。
简化后的问题代码如下:
import asyncio import aiohttp import types import requests # 无法修改的Library类 class Library: def do(self): self._request('example.com') # 其他业务代码... def _request(self, url): return requests.get(url).text # 计划猴子补丁替换的异步方法 async def new_request(self, url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: return await response.text() async def main(): library = Library() # 执行猴子补丁 library._request = types.MethodType(new_request, library) # 尝试异步调用库方法,期望用aiohttp执行请求 asyncio.gather( library.do(), library.do(), library.do() ) print('Done.') asyncio.run(main())
运行后出现两个问题:
asyncio.gather调用时抛出TypeError: An asyncio.Future, a coroutine or an awaitable is requiredself._request('example.com')处触发RuntimeWarning: coroutine 'new_request' was never awaited
请问:不修改Library类源码能否让代码正常运行?若不能,还有哪些方案可实现该库的异步请求?
解答
不修改Library类源码的前提下,直接用异步函数替换同步的_request方法无法让代码正常运行,原因有两点:
- 原
do方法是同步逻辑,调用_request时不会执行await,导致异步协程被创建但从未触发执行,引发警告; library.do()是同步函数调用,返回值为None而非asyncio可识别的可等待对象(Future/协程),不符合asyncio.gather的参数要求,因此抛出类型错误。
以下是两种可行的替代方案:
方案1:同步包装异步请求(猴子补丁+线程池并行)
将异步请求逻辑用同步函数包装,在包装函数内通过事件循环执行异步请求,保留原do方法的调用逻辑不变,同时复用aiohttp的异步能力。若要并行执行多个do调用,需借助线程池避免阻塞事件循环(这属于你提到的最后备选多线程方案,但已是不修改源码下的最优解)。
示例代码:
import asyncio import aiohttp import types import requests # 无法修改的Library类 class Library: def do(self): result = self._request('http://example.com') # 原库中的其他业务代码... return result def _request(self, url): return requests.get(url).text # 核心异步请求逻辑 async def _async_request(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: return await response.text() # 同步包装函数,用于猴子补丁 def sync_wrap_async_request(self, url): try: # 获取当前活跃的事件循环 loop = asyncio.get_running_loop() except RuntimeError: # 无活跃循环时创建新循环 loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # 阻塞等待异步请求结果 return loop.run_until_complete(_async_request(url)) async def main(): library = Library() # 替换原_request方法 library._request = types.MethodType(sync_wrap_async_request, library) # 用线程池包装同步do方法,实现并行执行 await asyncio.gather( asyncio.to_thread(library.do), asyncio.to_thread(library.do), asyncio.to_thread(library.do) ) print('Done.') asyncio.run(main())
方案2:异步化do方法(依赖原逻辑可复制)
如果原Library.do的逻辑简单且长期稳定,可以给实例动态添加异步版本的do方法,直接调用异步的_request,无需依赖线程池。但此方法的缺点是需要完全复制原do的业务逻辑,若原库更新则会出现逻辑不一致的问题。
示例代码:
import asyncio import aiohttp import types import requests # 无法修改的Library类 class Library: def do(self): self._request('http://example.com') # 原库中的其他业务代码... def _request(self, url): return requests.get(url).text # 异步版_request async def async_request(self, url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: return await response.text() # 异步版do,完全复制原do的业务逻辑 async def async_do(self): await self._request('http://example.com') # 复制原do中的其他业务代码... async def main(): library = Library() # 替换_request和do方法 library._request = types.MethodType(async_request, library) library.do = types.MethodType(async_do, library) # 直接用asyncio.gather执行异步方法 await asyncio.gather( library.do(), library.do(), library.do() ) print('Done.') asyncio.run(main())
内容的提问来源于stack exchange,提问作者g00dds
相关产品推荐
相关产品推荐

