为什么使用 `arg=None` 可以解决Python中可变默认参数的问题?

37

我学习Python到了处理可变默认参数问题的阶段,参考这个

# BAD: if `a_list` is not passed in, the default will wrongly retain its contents between successive function calls
def bad_append(new_item, a_list=[]):
    a_list.append(new_item)
    return a_list

# GOOD: if `a_list` is not passed in, the default will always correctly be []
def good_append(new_item, a_list=None):
    if a_list is None:
        a_list = []
    a_list.append(new_item)
    return a_list
我知道a_list只在第一次遇到def语句时初始化,这就是为什么后续对bad_append的调用会使用同一个列表对象。
我不明白的是为什么good_append表现得不同。看起来a_list仍然只会被初始化一次;因此,if语句只会在函数第一次调用时为真,这意味着a_list只会在第一次调用时被重置为[],这意味着它仍然会累积所有过去的new_item值,并且仍然有错误。
为什么它不是这样的呢?我缺少哪些概念?每次运行good_append时,a_list是如何被清空的?
5个回答

31
看起来 a_list 只会被初始化一次。 “初始化”并不是 Python 中变量的操作,因为 Python 中的变量只是名称。只有对象才会被初始化,并且通过类的 __init__ 方法进行初始化。 当您编写 a = 0 时,这是一种赋值。这表示“a 将引用由表达式 0 描述的对象”。这不是初始化;a 可以在任何后续时间命名任何其他类型的任何内容,这是将其他内容分配给 a 的结果。赋值只是赋值。第一个并没有特殊之处。 当您编写 def good_append(new_item, a_list=None) 时,这不是“初始化”a_list。它正在设置对对象的内部引用,该对象是评估 None 的结果,因此当 good_append 在没有第二个参数的情况下调用时,该对象会自动分配给 a_list。 意味着每次只有在第一次调用时 a_list 才会被重置为 []。 不,每次在 a_list 为 None 开始时,a_list 都会被设置为 []。也就是说,当明确传递 None 或省略参数时会发生这种情况。 [] 的问题出现在于,在此上下文中只评估了一次表达式 []。当函数编译时,[] 被评估,创建了一个特定的列表对象(开始为空),并将该对象用作默认值。 good_append 运行时如何清除 a_list? 没有必要这样做。

您是否听说过“可变默认参数”这个问题?

None 不可变。

问题发生在修改参数默认值所引用的对象上。

a_list = [] 并不会修改 a_list 之前所引用的任何对象。它不能这样做;任意对象都不能神奇地转变为空列表。 a_list = [] 的意思是“a_list 应该停止引用之前所引用的对象,开始引用 []”。之前所引用的对象没有改变。

当函数编译时,如果其中一个参数有默认值,则该默认值 - 一个对象 - 被编译进函数中(函数本身也是一个对象!)。当您编写修改对象的代码时,对象会被修改。如果所引用的对象恰好是编译进函数中的对象,则它仍然会被修改。

但您无法修改 None。它是不可变的。

您可以修改 []。它是一个列表,列表是可变的。将项附加到列表会更改列表。


1
非常感谢你的出色回答。我正在努力决定是否将这个答案或@glglgl的答案标记为正确。另一个答案包含了使我能够理解你的答案的单个启示性短语;你的答案整体上更为详尽和易懂,但不知何故没有让我像另一个答案那样恍然大悟。如果有一种方法可以在一个问题上给予两个绿色勾选标记,你的答案肯定会成为另一个(如果我继续犹豫,它可能再次成为唯一的)。 - 75th Trombone

21

a_list的默认值(或者任何其他默认值)在函数初始化后存储在函数内部,因此可以以任何方式修改:

>>> def f(x=[]): return x
...
>>> f.func_defaults
([],)
>>> f.func_defaults[0] is f()
True

对于Python 3而言:

>>> def f(x=[]): return x
...
>>> f.__defaults__
([],)
>>> f.__defaults__[0] is f()
True

func_defaults中的值与函数内部众所周知的变量相同(在我的示例中返回以便从外部访问)。

换句话说,调用f()时发生了隐式的x = f.func_defaults [0]。如果随后修改该对象,则会保留该修改。

相比之下,函数内部的赋值始终会得到一个新的[]。任何修改都将持续到对该[]的最后引用消失;在下一次函数调用时,将创建一个新的[]

再次说明,[]并不总是在每次执行时获取相同的对象,但它(在默认参数的情况下)仅执行一次,然后被保留。


2
非常感谢,句子“调用f()时发生的情况是隐式的x = f.func_defaults[0]”对我理解很重要。 - 75th Trombone
1
如此之多,以至于我再次改变主意,并将其标记为正确答案。 - 75th Trombone
为了强调这一点:赋值x=[](在函数定义中)是通过代理执行的,第一部分f.__defaults__[0] = []在定义期间执行,第二部分x = f.__defaults__[0]在调用期间执行。 - Jann Poppinga
@user985366,“IOW”并不罕见。但是你说得对,最好明确而不是含糊其辞。 - glglgl

17

只有默认值是可变的时候才存在这个问题,而None不是。与函数对象一起存储的是默认值。调用函数时,函数的上下文将使用默认值进行初始化。

a_list = []

只是在当前函数调用的上下文中将一个新对象赋值给名称a_list。它不会以任何方式修改None


我的印象是,OP对赋值和作用域的心理模型是错误的。我重写了答案,以使这一点更清晰。 - phihag
我的赋值的心理模型确实是错误的;事实上,即使现在我对问题有了更好的理解,它仍然可能是错误的。我不明白的是,在函数定义中执行a_list = None时,函数内部实际上有同一对象的另一个名称,并且参数的可见名称在每次调用函数时都会被重新分配给该对象。 - 75th Trombone

4
不,在“good_insert”函数中,“a_list”并非仅初始化一次。
每次调用该函数而未指定“a_list”参数时,都将使用默认值,并使用新的“list”实例返回,新列表不会替换默认值。

0

Python教程中提到:

默认值仅被评估一次。

评估(仅一次)的默认值被内部存储(为简单起见命名为x)。

[]的情况: 当你用a_list默认为[]定义函数时,如果你不提供a_list则会将内部变量x赋值给它。因此,当你向a_list添加元素时,实际上是在向x中添加元素(因为a_listx现在引用同一个变量)。当你再次调用该函数而不使用a_list时,已更新的x会重新分配给a_list

case None: 变量x只被计算一次并存储了值None。如果你没有提供a_list,那么变量x将被赋值为a_list。但是你当然不会对x进行追加操作。你会重新将一个空数组赋值给a_list。此时,xa_list是不同的变量。同样地,当你再次调用函数而没有提供a_list时,它首先从x获取值None,但然后a_list再次被赋值为空数组。

请注意,对于a_list = []的情况,如果在调用函数时为a_list提供了显式值,则新参数不会覆盖x,因为它只被计算一次。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接