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

其他回答

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

>>> 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,也不管是否涉及默认参数,使用作为参数传入的可变值作为本地临时变量是一个非常糟糕的想法。

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

1) 所谓的“可变默认参数”问题通常是一个特殊的例子,表明:“所有存在此问题的函数在实际参数上也存在类似的副作用问题,”这违反了函数式编程的规则,通常是不可想象的,应该将两者结合起来。

例子:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

解决方案:副本一个绝对安全的解决方案是首先复制或深度复制输入对象,然后对复制进行任何操作。

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

许多内置可变类型都有一个复制方法,比如some_dict.copy()或some_set.copy(),或者可以像somelist[:]或list(some_list)那样轻松复制。每个对象也可以通过copy.copy(any_object)进行复制,或者通过copy.deepcopy()进行更彻底的复制(如果可变对象是由可变对象组成的,则后者很有用)。有些对象基本上基于“文件”对象等副作用,无法通过复制进行有意义的复制。复制

类似SO问题的示例问题

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

它不应该保存在该函数返回的实例的任何公共属性中。(假设实例的私有属性不应按照约定从该类或子类之外进行修改。即_var1是私有属性)

结论:输入参数对象不应就地修改(变异),也不应绑定到函数返回的对象中。(如果我们更喜欢没有副作用的编程,这是强烈建议的。请参阅Wiki中关于“副作用”的内容(前两段与本文相关)。).)

2)只有当对实际参数的副作用是必需的,但对默认参数不需要时,有用的解决方案才是def。。。(var1=无):如果var1为无:var1=[]更多。。

3) 在某些情况下,默认参数的可变行为很有用。

您可以通过替换对象(以及与范围的关系)来解决此问题:

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

丑陋,但它奏效了。

这个“bug”给了我很多加班时间!但我开始看到它的潜在用途(但我还是希望它在执行时使用)

我会给你一个我认为有用的例子。

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

打印以下内容

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

你问的是为什么会这样:

def func(a=[], b = 2):
    pass

在内部并不等同于此:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

除了显式调用func(None,None)的情况,我们将忽略它。

换句话说,与其计算默认参数,不如存储每个参数,并在调用函数时计算它们?

一个答案可能就在这里——它可以有效地将每个带有默认参数的函数转换为闭包。即使所有数据都隐藏在解释器中,而不是完全关闭,数据也必须存储在某个地方。它会更慢,占用更多内存。