任何人只要长时间摆弄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中的函数是一级对象,而不仅仅是一段代码。

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

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

其他回答

使用None的简单解决方法

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]

最简短的答案可能是“定义就是执行”,因此整个论点没有严格意义。作为一个更做作的例子,您可以引用以下内容:

def a(): return []

def b(x=a()):
    print x

希望这足以表明,在def语句执行时不执行默认参数表达式并不容易,或者没有意义,或者两者兼而有之。

不过,我同意,当您尝试使用默认构造函数时,这是一个陷阱。

我将演示一种将默认列表值传递给函数的替代结构(它与字典同样适用)。

正如其他人广泛评论的那样,列表参数在定义时绑定到函数,而不是在执行时。由于列表和字典是可变的,因此对该参数的任何更改都将影响对该函数的其他调用。因此,对函数的后续调用将接收此共享列表,该列表可能已被对函数的任何其他调用更改。更糟糕的是,两个参数同时使用该函数的共享参数,而忽略了另一个参数所做的更改。

错误的方法(可能…):

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

您可以使用id:

>>> id(a)
5347866528

>>> id(b)
5347866528

根据Brett Slatkin的《有效的Python:59种编写更好Python的具体方法》,第20项:使用None和Docstring指定动态默认参数(第48页)

在Python中实现所需结果的惯例是提供默认值None,并记录实际行为在docstring中。

此实现确保对函数的每个调用都接收默认列表或传递给函数的列表。

首选方法:

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

“错误方法”可能有合法的用例,程序员希望共享默认列表参数,但这更可能是例外而不是规则。

是的,这是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的行为在这里代表了一个设计缺陷。

如果考虑到以下因素,这种行为并不奇怪:

尝试赋值时只读类属性的行为,以及函数是对象(在公认的答案中解释得很好)。

(2)的作用已在本主题中广泛讨论。(1) 很可能是令人惊讶的原因,因为这种行为在来自其他语言时并不“直观”。

(1) 在Python教程中对类进行了描述。尝试将值分配给只读类属性时:

…在最内部范围之外找到的所有变量都是只读(尝试写入这样的变量只会创建一个最内部范围中的新局部变量,保留相同的命名的外部变量保持不变)。

回顾最初的示例,并考虑以上几点:

def foo(a=[]):
    a.append(5)
    return a

这里foo是一个对象,a是foo的一个属性(在foo.func_defs[0]中可用)。由于a是一个列表,因此a是可变的,因此是foo读写属性。当函数实例化时,它被初始化为签名指定的空列表,并且只要函数对象存在,它就可用于读取和写入。

在不覆盖默认值的情况下调用foo使用foo.func_defs中的默认值。在这种情况下,foo.func_descfs[0]用于函数内对象的代码范围。更改foo.func_defs[0],它是foo对象的一部分,在执行foo中的代码之间持续存在。

现在,将其与文档中关于模拟其他语言的默认参数行为的示例进行比较,以便每次执行函数时都使用函数签名默认值:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

考虑到(1)和(2),可以看出为什么这会实现所需的行为:

当foo函数对象被实例化时,foo.func_defs[0]被设置为None,这是一个不可变的对象。当函数以默认值执行时(函数调用中没有为L指定参数),foo.func_defs[0](None)在本地作用域中可用为L。当L=[]时,foo.func_defs[0]处的赋值无法成功,因为该属性是只读的。根据(1),在局部作用域中创建一个新的局部变量(也称为L),并用于函数调用的其余部分。因此,对于未来的foo调用,foo.func_defs[0]保持不变。