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

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


当前回答

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

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

(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]保持不变。

其他回答

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

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

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

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

你问的是为什么会这样:

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)的情况,我们将忽略它。

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

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

我有时会利用这种行为来替代以下模式:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

如果singleton仅由use_singleton使用,我喜欢以下模式作为替换:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

我用它来实例化访问外部资源的客户机类,也用来创建用于内存化的字典或列表。

由于我不认为这种模式是众所周知的,所以我确实发表了简短的评论,以防止未来的误解。

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

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

(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]保持不变。

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) 在某些情况下,默认参数的可变行为很有用。