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

其他回答

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

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

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

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

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

你问的是为什么会这样:

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防御5分

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

我过去认为在运行时创建对象是更好的方法。我现在不太确定,因为你确实失去了一些有用的功能,尽管这可能是值得的,无论是为了防止新手混淆。这样做的缺点是:

1.性能

def foo(arg=something_expensive_to_compute())):
    ...

如果使用了调用时求值,那么每次使用函数时都会调用代价高昂的函数,而无需参数。您要么为每次调用付出昂贵的代价,要么需要手动从外部缓存值,从而污染您的命名空间并增加冗长。

2.强制绑定参数

一个有用的技巧是在创建lambda时将lambda的参数绑定到变量的当前绑定。例如:

funcs = [ lambda i=i: i for i in range(10)]

这将返回分别返回0,1,2,3…的函数列表。如果行为发生了变化,它们会将i绑定到i的调用时间值,因此您将得到一个函数列表,所有函数都返回了9。

否则,实现这一点的唯一方法是使用i边界创建一个进一步的闭包,即:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3.反思

考虑代码:

def foo(a='test', b=100, c=[]):
   print a,b,c

我们可以使用inspect模块获取有关参数和默认值的信息

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

这些信息对于文档生成、元编程、装饰器等非常有用。

现在,假设违约行为可以被改变,这相当于:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

然而,我们已经失去了自省的能力,无法看到默认参数是什么。因为对象还没有被构造,所以我们无法在不调用函数的情况下获取它们。我们所能做的最好的方法是存储源代码并将其作为字符串返回。