Python的函数的缺省值参数(空列表)之坑
From
https://www.jianshu.com/p/9f899d829562
def add(x,lst = []): if not(x in lst): lst.append(x) return lst list1 = add(1) print(list1) list2 = add(2) print(list2) list3 = add(3,[11,12,13,14]) print(list3) list4 = add(4) print(list4)
运行结果如下:
[1]
[1, 2]
[11, 12, 13, 14, 3]
[1, 2, 4]
Python缺省值参数之坑
最近学习编程的过程中,被一个大坑折磨了挺久。
当时我正在编写一个函数,函数的过程的参数是一个带缺省值的列表对象,该函数需要对列表进行修改,最终函数的返回值是这个修改后的列表。整个函数可以抽象成下面这样简单的函数:
可以看到append_lst( )这个函数特别简单,就是将输入变量lst的尾部添加一个1,最后将其返回。最下面两行是将调用得到的列表赋值给lst_update,然后将其打印出来。
试着执行一下:
输出结果是一个只有一个元素的列表[1],没什么奇怪的,对吧?
继续调用一次append_lst( )函数,也就是继续执行一次上面jupyter_notebook中的第二个单位格,得到如下结果:
嗯?为什么结果是[1, 1],第二次执行的时候,函数append_lst( ) 一字没改,调用的方式也一个字没有改,结果理应和第一次调用显示相同才对!这次的结果居然是列表[1, 1],而不是上一次调用得到的[1]!到底是什么力量在作祟,还是出现了某种神秘的意外?
再调用一次看看吧:
诶呦我的天,真是奇了怪了!第三次调用的结果比第二次还要长!同样的函数,三次调用三次结果都不同,这函数真是太不稳定了!小小的函数体中到底蕴藏了什么样的秘密?
在向各路大神求助的过程中,这个坑,曾经坑倒无数学习Python的少年,绝对算得上Python的经典大坑。这道题甚至出现在很多人的面试题中。
破局关键:可变对象
其中有一位大神一阵见血地指出了关键:说并不是所有带有缺省值的函数都会这么不讲道理,只有列表、集和等这样的可变对象才会出现这样的情况。这需要理解Python中的一个特性:所有的变量名都是各种对象的名字,不是对象的本身,而一个对象可以有多个变量名指向它。
对于int,float,string, tuple, dict这些不可变的变量,我们用赋值语句对他们进行赋值看起来像是修改了他们的值,但是本质上只是创建了一个新的对象,并且将原来的变量名指向新创建的对象。真正的修改是针对list、set这些可变变量而言的,如下图:
修改列表
首先创建一个[1, 2, 3]列表对象,并且用my_lst1这个变量名指向它,然后用另一个变量名my_lst_2指向my_lst_1所指向的对象,这样两个变量名就都指向了同样的对象[1, 2, 3]。通过"A is B"这样的判断语句,发现两个变量确实都指向了同样的对象。然后用append( )函数修改my_lst1所指向的对象,这时候打印出my_lst1,相应的值确实发生了改变。而在此刻打印出my_lst2的值,会发现即便刚刚没有手动修改my_lst2的值,但是其结果还是跟着改变了。这个实验可以说明:列表是一个可变对象,这个对象的值修改时,所有指向这个对象的变量也会跟着修改。
根本原因
大神说,在理解了可变变量的原理后,就可以理解一开始的问题了。再看一眼一开始的函数:
第一次调用函数时,变量lst确实被缺省值赋予了空列表,此后,根据append( )方法,lst被修改为[1],并且作为返回值传递给了lst_update。在这个时候,lst和lst_update两个变量同时指向了相同的对象[1]。在Python中对象的作用域由变量名决定,这个时候,由于指向对象[1]的变量lst_update是全局变量, 指向[1]的另一个变量lst因此也获得了全局变量的声明周期(但不是说lst就成了全局变量,因为lst并不能在函数append_lst( )之外的地方被使用),也就是说,lst这个变量并不会像普通的局部变量一样随着函数的结束而消亡。
这样第二次调用append_lst( )函数的时候,因为变量名lst依旧指向着[1]这个对象,所以其不在会被def append_lst(lst=[ ])语句重新赋予初始值[ ]。最后函数调用后返回的结果自然也是再对象[1]的基础上再添加,变成[1, 1]。
第三次调用时,lst和append_lst共同指向了[1, 1],调用的结果也是再[1, 1]上再添加,变成成了[1, 1, 1]。
实验验证
其实要验证大神的说法,也很简单,只需要在每次调用append_lst( )函数的时候用"is"方法判断变量lst和变量lst_update是否指向同样的对象就行:
很容易看出在第一次调用的时候,由于lst被赋予了空列表,其与lst_update指向的对象不是用一个,它们的id号也不是相同的。而在第二次和第三次调用的时候,lst和lst_update确实都指向这同样的对象,从它们的id号相同也可以看出。由此可见大神的说法确实是对的。
解决办法
大神说对了,但是这个坑背后的深层次原因就是Python处理机制的问题了。我以为先不用太去深究背后深层次原因,但是认得这个坑,知道这个坑的解决办法还是必要的。
下次如果遇到这样的情况:某个函数的参数是个可变对象,函数要对这个对象进行修改。可以不需要设置返回值和参数对象相同。因为可变对象在函数内进行修改,函数外指向该对象的全局变量也会得到修改。同时可以用bool值作为函数的返回类型,告诉调用处是否修改成功,如下图所示:
当然在具体的情况中,也应该想想有什么更好的方式去解决,总之下次遇到可变变量作为有缺省值的函数参数这样的坑的时候,千万不能一头跳进去。