Python类函数的默认变量是类对象吗?
今天下午我在写代码的时候,发现了一个bug。我注意到我新创建的一个对象的默认值居然是从另一个对象那儿带过来的!比如说:
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!
我知道可以通过这样来解决这个问题:
class One(object):
def __init__(self, my_list=None):
self.my_list = my_list if my_list is not None else []
我想知道的是……为什么?为什么Python的类会让默认值在类的不同实例之间共享呢?
6 个回答
这是Python中默认参数的标准行为,不仅仅是在类里面。
想了解更多,可以查看这个链接:函数/方法参数的可变默认值。
这是Python默认值工作方式的一个已知特性,常常让不太注意的人感到惊讶。空数组对象[]
是在函数定义的时候创建的,而不是在函数调用的时候。
要解决这个问题,可以尝试:
def __init__(self, my_list=None):
if my_list is None:
my_list = []
self.my_list = my_list
有很多人提到,这其实是Python中“可变默认参数”问题的一个例子。简单来说,默认参数必须在函数“外部”存在,才能传递给函数。
但这个问题的真正根源与默认参数没有关系。每当你发现可变的默认值被修改可能会导致问题时,你需要问自己:如果是显式提供的值被修改,那会不会有问题?除非有人对你的类非常了解,否则以下的行为也会让人感到意外(因此可能会导致错误):
>>> 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']
通常情况下,如果你发现自己在用None
作为默认值,并且使用if value is None: value = default
的模式,那你其实不应该这样做。你应该避免修改你的参数!参数不应该被视为被调用代码所拥有,除非明确说明它们是被拥有的。
在这种情况下(尤其是因为你在初始化一个类实例,所以可变变量会存在很长时间,并可能被其他方法和其他代码使用),我会这样做:
class One(object):
def __init__(self, my_list=[])
self.my_list = list(my_list)
这样你就是从输入的列表中初始化你的类的数据,而不是占有一个已经存在的列表。这样就没有风险让两个不同的实例共享同一个列表,也不会与调用者的变量共享,而调用者可能还想继续使用这个变量。这样做还有个好处,就是你的调用者可以提供元组、生成器、字符串、集合、字典、自定义的可迭代类等,而你知道self.my_list一定有append
方法,因为你自己创建了它。
不过这里还有一个潜在问题,如果列表中的元素本身是可变的,那么调用者和这个实例之间仍然可能会意外干扰。我发现这在我的代码中并不常见(所以我不会自动对所有东西进行深拷贝),但你需要对此保持警惕。
另一个问题是,如果my_list可能非常大,复制会很耗费资源。在这种情况下,你需要做出权衡。也许最终还是使用传入的列表比较好,并使用if my_list is None: my_list = []
的模式来防止所有默认实例共享同一个列表。但如果这样做,你需要在文档或类名中明确说明,调用者放弃了他们用来初始化实例的列表的所有权。或者,如果你真的想要构建一个仅用于封装在One
实例中的列表,也许你应该考虑在One
的初始化过程中创建这个列表,而不是先构建它;毕竟,它实际上是实例的一部分,而不是一个初始化值。不过,有时候这样做的灵活性不够。
有时候你确实想要有别名的效果,让代码通过修改它们都能访问的值来进行沟通。不过在我决定采用这样的设计之前,我会非常谨慎。而且这会让其他人感到意外(包括你在几个月后回到代码时),所以再次强调,文档是你的好朋友!
在我看来,教育新的Python程序员关于“可变默认参数”的问题其实是(稍微)有害的。我们应该问他们“你为什么要修改你的参数?”(然后再指出Python中默认参数的工作方式)。函数有一个合理的默认参数,往往是一个很好的指示,说明它并不是用来接收一个已有值的所有权的,所以无论它是否得到了默认值,它可能都不应该修改这个参数。