python类函数的默认变量是类对象吗?

Python class function default variables are class objects?

本问题已经有最佳答案,请猛点这里访问。

Possible Duplicate:
“Least Astonishment” in Python: The Mutable Default Argument

今天下午我写了一些代码,在代码中偶然发现了一个错误。我注意到我新创建的一个对象的默认值是从另一个对象传递过来的!例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class One(object):
    def __init__(self, my_list=[]):
        self.my_list = my_list

one1 = One()
print(one1.my_list)
[] # empty list, what you'd expect.

one1.my_list.append('hi')
print(one1.my_list)
['hi'] # list with the new value in it, what you'd expect.

one2 = One()
print(one2.my_list)
['hi'] # Hey! It saved the variable from the other One!

所以我知道这样做可以解决问题:

1
2
3
class One(object):
    def __init__(self, my_list=None):
        self.my_list = my_list if my_list is not None else []

我想知道的是…为什么?为什么要对python类进行结构化,以便在类的实例之间保存默认值?

事先谢谢!


这是Python默认值工作方式的一种已知行为,对于不谨慎的人来说,这通常是令人惊讶的。空数组对象[]是在定义函数时创建的,而不是在调用函数时创建的。

要修复它,请尝试:

1
2
3
4
def __init__(self, my_list=None):
    if my_list is None:
        my_list = []
    self.my_list = my_list


其他一些人指出,这是Python中"可变默认参数"问题的一个实例。基本原因是默认参数必须存在于函数的"外部"才能传递给函数。

但这个问题的真正根源与默认参数无关。任何时候,如果修改可变的默认值都是不好的,您真的需要问问自己:如果修改了显式提供的值,这会是不好的吗?除非有人非常熟悉您的类的胆量,否则以下行为也会非常令人惊讶(因此导致错误):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class One(object):
...     def __init__(self, my_list=[]):
...         self.my_list = my_list
...
>>> alist = ['hello']
>>> one1 = One(alist)
>>> alist.append('world')
>>> one2 = One(alist)
>>>
>>> print(one1.my_list) # Huh? This isn't what I initialised one1 with!
['hello', 'world']
>>> print(one2.my_list) # At least this one's okay...
['hello', 'world']
>>> del alist[0]
>>> print one2.my_list # What the hell? I just modified a local variable and a class instance somewhere else got changed?
['world']

10次中有9次,如果你发现自己在使用None作为默认值并使用if value is None: value = default的"模式",你不应该这样做。你不应该修改你的论点!参数不应该被视为被调用代码的所有者,除非它被明确地记录为拥有它们的所有权。

在这种情况下(尤其是因为您正在初始化一个类实例,所以可变变量将使用很长时间,并被其他方法和可能从实例中检索它的其他代码使用),我将执行以下操作:

1
2
3
class One(object):
    def __init__(self, my_list=[])
        self.my_list = list(my_list)

现在,您要从作为输入提供的列表中初始化类的数据,而不是取得一个预先存在的列表的所有权。两个独立的实例最终共享同一个列表,或者列表与调用者中的变量共享(调用者可能希望继续使用该变量),都没有危险。您的调用者还可以提供元组、生成器、字符串、集合、字典、自制的自定义可重写类等,这也有很好的效果,而且您知道您仍然可以依靠self.my-list有一个append方法,因为您是自己创建的。

这里仍然存在一个潜在的问题,如果列表中包含的元素本身是可变的,那么调用者和这个实例仍然会意外地相互干扰。我发现在我的代码实践中,这并不是一个经常出现的问题(所以我不会自动地对所有内容进行深入的复制),但是你必须意识到这一点。

另一个问题是,如果我的_列表非常大,那么拷贝可能很昂贵。在那里你必须权衡一下。在这种情况下,最好还是使用传入列表,并使用if my_list is None: my_list = []模式来防止所有默认实例共享一个列表。但是,如果您这样做了,您需要在文档或类的名称中明确表示,调用方正在放弃其用于初始化实例的列表的所有权。或者,如果您真的想构建一个列表只是为了封装在一个One的实例中,那么也许您应该考虑如何在One的初始化中封装列表的创建,而不是首先构造它;毕竟,它实际上是实例的一部分,而不是初始化值。但有时这还不够灵活。

有时,您真的希望进行别名操作,并且让代码通过改变它们都可以访问的值进行通信。然而,在我致力于这样一个设计之前,我想得很努力。它会让其他人吃惊(当你在x个月内回到代码中时),所以文档也是你的朋友!

在我看来,向新的Python程序员介绍"可变默认参数"gotcha实际上(稍微)有害。我们应该问他们"你为什么要修改你的论点?"(然后指出默认参数在Python中的工作方式)。一个函数有一个合理的默认参数,这通常是一个很好的指标,表明它并不是用来接收一个预先存在的值的所有权的,所以不管它是否得到默认值,它可能都不应该修改这个参数。


基本上,python函数对象存储了一个默认参数的元组,这对于整数等不可变的事物来说是很好的,但是列表和其他可变对象通常在适当的位置进行修改,从而导致您观察到的行为。


这是默认参数在Python中任何位置(而不仅仅是在类中)的标准行为。有关详细说明,请参阅函数/方法参数的可变默认值。


python函数是对象。函数的默认参数是该函数的属性。因此,如果参数的默认值是可变的,并且在函数内部进行了修改,那么这些更改将反映在对该函数的后续调用中。


这不是一个答案,但值得注意的是,对于在任何类函数之外定义的类变量也是如此。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class one:
...     myList = []
...
>>>
>>> one1 = one()
>>> one1.myList
[]
>>> one2 = one()
>>> one2.myList.append("Hello Thar!")
>>>
>>> one1.myList
['Hello Thar!']
>>>

注意,myList的值不仅持续存在,而且myList的每个实例都指向同一个列表。

我自己也遇到了这个bug/特性,花了大约3个小时的时间试图弄清楚到底发生了什么。在获取有效数据时进行调试是相当困难的,但这不是来自本地计算,而是以前的计算。

更糟的是,这不仅仅是一个默认参数。你不能只把myList放在类定义中,它必须被设置为等于某个值,尽管它被设置为等于的值只被计算一次。

至少对我来说,解决方案是简单地在__init__中创建所有类变量。