任何人只要长时间摆弄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中很不幸。

其他答案,或至少其中一些答案,要么是第1点和第2点,但不是第3点,要么就是第3点而淡化第1点或第2点。但这三个都是真的。

在这里,在中途换马可能会导致严重的破坏,而且通过改变Python来直观地处理Stefano的开头片段可能会产生更多的问题。也许有人很了解Python的内部结构,就能解释一个后果雷区。然而

现有的行为不是Pythonic的,Python之所以成功,是因为该语言几乎没有违反最不令人惊讶的原则。这是一个真正的问题,无论根除它是否明智。这是一种设计缺陷。如果你通过尝试追踪行为来更好地理解语言,我可以说C++完成了所有这些以及更多的工作;例如,通过导航细微的指针错误,您可以学到很多东西。但这并不是Pythonic的:那些对Python足够关心并在这种行为面前坚持不懈的人都是被这种语言所吸引的人,因为Python比其他语言的惊喜要少得多。当他们惊讶于用很少的时间就能让一些东西发挥作用——而不是因为设计失误——我的意思是,隐藏的逻辑谜题——这违背了程序员的直觉时,达博人和好奇者就成了Python爱好者,因为Python很好用。

其他回答

Python:可变默认参数

将函数编译为函数对象时,将计算默认参数。当被该函数多次使用时,它们仍然是同一个对象。

当它们是可变的时,当它们发生突变时(例如,通过向其中添加元素),它们在连续调用时保持突变。

它们保持变异,因为它们每次都是同一个物体。

等效代码:

由于在编译和实例化函数对象时列表绑定到函数,因此:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

几乎完全等同于此:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

集会示威

这里有一个演示-您可以验证每次引用它们时它们都是相同的对象

看到列表是在函数完成编译到函数对象之前创建的,观察到每次引用列表时id都是相同的,观察到当第二次调用使用该列表的函数时该列表保持改变,观察从源打印输出的顺序(我方便地为您编号):

示例.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

并使用python example.py运行它:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

这是否违反了“最少惊讶”的原则?

这种执行顺序经常让Python的新用户感到困惑。如果您了解Python执行模型,那么它将变得非常令人期待。

对Python新用户的常规说明:

但这就是为什么对新用户的通常指示是创建默认参数,如下所示:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

这使用None单例作为一个sentinel对象来告诉函数我们是否得到了默认值以外的参数。如果没有参数,那么我们实际上希望使用新的空列表[]作为默认值。

正如关于控制流的教程部分所说:

如果您不希望在后续调用之间共享默认值,您可以改为这样编写函数:定义f(a,L=无):如果L为无:L=[]L.附加(a)返回L

假设您有以下代码

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

文件的相关部分:

执行函数定义时,从左到右计算默认参数值。这意味着在定义函数时,表达式将求值一次,并且每次调用都使用相同的“预计算”值。当默认参数是可变对象(例如列表或字典)时,这一点尤其重要:如果函数修改了对象(例如,通过将项附加到列表),则默认值实际上已被修改。这通常不是预期的。解决此问题的一种方法是使用None作为默认值,并在函数体中显式测试它,例如:def whats_on_the_telly(企鹅=无):如果企鹅为无:企鹅=[]企鹅追加(“动物园的财产”)返回企鹅

嗯,原因很简单,绑定是在代码执行时完成的,函数定义是执行的,嗯。。。当定义函数时。

比较一下:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

这段代码遭遇了完全相同的意外事件。香蕉是一个类属性,因此,当您向它添加内容时,它会添加到该类的所有实例中。原因完全相同。

这只是“它是如何工作的”,在函数情况下使它以不同的方式工作可能会很复杂,在类情况下可能是不可能的,或者至少会大大降低对象实例化的速度,因为您必须保留类代码,并在创建对象时执行它。

是的,这是出乎意料的。但一旦一分钱下降,它就完全符合Python的工作原理。事实上,这是一个很好的教学辅助工具,一旦你了解了为什么会发生这种情况,你就会更好地了解python。

也就是说,它应该在任何好的Python教程中占据突出位置。因为正如你提到的,每个人迟早都会遇到这个问题。

是的,这是Python中的一个设计缺陷

我看过所有其他答案,但我不相信。这种设计确实违反了最小惊讶的原则。

默认值可以设计为在调用函数时计算,而不是在定义函数时计算。Javascript是这样做的:

函数foo(a=[]){a.推动(5);返回a;}console.log(foo());//[5]console.log(foo());//[5]console.log(foo());//[5]

作为进一步证明这是一个设计缺陷的证据,Python核心开发人员目前正在讨论引入新语法来解决这个问题。请参阅本文:Python的后期绑定参数默认值。

为了进一步证明这是一个设计缺陷,如果你搜索“Python gotchas”,这个设计被称为gotcha,通常是列表中的第一个gotcha,在前9个Google结果(1、2、3、4、5、6、7、8、9)中。相反,如果你搜索“Javascript gotchas”,Javascript中默认参数的行为甚至一次都没有被提到过。

根据定义,Gotchas违反了最小惊讶的原则。它们令人惊讶。鉴于默认参数值的行为有着更高级的设计,不可避免的结论是Python的行为在这里代表了一个设计缺陷。