调用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
相关产品推荐
相关产品推荐

