DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

asyncio.gather 传入列表时报 unhashable type: 'list'

asyncio.gather 接收多个可等待对象,任务列表需用星号展开。核对元素、结果顺序、阻塞操作与异常取消规则,避免修复参数后改变业务并发语义。

把多个协程放进 jobs 列表后,调用 await asyncio.gather(jobs),堆栈却报 TypeError: unhashable type: 'list'。若失败点就是这一行,先检查参数形状:gather() 接收多个可等待对象作为位置参数,不会自动把一个列表展开成任务。

下面使用 Python 3.7 及以上的 asyncio API;TaskGroup 的补充说明要求 Python 3.11 及以上。报错文字可能随实现和参数而变化,定位时还要保留完整堆栈。

把列表元素传进去,而不是把列表当成一个任务

以下是错误写法的结构示例,fetch_one() 是异步函数:

jobs = [fetch_one("A"), fetch_one("B")]
results = await asyncio.gather(jobs)

调用相当于只传了一个参数,这个参数是普通列表。CPython 的 gather() 实现会用映射处理传入对象;列表不可哈希,因此可能在这里先报 unhashable type: 'list',还没走到可等待对象的检查。

使用 *jobs 把列表展开。下面是一份无外部请求的完整示例:

import asyncio

async def fetch_one(name):
    await asyncio.sleep(0)
    return {"name": name, "status": "ready"}

async def main():
    names = ["A", "B"]
    jobs = [fetch_one(name) for name in names]
    results = await asyncio.gather(*jobs)
    for name, result in zip(names, results):
        print(name, result)

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

结果列表按传入顺序排列,不按完成顺序排列。即使 B 先结束,results[0] 仍对应 A。输入为空时,在运行中的异步入口里 await asyncio.gather(*jobs) 会得到空列表。

列表里可以放新协程对象,也可以放当前循环所属的 Task 或 Future;传入协程时 gather() 会安排执行,通常不必提前逐个 create_task()。如果要单独保存和管理任务,才按需要创建 Task。

不要靠改成 tuple、set 或循环 await 消除报错

asyncio.gather(tuple(jobs)) 仍然只传了一个参数,元组也不是这批任务本身。改为 set 还可能改变顺序或丢掉重复项,不能修正 API 调用方式。应保留列表并展开其元素。

再检查元素是否确实可等待。若列表里是 fetch_one 函数名、已算出的普通返回值,或每项又是一层列表,增加 * 后还会出现其他类型错误。异步函数应在需要执行时调用,得到新协程;不能把已经等待结束的原始协程塞回下一批重用。

下面这种写法逐项等到结束,再创建下一次调用,改变了并发行为:

results = []
for name in names:
    results.append(await fetch_one(name))

它适合刻意串行执行的操作;需要同时等待独立任务时,应保留 gather(*jobs)。此外,async def 里如果直接调用耗时的同步网络请求、subprocess.run() 或 time.sleep(),仍可能阻塞事件循环。展开参数只修复调用方式,不会自动把阻塞操作变成异步 I/O。

一个任务失败,默认不会等待并收齐所有结果

gather() 默认把最先产生的业务异常交给等待者,其他任务不会因为这一条异常自动被取消。若顶层 main() 因异常退出,asyncio.run() 随后的退出清理又可能取消尚未完成的任务;这不能解释成 gather() 自身的失败策略。

如果业务需要每项成功或失败都形成结果,可以显式使用 return_exceptions=True,再逐项处理。下面片段放在前面的 main() 中,替换创建 jobs 后的汇总部分:

results = await asyncio.gather(*jobs, return_exceptions=True)
for name, result in zip(names, results):
    if isinstance(result, asyncio.CancelledError):
        raise result
    if isinstance(result, Exception):
        print(name, "failed", str(result))
    else:
        print(name, "ok", result)

这把普通业务异常当作对应位置的结果处理,同时把取消继续交给上层;不要只检查 Exception 就把取消对象误当成业务成功。整个 gather() 被取消时,尚未完成的子任务也会收到取消,这与某一个子任务自己失败不同。

若需求是“任何一项业务失败,就取消其余任务并等待清理”,Python 3.11 及以上可以评估 TaskGroup。它的任务管理和异常汇总方式不同,不能仅为修正列表展开就直接替换全部汇总逻辑。

改动后核对输入元素与返回值一一对应,以及正常完成、单项失败、调用方取消这三种情况是否符合业务约定。请求量很大时,还需另设并发上限或分批;gather() 接收一整批任务,不替应用控制外部服务的负载。