最近学习编程的过程中,被一个大坑折磨了挺久。

当时我正在编写一个函数,函数的过程的参数是一个带缺省值的列表对象,该函数需要对列表进行修改,最终函数的返回值是这个修改后的列表。整个函数可以抽象成下面这样简单的函数:


修改列表函数

可以看到append_lst( )这个函数特别简单,就是将输入变量lst的尾部添加一个1,最后将其返回。最下面两行是将调用得到的列表赋值给lst_update,然后将其打印出来。

试着执行一下:


第一次调用append_lst( )

输出结果是一个只有一个元素的列表[1],没什么奇怪的,对吧?

继续调用一次append_lst( )函数,也就是继续执行一次上面jupyter_notebook中的第二个单位格,得到如下结果:


第二次调用append_lst( )

嗯?为什么结果是[1,  1],第二次执行的时候,函数append_lst( ) 一字没改,调用的方式也一个字没有改,结果理应和第一次调用显示相同才对!这次的结果居然是列表[1, 1],而不是上一次调用得到的[1]!到底是什么力量在作祟,还是出现了某种神秘的意外?

再调用一次看看吧:


第三次调用append_lst( )

诶呦我的天,真是奇了怪了!第三次调用的结果比第二次还要长!同样的函数,三次调用三次结果都不同,这函数真是太不稳定了!小小的函数体中到底蕴藏了什么样的秘密?

在向各路大神求助的过程中,这个坑,曾经坑倒无数学习Python的少年,绝对算得上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的值,但是其结果还是跟着改变了。这个实验可以说明:列表是一个可变对象,这个对象的值修改时,所有指向这个对象的变量也会跟着修改。

根本原因

大神说,在理解了可变变量的原理后,就可以理解一开始的问题了。再看一眼一开始的函数:


append_lst( )函数

第一次调用函数时,变量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值作为函数的返回类型,告诉调用处是否修改成功,如下图所示:


一种代替原方案的模式

当然在具体的情况中,也应该想想有什么更好的方式去解决,总之下次遇到可变变量作为有缺省值的函数参数这样的坑的时候,千万不能一头跳进去。