Python对默认参数值的处理方法是少有的几个易使大多数新手Python程序员犯错的地方之一。(通常只犯一次)
导致困惑的地方是当你使用“可变”对象作为(参数的)默认值时的(程序)行为。(可变)也就是说值可以原地修改,像列表或字典。
看下面的例子:
就像你看到的那样,列表变得越来越长。如果你查看列表的ID,你会发现函数实际上总是返回同一个对象。
原因很简单:函数在每次调用时总是使用同一个对象。我们做的修改行为有“粘性”。
为什么会这样?
属于函数定义的默认参数值当且仅当“def”语句执行时求值。请看:
语言参考中的相关章节。
要注意Python中的“def”语句是可执行的,而且默认参数是在“def”语句的环境下求值的。如果“def”语句执行了多次,那么它每次将产生新的函数对象(对象会带着全新的默认值)。下面我们会看到这样的例子的。
那要怎么做?
迂回方法是,就像其他人已经提到了的,使用占位符值来替代默认值。None是一个常用的值:
如果你需要处理任意对象(包括None),你可以使用哨兵对象:
在很早以前,即在“object”对象引入之前,有时你会看到像下面这样的代码:
这个代码用于创建一个具有唯一ID的对象;这对中括号([])在每次执行时产生新的列表。
对可变默认参数的合法(合理)使用
最后,要提到很多高级的Python代码经常使用这个机制的好处。例如,假设你要在一个循环里创建了一堆UI按钮,而你可能会使用像下面这样的代码:
这样你会发现所有的回调函数都打印出相同的值(在这个情况下,很可能是9)。原因是Python的嵌套作用域是绑定到变量的,而不是绑定到对象值的。所以所有的回调函数实例将会看到当前(也是最后)的变量“i”的值。为了修正这个问题,使用下面的代码:
那个“i=i”的步伐将绑定参数“i”(一个局部变量)到当前的外部变量“i”的值。
两个其他的例子使用的是局部缓存:
(这对某些递归算法很好)
另一个例子,对应高度优化的代码,对全局名字的局部重新绑定:
详细来说,这是怎么工作的?
当Python执行“def”语句时,它使用一些已有的东西(包括编译了的函数体的代码和当前的名字空间),然后创建出一个新的函数对象。当它做这个的时候,默认值也会被求值。
这些各种各样的组件也能作为函数对象的属性而访问。使用我们先前定义过的函数:
由于你可以访问它们,因而你也可以修改它们:
当然,这可不是我推荐的正常使用方法。
另一个重置默认值的方法就是简单的重新执行一下那个相同的“def”语句。Python会重新创建一个新的到这个代码对象绑定,重新计算默认值,然后将这个函数对象赋值给同以前一样的那个变量。但是要着重指出,请在你确切知道你在做什么的时候才能这么做。
最后,如果你刚好有函数各个部分,而不是函数本身,你可以使用new模块中的function类来创建你自己的函数对象。