You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

调用asyncio.sleep()为何会意外修改请求参数对象?

异步API调用中params字典被意外修改的问题与解决办法

问题描述

我编写了一个用于模拟API调用的异步request()函数,创建多个携带不同参数的任务调用该函数并等待执行完成。原本一切正常,但在request()中添加await asyncio.sleep()(限流逻辑,无法省略)后,发现request()接收的params字典被意外修改。

以下是我的异步API封装代码:

"""Async API wrapper"""
import asyncio
import aiohttp

async def request(params):
    print(f'params before: {params}')
    await asyncio.sleep(1)
    print(f'params after: {params}')
    return {'success': True, 'data': []}

async def get_resource(endpoint, start=0, filter_id=0, params={}):
    params['limit'] = 10
    params['start'] = start
    if filter_id:
        params['filter_id'] = filter_id
    r = await request(params)
    if r["success"]:
        return r
    else:
        raise ValueError(f'Request to /{endpoint} with {params} was not successful.')

async def get_bulk_resource():
    async with aiohttp.ClientSession():
        res = []
        tasks = [asyncio.create_task(get_resource(endpoint='users', start=start)) for start in range(1, 101)]
        for task in asyncio.as_completed(tasks):
            r = await task
            res += r["data"]
        return res

lst = asyncio.get_event_loop().run_until_complete(get_bulk_resource())

打印结果显示:

  • await asyncio.sleep(1)执行前,各任务的params参数正常:
    params before: {'limit': 10, 'start': 1}
    params before: {'limit': 10, 'start': 2}
    ...
    params before: {'limit': 10, 'start': 100}
    
  • 但sleep执行完成后,所有任务的start参数都变成了最后一页的值:
    params after: {'limit': 10, 'start': 100}
    params after: {'limit': 10, 'start': 100}
    ...
    params after: {'limit': 10, 'start': 100}
    

原因分析

这是Python默认参数的常见陷阱,结合异步任务的执行特性放大了问题:

  • Python中,函数的默认参数是在函数定义时创建的,而不是每次调用函数时重新生成。所以get_resource里的params={},实际上是模块加载时就生成了一个空字典,所有未传入params的get_resource调用,都会共享这同一个字典对象。
  • 异步任务创建后,会快速循环执行get_resource的前半段逻辑(修改params字典)。当第一个任务执行到await asyncio.sleep(1)时,它会让出事件循环,其他任务继续执行并修改这个共享字典的start值。等sleep结束后,这个字典已经被最后一个任务修改为start=100,所以所有任务打印的params after都是同一个值。

解决方案

核心是确保每个get_resource调用都使用独立的params字典,避免共享。有两种可靠方法:

方法1:修改默认参数为None,在函数内部初始化独立字典

把params={}改成params=None,在函数内部判断并生成新字典,确保每次调用都有独立的实例:

async def get_resource(endpoint, start=0, filter_id=0, params=None):
    # 若传入了params则复制一份,否则新建空字典,避免共享
    params = params.copy() if params is not None else {}
    params['limit'] = 10
    params['start'] = start
    if filter_id:
        params['filter_id'] = filter_id
    r = await request(params)
    if r["success"]:
        return r
    else:
        raise ValueError(f'Request to /{endpoint} with {params} was not successful.')

方法2:创建任务时显式传入独立字典

如果想保留默认参数写法,也可以在生成任务时主动传递空字典,让每个任务使用独立的params:

tasks = [asyncio.create_task(get_resource(endpoint='users', start=start, params={})) for start in range(1, 101)]

这两种方法都能保证每个异步任务操作的是独立字典,不会互相干扰,同时完全保留asyncio.sleep()的限流逻辑。

内容的提问来源于stack exchange,提问作者Dawid Januszkiewicz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 09:27:29