兼容同步和异步客户端的Python函数

2 投票
1 回答
51 浏览
提问于 2025-04-14 16:55

我正在开发一个库,这个库需要同时支持同步和异步用户,同时尽量减少库内部的代码重复。理想的情况是实现一个异步的库(因为这个库会进行远程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() 的方式,同时确保我的库文档非常清晰,讨论所有你现在认为可能会有问题的边缘情况,这样用户就能全面了解情况,做出最佳选择。

撰写回答