任何人只要长时间摆弄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行将是“混合”的,即部分绑定(函数对象)将在定义时发生,部分绑定(默认参数的赋值)将在函数调用时发生。

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


当前回答

这是一种性能优化。由于此功能,您认为这两个函数调用中哪一个更快?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

我会给你一个提示。这是拆卸(参见http://docs.python.org/library/dis.html):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

我怀疑有经验的行为是否有实际用途(谁真的在C中使用了静态变量,而没有滋生bug?)

正如您所看到的,使用不可变的默认参数会带来性能上的好处。如果它是一个频繁调用的函数,或者默认参数需要很长时间才能构造,那么这可能会有所不同。此外,请记住Python不是C。在C中,您可以使用非常免费的常量。在Python中,你没有这个好处。

其他回答

Python防御5分

简单:行为在以下意义上很简单:大多数人只会陷入一次,而不是几次。一致性:Python始终传递对象,而不是名称。显然,默认参数是函数的一部分标题(而不是函数体)。因此,应该对其进行评估在模块加载时(并且仅在模块加载时间,除非嵌套),而不是在函数调用时。有用性:正如Frederik Lundh在解释中指出的在“Python中的默认参数值”中当前行为对于高级编程非常有用。(谨慎使用。)足够的文档:在最基本的Python文档中,在教程中,这个问题被大声宣布为第节第一小节中的“重要警告”“更多关于定义函数”。警告甚至使用粗体,这很少应用于标题之外。RTF:阅读详细手册。元学习:落入陷阱实际上是一个非常有帮助的时刻(至少如果你是一个反思型学习者),因为你随后会更好地理解这一点上述“一致性”将教你很多关于Python的知识。

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

def a(): return []

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

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

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

使用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]

有一种简单的方法可以理解为什么会发生这种情况。

Python在命名空间中从上到下执行代码。

“内部”恰恰体现了这一规则。

这种选择的原因是“让语言适合你的头脑”。所有奇怪的角落情况都倾向于简化为在命名空间中执行代码:默认免疫、嵌套函数、类(编译完成时有一点补丁)、自参数等。类似地,复杂语法可以用简单语法编写:a.foo(…)只是a.lookup('fo').__call__(a,…)。这适用于列表理解;装饰工;元类;以及更多。这可以让你看到一个近乎完美的奇怪角落。这种语言适合你的头脑。

你应该坚持下去。学习Python对语言有一段时间的不满,但它会让你感到舒服。这是我用过的唯一一种语言,你越看角落里的案例,它就越简单。

继续黑客攻击!做好记录。

对于您的特定代码,太详细了:

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

foo()

是一个语句,相当于:

开始创建代码对象。现在就解释(a=[])。[]是参数a的默认值。它是列表类型的,因为[]总是这样。将:之后的所有代码编译成Python字节码,并将其粘贴到另一个列表中。使用“code”字段中的参数和代码创建可调用字典将可调用对象添加到“foo”字段中的当前命名空间。

然后,它转到下一行foo()。

它不是保留字,所以在名称空间中查找它。调用函数,该函数将使用列表作为默认参数。开始在其命名空间中执行其字节码。append不会创建新列表,因此旧列表被修改。

这实际上与默认值无关,只是当您使用可变默认值编写函数时,它通常会出现意外行为。

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

在这段代码中看不到默认值,但您会遇到完全相同的问题。

问题是,foo正在修改从调用方传入的可变变量,而调用方并不期望这样做。如果函数的调用类似于append_5,那么这样的代码就可以了;那么调用者将调用函数以修改传入的值,并且行为是预期的。但是这样的函数不太可能采用默认参数,并且可能不会返回列表(因为调用者已经有了对该列表的引用;它刚刚传入的那个)。

您的原始foo(带有默认参数)不应该修改a,无论它是显式传入还是获得默认值。除非从上下文/名称/文档中可以清楚地看到参数应该被修改,否则代码应该保留可变参数。无论我们是否使用Python,也不管是否涉及默认参数,使用作为参数传入的可变值作为本地临时变量是一个非常糟糕的想法。

如果在计算过程中需要破坏性地操作本地临时变量,并且需要从参数值开始操作,则需要创建副本。