任何人只要长时间摆弄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中的一个设计缺陷
我看过所有其他答案,但我不相信。这种设计确实违反了最小惊讶的原则。
默认值可以设计为在调用函数时计算,而不是在定义函数时计算。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的行为在这里代表了一个设计缺陷。
TLDR:定义时间默认值是一致的,严格来说更具表达力。
定义函数会影响两个作用域:包含函数的定义作用域和函数所包含的执行作用域。虽然很清楚块是如何映射到作用域的,但问题是def<name>(<args=defaults>):属于:
... # defining scope
def name(parameter=default): # ???
... # execution scope
def-name部分必须在定义范围内求值,毕竟我们希望name在定义范围中可用。仅在函数内部求值将使其无法访问。
由于参数是一个常量名称,所以我们可以在定义名称的同时对其进行“求值”。这还有一个优点,它生成的函数具有已知签名name(parameter=…):,而不是裸名(…):。
现在,何时评估默认值?
一致性已经表明“在定义时”:def<name>(<args=defaults>)的所有其他属性:也最好在定义时进行评估。推迟部分时间将是一个令人惊讶的选择。
这两种选择也不等同:如果在定义时计算默认值,它仍然会影响执行时间。如果在执行时计算默认值,则不会影响定义时间。选择“at definition”可以表达两种情况,而选择“at executing”只能表达一种情况:
def name(parameter=defined): # set default at definition time
...
def name(parameter=default): # delay default until execution time
parameter = default if parameter is None else parameter
...
假设您有以下代码
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当前的方法:在定义函数时计算默认函数参数,并且该对象始终是默认值。我想他们可以使用空列表进行特殊情况处理,但这种特殊情况会引起更大的惊讶,更不用说向后不兼容了。
嗯,原因很简单,绑定是在代码执行时完成的,函数定义是执行的,嗯。。。当定义函数时。
比较一下:
class BananaBunch:
bananas = []
def addBanana(self, banana):
self.bananas.append(banana)
这段代码遭遇了完全相同的意外事件。香蕉是一个类属性,因此,当您向它添加内容时,它会添加到该类的所有实例中。原因完全相同。
这只是“它是如何工作的”,在函数情况下使它以不同的方式工作可能会很复杂,在类情况下可能是不可能的,或者至少会大大降低对象实例化的速度,因为您必须保留类代码,并在创建对象时执行它。
是的,这是出乎意料的。但一旦一分钱下降,它就完全符合Python的工作原理。事实上,这是一个很好的教学辅助工具,一旦你了解了为什么会发生这种情况,你就会更好地了解python。
也就是说,它应该在任何好的Python教程中占据突出位置。因为正如你提到的,每个人迟早都会遇到这个问题。