任何人只要长时间摆弄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行将是“混合”的,即部分绑定(函数对象)将在定义时发生,部分绑定(默认参数的赋值)将在函数调用时发生。

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


当前回答

当我们这样做时:

def foo(a=[]):
    ...

…如果调用者没有传递a的值,我们将参数a分配给未命名列表。

为了简化讨论,让我们暂时为未命名列表命名。帕夫洛怎么样?

def foo(a=pavlo):
   ...

在任何时候,如果调用者没有告诉我们a是什么,我们就重用pavlo。

如果pavlo是可变的(可修改的),而foo最终对其进行了修改,那么在下次调用foo时我们会注意到这样的效果,而不指定a。

这就是你看到的(记住,pavlo被初始化为[]):

 >>> foo()
 [5]

现在,帕夫洛是[5]。

再次调用foo()将再次修改pavlo:

>>> foo()
[5, 5]

在调用foo()时指定a可确保不会触及pavlo。

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

所以,帕夫洛仍然是[5]。

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

其他回答

假设您有以下代码

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抽象的目的)。

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

这可能是真的:

有人正在使用每种语言/库功能,并且在这里改变行为是不明智的,但是

坚持上述两个特征是完全一致的,并且仍然提出另一点:

这是一个令人困惑的特性,在Python中很不幸。

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

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

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

我认为这个问题的答案在于python如何将数据传递给参数(通过值或引用传递),而不是可变性或python如何处理“def”语句。

简要介绍。首先,python中有两种数据类型,一种是简单的基本数据类型,如数字,另一种数据类型是对象。第二,当将数据传递给参数时,python按值传递基本数据类型,即将值的本地副本传递给本地变量,但按引用传递对象,即指向对象的指针。

承认以上两点,让我们解释一下python代码发生了什么。这只是因为通过对象的引用传递,但与可变/不可变无关,或者可以说“def”语句在定义时只执行一次。

[]是一个对象,因此python将[]的引用传递给a,即a只是指向[]的指针,该指针作为对象存储在内存中。只有一个[]副本,但是有很多引用。对于第一个foo(),列表[]通过append方法更改为1。但请注意,列表对象只有一个副本,该对象现在变为1。当运行第二个foo()时,effbot网页所说的(不再计算项目)是错误的。a被评估为列表对象,尽管现在对象的内容是1。这是通过引用传递的效果!foo(3)的结果可以很容易地以相同的方式导出。

为了进一步验证我的答案,让我们看看另外两个代码。

=====第2名========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]是一个对象,None也是(前者是可变的,后者是不可变的。但可变性与问题无关)。空间中没有任何东西,但我们知道它在那里,那里只有一个“无”的副本。因此,每次调用foo时,项都会被求值为None(而不是某个只求值一次的答案),明确地说,引用(或地址)为None。然后在foo中,item被更改为[],即指向另一个具有不同地址的对象。

=====第3位=======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

foo(1)的调用使项指向一个地址为11111111的列表对象[]。在后续的foo函数中,列表的内容被更改为1,但地址没有更改,仍然是11111111。然后foo(2,[])就要来了。虽然foo(2,[])中的[]与调用foo(1)时的默认参数[]具有相同的内容,但它们的地址不同!因为我们显式地提供了参数,所以项必须获取这个新[]的地址,比如2222222,并在进行一些更改后返回它。现在执行foo(3)。由于只提供了x,因此项必须再次采用其默认值。默认值是多少?它是在定义foo函数时设置的:位于11111111中的列表对象。因此,项目被评估为具有元素1的地址11111111。位于2222222的列表还包含一个元素2,但它不再由项目指向。因此,追加3将生成项目[1,3]。

从上面的解释中,我们可以看到,在接受的答案中推荐的effbot网页未能给出这个问题的相关答案。此外,我认为effbot网页中的一点是错误的。我认为关于UI.Button的代码是正确的:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

每个按钮都可以保存一个不同的回调函数,该函数将显示不同的i值。我可以提供一个示例来说明这一点:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

如果我们执行x[7](),我们将得到预期的7,x[9]()将得到9,即i的另一个值。