任何人只要长时间摆弄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 foo(a=[]):
    ...

…如果调用者没有传递a的值,我们将参数a分配给未命名列表。

为了简化讨论,让我们暂时为未命名列表命名。帕夫洛怎么样?

def foo(a=pavlo):
   ...

在任何时候,如果调用者没有告诉我们a是什么,我们就重用pavlo。

如果pavlo是可变的(可修改的),而foo最终对其进行了修改,那么在下次调用foo时我们会注意到这样的效果,而不指定a。

这就是你看到的(记住,pavlo被初始化为[]):

 >>> foo()
 [5]

现在,帕夫洛是[5]。

再次调用foo()将再次修改pavlo:

>>> foo()
[5, 5]

在调用foo()时指定a可确保不会触及pavlo。

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

所以,帕夫洛仍然是[5]。

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

其他回答

你问的是为什么会这样:

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):

这种行为很容易解释为:

函数(类等)声明只执行一次,创建所有默认值对象所有内容都通过引用传递

So:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c

a不改变-每次赋值调用都创建新的int对象-打印新对象b不变-新数组是从默认值构建并打印的c更改-对同一对象执行操作-并打印

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

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

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

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

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

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