我们就用实际的举例来演示我们今天所要讨论的主要内容。
下面一段代码定义了一个名为 generate_new_list_with
的函数。该函数的本意是在每次调用时都新建一个包含有给定 element
值的list。而实际运行结果如下:
可见代码运行结果并不和我们预期的一样。list_2
在函数的第二次调用时并没有得到一个新的list并填入2,而是在第一次调用结果的基础上append了一个2。为什么会发生这样在其他编程语言中简直就是设计bug一样的问题呢?
要了解这个问题的原因我们先需要一个准备知识,那就是:Python变量到底是如何实现的?
Python变量区别于其他编程语言的申明&赋值方式,采用的是创建&指向的类似于指针的方式实现的。即Python中的变量实际上是对值或者对象的一个指针(简单的说他们是值得一个名字)。我们来看一个例子。
对于传统语言,上面这段代码的执行方式将会是,先在内存中申明一个p
的变量,然后将1
存入变量p
所在内存。执行加法操作的时候得到2
的结果,将2
这个数值再次存入到p
所在内存地址中。可见整个执行过程中,变化的是变量p
所在内存地址上的值
面这段代码中,Python实际上是现在执行内存中创建了一个1
的对象,并将p
指向了它。在执行加法操作的时候,实际上通过加法操作得到了一个2
的新对象,并将p
指向这个新的对象。可见整个执行过程中,变化的是p
指向的内存地址
一句话来解释:Python函数的参数默认值,是在编译阶段就绑定的。
可见如果参数默认值是在函数编译compile
阶段就已经被确定。之后所有的函数调用时,如果参数不显示的给予赋值,那么所谓的参数默认值不过是一个指向那个在compile
阶段就已经存在的对象的指针。如果调用函数时,没有显示指定传入参数值得话。那么所有这种情况下的该参数都会作为编译时创建的那个对象的一种别名存在。
如果参数的默认值是一个不可变(Imuttable
)数值,那么在函数体内如果修改了该参数,那么参数就会重新指向另一个新的不可变值。而如果参数默认值是和本文最开始的举例一样,是一个可变对象(Muttable
),那么情况就比较糟糕了。所有函数体内对于该参数的修改,实际上都是对compile
阶段就已经确定的那个对象的修改。
当然最好的方式是不要使用可变对象作为函数默认值。如果非要这么用的话,下面是一种解决方案。还是以文章开头的需求为例:
在这个回答中,答题者认为出于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.
The phrases “this is not generally what was intended” and “a way around this is” smell like they’re documenting a design flaw.
好吧,这么看来,如果没有来自于Python作者的亲自陈清,这个问题的答案就一直会是一个谜了。