跳转至

25. 最小经验原则(POLA)与可变默认参数

题目

任何长时间学习Python的人都会遇到下面的问题。

def foo(a=[]):
    a.append(5)
    return a

Python初学者期望这个函数总是会返回一个只包含一个元素的列表:[5],结果并非如此。

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

我的主管曾遇到过这个问题,并称其为语言的“戏剧性设计缺陷”。我回答说这个行为可能另有深意,如果不理解内部实现,那确实令人费解。但是,我没法解释:在函数定义中绑定默认参数的原因是什么,为什么不是在函数执行时?我怀疑这种方式是否具有实际用处(就如在C中使用静态变量而没有引发bug)。

这里有一个有趣的例子:

>>> def a():
...     print("a executed")
...     return []
...
>>>
>>> def b(x=a()):
...     x.append(5)
...     print(x)
...
a executed
>>> b()
[5]
>>> b()
[5, 5]

默认参数在函数定义时就已经计算好了。链接

回答一

事实上,这并不是一个设计缺陷,而且与内部实现、性能无关。

这仅仅是因为一个事实:在Python中函数是一等公民,而不只是代码片段。

继续深入思考,你会觉得这是合理的:函数也是对象,在定义时被执行得到的对象;默认参数类似“成员数据”,因此它们的状态在多次调用后会发生改变——就如在任何其他对象里一样。

对于这个问题,Effbot在Default Parameter Values in Python完美地解释了这种行为的原因。

我觉得这篇文章简洁明了,如果想对函数对象是如何工作的有更好的理解,也建议阅读。

为什么会这样

默认参数值会被计算,当且仅当其所属的def语句被执行。

def是Python中的可执行语句,而且默认参数是在def语句环境里被计算。如果执行def语句多次,每次它将会产生新的函数对象(默认参数也会重新计算)

替代方法

使用占位符代替修改默认值,None是个很好的选择。

def myfunc(value=None):
    if value is None:
        value = []
    # modify value here

具体是怎么执行的

当Python执行def语句时,它需要一些已经生成的部分(包括函数体和当前命名空间的编译代码)创建一个新的函数对象。默认参数也是在这时候被计算的。

各个部分作为函数对象的属性:

>>> function.func_name
'function'
>>> function.func_code
<code object function at 00BEC770, file "<stdin>", line 1>
>>> function.func_defaults
([1, 1, 1],)
>>> function.func_globals
{'function': <function function at 0x00BF1C30>,
'__builtins__': <module '__builtin__' (built-in)>,
'__name__': '__main__', '__doc__': None}

因为我们可以访问到默认参数,因此可以改变它们

>>> function.func_defaults[0][:] = []
>>> function()
[1]
>>> function.func_defaults
([1],)

另一种重置默认参数的方法时简单地重新执行相同的def语句。Python将会创建创建新的绑定给这个函数对象,重新计算默认参数,像之前一样将函数对象赋值给同一个变量。但是话说回来,当且仅当你知道你在做什么时才这么去使用。