兼容同步和异步客户端的Python函数
我正在开发一个库,这个库需要同时支持同步和异步用户,同时尽量减少库内部的代码重复。理想的情况是实现一个异步的库(因为这个库会进行远程API调用),然后通过一个包装器来添加对同步的支持。考虑以下这个函数作为库的一部分。
# Library code
async def internal_function():
"""Internal part of my library doing HTTP call"""
pass
我的想法是提供一个包装器,能够检测库的用户是使用异步还是同步。
# Library code
def api_call():
"""Public api of my library"""
if asyncio.get_event_loop().is_running():
# Async caller
return internal_function() # This returns a coroutine, so the caller needs to await it
# Sync caller, so we need to run the coroutine in a blocking way
return asyncio.run(internal_function())
一开始,这看起来是个解决方案。这样我可以支持:
- 从异步函数调用这个函数的用户,
- 从笔记本(也是异步的)调用这个函数的用户,
- 从普通的Python脚本调用这个函数的用户(在这种情况下,它会退回到阻塞的
asyncio.run
)。
然而,有些情况下,函数是在事件循环中被调用的,但直接调用它的是一个同步函数。
# Code from users of the library
async def entrypoint():
"""This entrypoint is called in an event loop, e.g. within fastlib"""
return legacy_sync_code() # Call to sync function
def legacy_sync_code():
"""Legacy code that does not support async"""
# Call to library code from sync function,
# expect value not coroutine, could not use await
api_response = api_call()
return api_response.json() # Needs to be value, not coroutine
在最后一行,调用 json()
失败了。api_call()
错误地推断出调用者可以等待响应,并返回了协程。
为了支持这些用户,我需要:
- 识别直接调用的函数是同步的,并且期望一个值而不是协程,
- 有一种方法可以以阻塞的方式等待
internal_function()
的结果。使用asyncio.run
只在普通的Python脚本中有效,如果代码是在更高层的事件循环中调用的,它就会失败。
如果我的库提供两个函数,比如 api_call_async()
和 api_call_sync()
,那么第一个问题可以得到缓解。
我希望我说得够清楚。我不明白为什么这在根本上是不可能的,不过,如果Python的设计不允许我以完全透明的方式支持同步和异步用户,我也能接受。
1 个回答
4
不要试图过于灵活或者去解决那些还没有发生的问题。这样只会让你的生活变得更困难,也可能让你的代码变得更复杂,最终反而不如你想象的那么灵活。让其他开发者自己承担一些责任并没有什么不对,不需要一直照顾他们。让他们自己决定是使用同步还是异步的方式。同时要注意,某些对你来说看起来有好处的实现细节(比如检测同步或异步),可能对其他人来说却是个麻烦,因为你可能在做一些他们并没有要求的事情。很容易因为想得太多而让自己陷入麻烦,然后浪费很多时间去修复自己制造的问题。我个人会选择分别使用 api_call_sync()
和 api_call_async()
的方式,同时确保我的库文档非常清晰,讨论所有你现在认为可能会有问题的边缘情况,这样用户就能全面了解情况,做出最佳选择。