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

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


当前回答

文件的相关部分:

执行函数定义时,从左到右计算默认参数值。这意味着在定义函数时,表达式将求值一次,并且每次调用都使用相同的“预计算”值。当默认参数是可变对象(例如列表或字典)时,这一点尤其重要:如果函数修改了对象(例如,通过将项附加到列表),则默认值实际上已被修改。这通常不是预期的。解决此问题的一种方法是使用None作为默认值,并在函数体中显式测试它,例如:def whats_on_the_telly(企鹅=无):如果企鹅为无:企鹅=[]企鹅追加(“动物园的财产”)返回企鹅

其他回答

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

这是一种性能优化。由于此功能,您认为这两个函数调用中哪一个更快?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

我会给你一个提示。这是拆卸(参见http://docs.python.org/library/dis.html):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

我怀疑有经验的行为是否有实际用途(谁真的在C中使用了静态变量,而没有滋生bug?)

正如您所看到的,使用不可变的默认参数会带来性能上的好处。如果它是一个频繁调用的函数,或者默认参数需要很长时间才能构造,那么这可能会有所不同。此外,请记住Python不是C。在C中,您可以使用非常免费的常量。在Python中,你没有这个好处。

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

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

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

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

你问的是为什么会这样:

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如何处理“def”语句。

简要介绍。首先,python中有两种数据类型,一种是简单的基本数据类型,如数字,另一种数据类型是对象。第二,当将数据传递给参数时,python按值传递基本数据类型,即将值的本地副本传递给本地变量,但按引用传递对象,即指向对象的指针。

承认以上两点,让我们解释一下python代码发生了什么。这只是因为通过对象的引用传递,但与可变/不可变无关,或者可以说“def”语句在定义时只执行一次。

[]是一个对象,因此python将[]的引用传递给a,即a只是指向[]的指针,该指针作为对象存储在内存中。只有一个[]副本,但是有很多引用。对于第一个foo(),列表[]通过append方法更改为1。但请注意,列表对象只有一个副本,该对象现在变为1。当运行第二个foo()时,effbot网页所说的(不再计算项目)是错误的。a被评估为列表对象,尽管现在对象的内容是1。这是通过引用传递的效果!foo(3)的结果可以很容易地以相同的方式导出。

为了进一步验证我的答案,让我们看看另外两个代码。

=====第2名========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]是一个对象,None也是(前者是可变的,后者是不可变的。但可变性与问题无关)。空间中没有任何东西,但我们知道它在那里,那里只有一个“无”的副本。因此,每次调用foo时,项都会被求值为None(而不是某个只求值一次的答案),明确地说,引用(或地址)为None。然后在foo中,item被更改为[],即指向另一个具有不同地址的对象。

=====第3位=======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

foo(1)的调用使项指向一个地址为11111111的列表对象[]。在后续的foo函数中,列表的内容被更改为1,但地址没有更改,仍然是11111111。然后foo(2,[])就要来了。虽然foo(2,[])中的[]与调用foo(1)时的默认参数[]具有相同的内容,但它们的地址不同!因为我们显式地提供了参数,所以项必须获取这个新[]的地址,比如2222222,并在进行一些更改后返回它。现在执行foo(3)。由于只提供了x,因此项必须再次采用其默认值。默认值是多少?它是在定义foo函数时设置的:位于11111111中的列表对象。因此,项目被评估为具有元素1的地址11111111。位于2222222的列表还包含一个元素2,但它不再由项目指向。因此,追加3将生成项目[1,3]。

从上面的解释中,我们可以看到,在接受的答案中推荐的effbot网页未能给出这个问题的相关答案。此外,我认为effbot网页中的一点是错误的。我认为关于UI.Button的代码是正确的:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

每个按钮都可以保存一个不同的回调函数,该函数将显示不同的i值。我可以提供一个示例来说明这一点:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

如果我们执行x[7](),我们将得到预期的7,x[9]()将得到9,即i的另一个值。