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() 接收一整批任务,不替应用控制外部服务的负载。


