“最小的惊讶”和可变的默认参数

任何人用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中使用静态变量,而没有繁殖错误?)

编辑

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在Python的默认参数值中对这种行为的原因有非常好的解释。
我发现它很清楚,我真的建议阅读它以更好地了解函数对象如何工作。


假设你有下面的代码

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

def eat(food=fruits):
    ...

当我看到吃饭声明时,最令人惊讶的是认为如果没有给出第一个参数,它将等于元组("apples", "bananas", "loganberries")

但是,后来在代码中,我会做类似的事情

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

那么如果默认参数在函数执行而不是函数声明中被绑定,那么我会惊讶(以非常糟糕的方式)发现水果已经被改变。 这会比发现你上面的foo函数改变列表更令人惊讶。

真正的问题在于可变变量,所有语言在某种程度上都有这个问题。 这里有一个问题:假设在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的当前方法:默认函数参数在函数定义时被评估,并且该对象始终是默认值。 我想他们可以使用空白列表进行特殊处理,但是这种特殊的外壳会引起更多的惊讶,更不用说倒退不相容。


AFAICS还没有人发布文档的相关部分:

当函数定义被执行时,评估默认参数值。 这意味着该表达式在函数被定义时被评估一次,并且每次调用都使用相同的“预先计算”值。 这对于了解默认参数是否为可变对象(例如列表或字典)时尤为重要:如果函数修改对象(例如,通过将项目附加到列表中),则默认值将被有效修改。 这通常不是预期的。 解决这个问题的方法是使用None作为默认值,并在函数体中明确地测试它[...]

链接地址: http://www.djcxy.com/p/743.html

上一篇: "Least Astonishment" and the Mutable Default Argument

下一篇: Change the author and committer name and e