Python踩坑记录:函数参数默认值,你确定你重新定义了么?
上班接了一个小需求,需要把原有接口函数参数从单选变为多选(user_id -> user_ids)。接口大概是这样:
@router.get('/example', response_model=ExampleResponse)
@log.logger
@permission_require(10086)
async def get_example_infos(
user_id: int = Query(None, title="example user_id")
):
......
我很快一看,小问题,int改str,内部做分割。突然一想,没这么简单,这样的修改会导致所有涉及到这个接口的传参改变,可能要全部排查一下所有子系统。amazing!!!!!微服务架构下稀碎的子系统排查起来,无异于坐牢。此时石头人开始转动他聪明的脑袋瓜了。。。。。。
灵机一动!加个参数user_ids用来单独传参,然后在实际业务层将user_id全部添入user_ids列表进行查询。理论可行,实践操作:
接口层:
@router.get('/example', response_model=ExampleResponse)
@log.logger
@permission_require(10086)
async def get_example_infos(
user_id: int = Query(None, title="example user_id"),
user_ids: str = Query(None, title="example user_ids")
):
......
业务层:
async def op_example_info(user_id: int = None, user_ids: list = []) -> tuple:
if user_id:
user_ids.append(user_id)
快速测试筛选无误,直接上线,全程不过一小时尔尔。
然而,不出意外就是出了意外。业务侧返回所有user的详细信息都变成了同一个人,也就是说不管怎么传user_id,最后都只能查出第一个点击的user_id。
百思不得其解。我是谁?我在哪?这是什么???
仔细排查后发现,原来问题出在操作层接口定义上。还记得我们的定义方式是user_ids: list = []么?就是这块出了问题。看下面这个例子
def test(b, a=[]):
if b:
a.append(b)
print(a)
return 1 if a else -1
---------------------------
test(1)
[1]
test(2)
[1, 2]
test(3)
[1, 2, 3]
事情似乎有点不对,十分有九分的不对。为什么在每次调用时候a并没有预想中的重新定义?
大致分析了下原因:
这种问题出现的原因是函数参数的默认值在函数定义时就被计算出来,并且在后续函数调用中被重复使用。
在这个例子中,函数
test
有两个参数,b
和a
。参数a
的默认值是一个空列表[]
。当函数被调用时,如果传入了参数b
,则将其添加到列表a
中。在第一次调用
test(1)
时,参数b
被传入函数,a
列表为空,所以将b
添加到a
中得到[1]
,然后打印出来。在第二次调用
test(2)
时,参数b
被传入函数,a
列表为上一次调用后的结果[1]
,所以将b
添加到a
中得到[1, 2]
,然后打印出来。这种问题的根本原因是函数参数的默认值在函数定义时就被计算出来,并且在后续函数调用中被重复使用。如果需要避免这种问题,可以将默认值设置为
None
,然后在函数内部判断并创建新的列表。
原来归根结底还得归因到python的内存分配上。那从内存角度看看为什么会出现这种问题呢?
在Python中,函数定义时,默认参数的值会被计算并分配内存空间。在这个例子中,参数
a
的默认值是一个空列表[]
。当函数test
被定义时,空列表[]
被创建并分配了内存空间,并与参数a
关联。
在第一次调用test(1)
时,参数b
被传入函数,并且操作列表a
,将b
添加到a
中。此时,a
列表在内存中的地址没有发生变化,仍然是之前定义时分配的内存空间。因此,修改后的列表a
会被保留,并打印出来。
在第二次调用test(2)
时,参数b
被传入函数,并且操作列表a
,将b
添加到a
中。这时候,由于a
列表在上一次调用后没有被重新分配内存空间,所以修改后的列表a
仍然是之前的结果[1]
。因此,再次添加元素后的列表a
变为[1, 2]
,并打印出来。从内存角度分析,问题出现的原因是函数参数的默认值在函数定义时就被创建并分配了内存空间,并且在后续函数调用时重复使用了这块内存空间。如果需要避免这种问题,可以在函数内部创建新的对象,而不是重复使用默认值。
大致分析上面的这个解答:
python的内存分配是在函数定义的时候将变量指针指向默认值的内存空间,其和c++不同的在于,并不是单独为这个变量声明一块空间,而是所有使用这个内存空间的变量都将指针指向这块空间即可,由此衍生出python中的深浅拷贝。因为我们在函数定义时候将默认值a指针指向一块列表空间,而此时在内存层面这块空间已经声明。当我们后续调用函数时候,解释器发现空间已经声明,无需再次开辟,于是会复用之前这块空间,而python的list append又只是在原有[]空间中添加指向元素参数的指针,并不会改变列表的内存值(可以使用id()函数查看变量的内存值)。这样就导致了上述问题的形成,基础错误。。。
这是关于以上python中函数踩坑的记录,由此衍生的知识点是:函数参数的默认值、函数传参的规范、函数的定义、pythonGC垃圾回收机制、python内存分配、可变类型和不可变类型、深浅拷贝。