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

新手上路,请多包涵

任何长期使用 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 行将是“混合”的意思是部分绑定(函数对象)发生在定义时,部分(默认参数的分配)发生在函数调用时。

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

原文由 Stefano Borini 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 279
2 个回答

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

一旦你这样想,它就完全有道理了:函数是一个根据其定义求值的对象;默认参数是一种“成员数据”,因此它们的状态可能会从一个调用更改为另一个调用 - 与任何其他对象完全一样。

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

原文由 rob 发布,翻译遵循 CC BY-SA 4.0 许可协议

假设你有以下代码

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

def eat(food=fruits):
    ...

当我看到 eat 的声明时,最不奇怪的是认为如果没有给出第一个参数,它将等于元组 ("apples", "bananas", "loganberries")

但是,假设稍后在代码中,我做了类似的事情

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

那么如果默认参数是在函数执行时绑定的而不是函数声明时,我会惊讶地(以一种非常糟糕的方式)发现 fruits 已经被改变了。这比发现上面的 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 中取出的人使用与他们放入的值相同的值,要么是即使他们的密钥似乎也无法检索他们的对象的人’ re using 实际上是用于将其放入映射中的同一对象(这实际上是 Python 不允许将其可变内置数据类型用作字典键的原因)。

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

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

原文由 Eli Courtwright 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题