DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS

Joblib 关闭缓存后 await call 报 tuple 不能等待

Joblib 1.6.0 的无缓存异步包装器继承同步 call,返回协程与元数据组成的元组。可等待普通调用;需要强制执行二元组契约时,单独适配关闭缓存分支。

关闭 Joblib 磁盘缓存后,同一段异步调用开始报:

TypeError: object tuple can't be used in 'await' expression
RuntimeWarning: coroutine ... was never awaited

一种具体写法是 wrapped = Memory(location=None).cache(async_function),随后执行 await wrapped.call(...)。这时异常并不说明异步函数需要重新加一次 async def;Joblib 返回对象的接口不一致才是要检查的位置。

Joblib 1.6.0 中,无缓存的异步包装器继承了同步 .call()。它立即返回 (协程对象, {}),尚未等待原函数;外层 await 接到的却是整个元组,于是报错,元组里那份协程也没有得到执行。开启缓存时使用的另一个异步包装器则有异步 .call(),因此切换缓存开关后才容易暴露。

不需要元数据时,直接等待包装后的普通调用

在明确关闭缓存的场景里,可使用普通调用入口:

import asyncio
from joblib import Memory

async def load_value(value):
    await asyncio.sleep(0)
    return value + 1

async def main():
    wrapped = Memory(location=None, verbose=0).cache(load_value)
    output = await wrapped(2)
    print(output)

if __name__ == "__main__":
    asyncio.run(main())

这个例子预期输出 3。返回的是业务结果,不是 .call() 的“结果、元数据”二元组;若业务结果本身是元组,也应完整保留它,不能按这次错误盲目解包。

已经在异步框架内运行时,保留框架管理的事件循环,直接等待所需调用,不再套一层 asyncio.run()。

需要强制执行契约时,把无缓存分支写清楚

如果上层接口必须返回 (output, metadata),无缓存分支可以明确执行 output = await load_value(...),再返回 (output, {})。这只是应用对关闭缓存场景的适配;开启缓存时,仍应保留原本用于强制重算的异步 .call(),不能一概换成普通调用,否则可能读到缓存而没有重算。

不要先执行失败的 wrapped.call(),再重试原函数。前者已经创建一份无人等待的协程,重复创建会继续产生警告;对于带外部副作用的函数,还会使执行次数难以判断。也不要把协程对象本身放进普通结果缓存,下一次读取可能得到不能重复等待的对象。

当前正式 1.6.0 仍未给无缓存包装器提供对应的异步 .call() 覆盖。后续更新时应核对这两个包装类型的具体接口,不能仅凭开发版本讨论认定已发布修正。

检查关闭缓存时的结果、异常传播及每次调用次数,再检查开启缓存时的命中和强制重算。两条路径都需要保留异步等待;只把 RuntimeWarning 过滤掉,不会让那份协程完成业务操作。