Python函数参数默认值陷阱及根本原因
引言
我们都知道,在使用Python调用函数时,添加默认参数可以简化函数的调用。可虽然默认参数很有用,但使用不当,也会掉坑里。
准备知识
要了解这个问题的原因我们先需要一个准备知识,那就是:Python变量到底是如何实现的?Python变量区别于其他编程语言的申明&赋值方式,采用的是创建&指向的类似于指针的方式实现的。即Python中的变量实际上是对值或者对象的一个指针(简单的说他们是值得一个名字)。我们来看一个例子。
p = 1
p = p + 1
对于传统语言,上面这段代码的执行方式将会是,先在内存中申明一个p的变量,然后将1存入变量p所在内存。执行加法操作的时候得到2的结果,将2这个数值再次存入到p所在内存地址中。可见整个执行过程中,变化的是变量p所在内存地址上的值;
上面这段代码中,Python实际上是现在执行内存中创建了一个1的对象,并将p指向了它。在执行加法操作的时候,实际上通过加法操作得到了一个2的新对象,并将p指向这个新的对象。可见整个执行过程中,变化的是p指向的内存地址。
基本原理
当你使用“可变”的对象作为函数中作为默认参数时会往往引起问题。因为在这种情况下参数可以在不创建新对象的情况下进行修改,例如 list dict。
1 | def function(list=[]): |
像你所看到的那样,list 变得越来越长。如果你仔细地查看这个 list。你会发现 list 一直是同一个对象。
1 | id(function()) |
我之后使用ruby来验证了一下:
1 | def function(list=[]) |
查看了一下每次调用function
创建的对象的id,如下所示:
1 | function.object_id |
可以很清楚的看出,ruby中不会存在这个问题。
为什么会发生这种情况?
当且仅当默认参数所在的“def”语句执行的时候,默认参数才会进行计算。请看文档描述的相关部分。
可见如果参数默认值是在函数编译compile
阶段就已经被确定。之后所有的函数调用时,如果参数不显示的给予赋值,那么所谓的参数默认值不过是一个指向那个在compile
阶段就已经存在的对象的指针。如果调用函数时,没有显示指定传入参数值得话。那么所有这种情况下的该参数都会作为编译时创建的那个对象的一种别名存在。如果参数的默认值是一个不可变(Imuttable
)数值,那么在函数体内如果修改了该参数,那么参数就会重新指向另一个新的不可变值。而如果参数默认值是和本文最开始的举例一样,是一个可变对象(Muttable
),那么情况就比较糟糕了。所有函数体内对于该参数的修改,实际上都是对compile
阶段就已经确定的那个对象的修改。
如何避免?
当然最好的方式是不要使用可变对象作为函数默认值。如果非要这么用的话,下面是一种解决方案。还是以文章开头的需求为例:
1 | def function(list=None): |
为什么Python要这么设计?
这个问题的答案在 StackOverflow 上可以找到答案。在这个回答中,答题者认为出于Python编译器的实现方式考虑,函数是一个内部一级对象。而参数默认值是这个对象的属性。在其他任何语言中,对象属性都是在对象创建时做绑定的。因此,函数参数默认值在编译时绑定也就不足为奇了。 然而,也有其他很多一些回答者不买账,认为即使是first-class object
也可以使用closure
的方式在执行时绑定。
甚至还有反驳者抛开实现逻辑,单纯从设计角度认为:只要是违背程序猿基本思考逻辑的行为,都是设计缺陷!下面是他们的一些论调: > Sorry, but anything considered “The biggest WTF in Python” is most definitely a design flaw. This is a source of bugs for everyone at some point, because no one expects that behavior at first - which means it should not have been designed that way to begin with.
好吧,这么看来,如果没有来自于Python作者的亲自陈清,这个问题的答案就一直会是一个谜了。