任何人只要长时间摆弄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 bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

其他回答

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

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

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

每个其他的答案都解释了为什么这实际上是一个好的和期望的行为,或者为什么你无论如何都不需要这个。我是为那些顽固的人准备的,他们想行使自己的权利,让语言服从自己的意愿,而不是相反。

我们将使用一个装饰器来“修复”这个行为,该装饰器将复制默认值,而不是为保留在默认值的每个位置参数重复使用相同的实例。

import inspect
from copy import deepcopy  # copy would fail on deep arguments like nested dicts

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(deepcopy(arg))
        return function(*new_args, **kw)
    return wrapper

现在让我们使用这个装饰器重新定义我们的函数:

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

foo() # '[5]'
foo() # '[5]' -- as desired

对于具有多个参数的函数来说,这一点尤为简洁。比较:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

with

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

需要注意的是,如果您尝试使用关键字args,则上述解决方案会中断,如下所示:

foo(a=[4])

可以调整装饰器以允许这一点,但我们将此作为读者的练习;)

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

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

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

你问的是为什么会这样:

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

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

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

我对Python解释器的内部工作一无所知(我也不是编译器和解释器的专家),所以如果我提出任何不合理或不可能的建议,不要怪我。

假设python对象是可变的,我认为在设计默认参数时应该考虑到这一点。实例化列表时:

a = []

你希望得到一个新的列表。

为什么a=[]

def x(a=[]):

在函数定义而不是调用上实例化新列表?这就像你在问“如果用户不提供参数,那么实例化一个新列表,并将其作为调用者生成的列表使用”。我认为这是模棱两可的:

def x(a=datetime.datetime.now()):

用户,是否希望a默认为定义或执行x时对应的日期时间?在本例中,与前一例一样,我将保持与默认参数“赋值”是函数的第一条指令(函数调用时调用datetime.now())相同的行为。另一方面,如果用户想要定义时间映射,他可以写:

b = datetime.datetime.now()
def x(a=b):

我知道,我知道:这是一个结束。或者Python可以提供一个关键字来强制定义时间绑定:

def x(static a=b):