任何人只要长时间摆弄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?)

编辑:

Baczek举了一个有趣的例子。连同您的大多数评论,特别是Utaal的评论,我进一步阐述了:

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

在我看来,设计决策似乎与将参数范围放在哪里有关:放在函数内部,还是与函数“一起”?

在函数内部进行绑定意味着当函数被调用而不是被定义时,x被有效地绑定到指定的默认值,这将带来一个严重的缺陷:def行将是“混合”的,即部分绑定(函数对象)将在定义时发生,部分绑定(默认参数的赋值)将在函数调用时发生。

实际行为更加一致:当执行该行时,该行的所有内容都会得到求值,这意味着在函数定义时。


当前回答

实际上,这不是设计缺陷,也不是因为内部构件或性能。这仅仅是因为Python中的函数是一级对象,而不仅仅是一段代码。

只要你这样想,那么它就完全有意义了:函数是根据其定义进行求值的对象;默认参数是一种“成员数据”,因此它们的状态可能会从一个调用更改到另一个调用-与任何其他对象中的状态完全相同。

无论如何,effbot(Fredrik Lundh)在Python的默认参数值中对这种行为的原因有很好的解释。我发现它非常清楚,我真的建议阅读它来更好地了解函数对象是如何工作的。

其他回答

只需将函数更改为:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

假设您有以下代码

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

当我看到eat的声明时,最不令人惊讶的是,如果没有给定第一个参数,它将等于元组(“apples”、“banans”、“loganberries”)

然而,假设稍后在代码中

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

那么,如果默认参数是在函数执行时绑定的,而不是在函数声明时绑定的话,我会惊讶地发现(以一种非常糟糕的方式)水果已经被更改了。这将比发现上面的foo函数正在改变列表更让IMO惊讶。

真正的问题在于可变变量,所有语言在某种程度上都存在这个问题。这里有一个问题:假设在Java中我有以下代码:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

现在,我的映射是使用StringBuffer键在放置到映射中时的值,还是通过引用存储该键?不管怎样,都有人感到惊讶;或者是试图使用与放入对象的值相同的值将对象从Map中取出的人,或者是即使他们使用的键实际上与用于将其放入映射中的对象相同,但似乎无法检索对象的人(这实际上就是Python不允许将其可变内置数据类型用作字典键的原因)。

你的例子是一个很好的例子,Python新手会感到惊讶和被咬。但我认为,如果我们“修复”了这一点,那么这只会造成一种不同的情况,即它们会被咬,而且这种情况会更不直观。此外,在处理可变变量时总是如此;你总是会遇到这样的情况:根据编写的代码,某人可能会直觉地期望一种或相反的行为。

我个人喜欢Python当前的方法:在定义函数时计算默认函数参数,并且该对象始终是默认值。我想他们可以使用空列表进行特殊情况处理,但这种特殊情况会引起更大的惊讶,更不用说向后不兼容了。

已经很忙的话题,但从我在这里读到的内容来看,以下内容帮助我意识到它是如何在内部工作的:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

这里的解决方案是:

使用None作为默认值(或随机数对象),并在运行时打开它以创建值;或使用lambda作为默认参数,并在try块中调用它以获得默认值(这是lambda抽象的目的)。

第二个选项很好,因为函数的用户可以传入一个可调用的,它可能已经存在(例如类型)

使用None的简单解决方法

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]