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

Why does using `arg=None` fix Python's mutable default argument issue?

提问人:75th Trombone 提问时间:5/21/2012 最后编辑:smci75th Trombone 更新时间:12/21/2021 访问量:7841

问:

我正在学习 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_listdefbad_append

我不明白的是为什么工作有什么不同。看起来仍然只初始化一次;因此,该语句仅在第一次调用函数时为 true,这意味着只会在第一次调用时重置为,这意味着它仍然会累积所有过去的值并且仍然有问题。good_appenda_listifa_list[]new_item

为什么不呢?我错过了什么概念?每次运行时如何擦拭干净?a_listgood_append

功能 参数 可变

评论


答:

17赞 phihag 5/21/2012 #1

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

a_list = []

只需在当前函数调用的上下文中为名称分配一个新对象。它不会以任何方式修改。a_listNone

评论

0赞 phihag 5/21/2012
我的印象是,OP的分配和范围的思维模式是错误的。我重写了答案,使它更清楚。
0赞 75th Trombone 5/21/2012
我的思维模式确实是错误的;事实上,即使现在我对这个问题有了更好的理解,它仍然可能是。我不明白的是,当您在函数定义中执行时,函数内部对同一对象具有另一个名称,并且参数的可见名称在每次调用函数开始时都会重新分配给该对象。a_list = None
4赞 mata 5/21/2012 #2

不,in 不是只启动一次。good_inserta_list

每次调用函数而不指定参数时,都会使用默认值,并使用并返回新的实例 ,新列表不会替换默认值。a_listlist

21赞 glglgl 5/21/2012 #3

的默认值(或任何其他默认值)在初始化后存储在函数的内部,因此可以以任何方式进行修改: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

因此,in 中的值是相同的,它是众所周知的内部函数(并且在我的示例中返回以便从外部访问它。func_defaults

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

相反,函数内部的赋值总是得到一个新的 .任何修改都将持续到最后一次引用消失为止;在下一个函数调用中,将创建一个新的函数。[][][]

再次,在每次执行时都获得相同的对象是不正确的,但它(在默认参数的情况下)只执行一次然后保留。[]

评论

2赞 75th Trombone 5/21/2012
谢谢;“打电话时会发生什么是隐含的”这句话对我的理解至关重要。f()x = f.func_defaults[0]
1赞 75th Trombone 5/21/2012
…如此之多,以至于我再次改变主意,并将其标记为正确答案。
0赞 Jann Poppinga 4/9/2021
要说明这一点:赋值(在函数定义中)通过代理执行,第一部分在定义期间执行,第二部分在调用期间执行。x=[]f.__defaults__[0] = []x = f.__defaults__[0]
0赞 glglgl 12/21/2021
@user985366“IOW”一点也不罕见。但明确总比隐含好,你是对的。
31赞 Karl Knechtel 5/21/2012 #4

看起来a_list仍然只会初始化一次

“初始化”不会发生在 Python 中的变量上,因为 Python 中的变量只是名称。“初始化”只发生在对象上,它是通过类的方法完成的。__init__

当你写 时,这是一个作业。也就是说,“应指由表达式”描述的对象。它不是初始化; 可以在以后的任何时间命名任何类型的任何其他内容,这是由于将其他内容分配给 的结果。分配只是分配。第一个并不特别。a = 0a0aa

当你写 时,那不是“初始化”。它设置对对象的内部引用,即计算的结果,以便在没有第二个参数的情况下调用时,该对象会自动分配给 。def good_append(new_item, a_list=None)a_listNonegood_appenda_list

这意味着a_list只会在第一次调用时重置为 []

不,设置为开始的任何时间。也就是说,当显式传递或省略参数时。a_list[]a_listNoneNone

之所以出现问题,是因为在此上下文中计算一次表达式。当函数被编译、被计算时,将创建一个特定的列表对象 - 该对象恰好是空的 - 并且该对象被用作默认对象。[][][]

每次运行时如何擦拭干净?a_listgood_append

事实并非如此。它不需要。

你知道这个问题是如何被描述为“可变默认参数”的吗?

None不可变。

当您修改参数作为默认值的对象时,会出现此问题。

a_list = []不会修改前面引用的任何对象。它不能;任意对象无法神奇地就地转换为空列表。 意思是“应停止指代其先前所指的内容,而开始指代”。先前引用的对象保持不变。a_lista_list = []a_list[]

当函数被编译时,其中一个参数具有默认值,该值(一个对象)将被烘焙到函数中(它本身也是一个对象!当您编写更改对象的代码时,该对象也会发生更改。如果被引用的对象恰好是烘焙到函数中的对象,它仍然会发生变异。

但是你不能变异.它是不可变的。None

你可以变异。它是一个列表,列表是可变的。将项目追加到列表会改变列表。[]

评论

1赞 75th Trombone 5/21/2012
感谢您的精彩回答。我正在自杀,试图决定是将这个答案标记为正确@glglgl还是正确。另一个答案包含一个启发性的短语,使我能够理解你的答案;你的答案作为一个整体更加彻底和易于理解,但不知何故并没有以同样的方式让灯点击。如果有一种方法可以在一个问题上给出两个绿色复选标记,那么你的绝对是另一个(如果我继续胡思乱想,可能会再次成为唯一的复选标记)。
0赞 Tuhin Paul 5/12/2020 #5

python 教程

默认值仅计算一次。

计算的(仅一次)默认值存储在内部(为简单起见,请将其命名为 1)。x

案例 []当您定义函数时,默认为 ,如果不提供 ,则在 时为其分配内部变量 x。因此,当你追加到 时,你实际上是在追加 (因为 和 现在引用同一个变量)。当您再次调用没有 的函数时,更新的函数将重新分配给 。a_list[]a_lista_listxa_listxa_listxa_list

案例 该值计算一次并存储在 中。如果未提供 ,则将变量 x 分配给 a_list。但你当然不会附加。将空数组重新分配给 。在这一点上,是不同的变量。同样,当您再次调用 without 函数时,它首先从中获取值,然后a_list再次分配给空数组。Nonexa_listxa_listxa_lista_listNonex

请注意,在这种情况下,如果在调用函数时提供显式值,则新参数不会重写,因为该参数仅计算一次。a_list = []a_listx