任何人只要长时间摆弄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
    ...

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

只要你这样想,那么它就完全有意义了:函数是根据其定义进行求值的对象;默认参数是一种“成员数据”,因此它们的状态可能会从一个调用更改到另一个调用-与任何其他对象中的状态完全相同。

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

你为什么不反省一下?

我真的很惊讶没有人对可调用对象执行Python(2和3适用)提供的深刻反省。

给定一个简单的小函数func,定义为:

>>> def func(a = []):
...    a.append(5)

当Python遇到它时,它要做的第一件事就是编译它,以便为这个函数创建一个代码对象。在完成此编译步骤时,Python计算*,然后将默认参数(此处为空列表[])存储在函数对象本身中。正如上面提到的答案:列表a现在可以被认为是函数func的成员。

因此,让我们做一些内省,前后检查一下列表是如何在函数对象内部展开的。我使用的是Python 3.x,对于Python 2也是如此(在Python 2中使用__defaults__或func_faults;是的,两个名称表示相同的东西)。

执行前功能:

>>> def func(a = []):
...     a.append(5)
...     

Python执行此定义后,它将接受指定的任何默认参数(此处a=[]),并将它们填充到函数对象的__defaults__属性中(相关部分:Callables):

>>> func.__defaults__
([],)

好的,所以__defaults__中的单个条目是一个空列表,正如预期的那样。

执行后的功能:

现在让我们执行此函数:

>>> func()

现在,让我们再次看看这些__defaults__:

>>> func.__defaults__
([5],)

惊讶的?对象内部的值发生了变化!对函数的连续调用现在只需追加到嵌入的列表对象:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

所以,这就是为什么会出现这种“缺陷”的原因,因为默认参数是函数对象的一部分。这里没有什么奇怪的事情,只是有点令人惊讶。

解决此问题的常见方法是使用None作为默认值,然后在函数体中初始化:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

由于每次都会重新执行函数体,因此如果没有为a传递参数,则总是会得到一个新的空列表。


要进一步验证__defaults__中的列表与函数func中使用的列表相同,只需更改函数以返回函数体中使用的list a的id。然后,将其与__defaults__中的列表(__defaults_中的位置[0])进行比较,您将看到这些列表实际上是如何引用同一列表实例的:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

一切都有自省的力量!


*要验证Python在编译函数期间是否计算默认参数,请尝试执行以下操作:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

正如您会注意到的,在构建函数并将其绑定到名称栏的过程之前,会调用input()。

这不是设计缺陷。任何人被这个绊倒都是在做错事。

我认为有3种情况可能会遇到此问题:

您打算将参数修改为函数的副作用。在这种情况下,使用默认参数是没有意义的。唯一的例外是当您滥用参数列表以具有函数属性时,例如cache={},并且根本不需要使用实际参数调用函数。你打算不修改参数,但你不小心修改了它。这是一个错误,修复它。您打算修改参数以在函数内部使用,但不希望修改在函数外部可见。在这种情况下,您需要复制参数,无论它是否为默认值!Python不是一种按值调用的语言,因此它不会为您创建副本,您需要对此进行明确说明。

问题中的例子可能属于第1类或第3类。奇怪的是,它既修改了传递的列表,又返回了它;你应该选择其中之一。

我过去认为在运行时创建对象是更好的方法。我现在不太确定,因为你确实失去了一些有用的功能,尽管这可能是值得的,无论是为了防止新手混淆。这样做的缺点是:

1.性能

def foo(arg=something_expensive_to_compute())):
    ...

如果使用了调用时求值,那么每次使用函数时都会调用代价高昂的函数,而无需参数。您要么为每次调用付出昂贵的代价,要么需要手动从外部缓存值,从而污染您的命名空间并增加冗长。

2.强制绑定参数

一个有用的技巧是在创建lambda时将lambda的参数绑定到变量的当前绑定。例如:

funcs = [ lambda i=i: i for i in range(10)]

这将返回分别返回0,1,2,3…的函数列表。如果行为发生了变化,它们会将i绑定到i的调用时间值,因此您将得到一个函数列表,所有函数都返回了9。

否则,实现这一点的唯一方法是使用i边界创建一个进一步的闭包,即:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3.反思

考虑代码:

def foo(a='test', b=100, c=[]):
   print a,b,c

我们可以使用inspect模块获取有关参数和默认值的信息

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

这些信息对于文档生成、元编程、装饰器等非常有用。

现在,假设违约行为可以被改变,这相当于:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

然而,我们已经失去了自省的能力,无法看到默认参数是什么。因为对象还没有被构造,所以我们无法在不调用函数的情况下获取它们。我们所能做的最好的方法是存储源代码并将其作为字符串返回。