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

Tor*_*ler 17 python

可能重复:
Python中的"最小惊讶":可变默认参数

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

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!
Run Code Online (Sandbox Code Playgroud)

所以我知道这可以通过这样解决:

class One(object):
    def __init__(self, my_list=None):
        self.my_list = my_list if my_list is not None else []
Run Code Online (Sandbox Code Playgroud)

我想知道的是......为什么?为什么Python类是结构化的,以便在类的实例中保存默认值?

提前致谢!

Gre*_*ill 9

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

要修复它,请尝试:

def __init__(self, my_list=None):
    if my_list is None:
        my_list = []
    self.my_list = my_list
Run Code Online (Sandbox Code Playgroud)

  • 请注意,您的解决方案存在潜在错误:如果将空列表传递给函数,并希望对象复制对该列表的引用,则`my_list或[]`表达式将选择*new*empty list` []`而不是`my_list`(因为空列表是假的). (5认同)
  • 我个人认为`如果foo是None:foo = mutable_default`在大多数情况下都是反模式.现在,该函数意外地改变了从外部显式传入的值.此外,你失去了实际传递"无"的能力,这可能有意义也可能没有意义. (2认同)
  • @Ben +1,如果可以的话,更多.我更喜欢`def func(arg =()):arg = list(arg); 继续进行()`.假设首先需要一个可变值.考虑到我们也应该让用户传入一个生成器,没有令人信服的理由禁止它......通常我们需要在这种情况下复制数据,如果我们正在做任何事情而不是迭代它为非 - 突变目的. (2认同)

Ben*_*Ben 4

其他几个人指出这是 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']
Run Code Online (Sandbox Code Playgroud)

十分之九,如果您发现自己采用了 usingNone作为默认值并使用的“模式” if value is None: value = default,那么您不应该这样做。你不应该修改你的论点!参数不应视为由被调用代码拥有,除非明确记录为拥有它们的所有权。

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

class One(object):
    def __init__(self, my_list=[])
        self.my_list = list(my_list)
Run Code Online (Sandbox Code Playgroud)

现在,您正在从作为输入提供的列表初始化类的数据,而不是获取预先存在的列表的所有权。不存在两个单独的实例最终共享相同列表的危险,也不存在与调用者中调用者可能想要继续使用的变量共享该列表的危险。它还具有很好的效果,您的调用者可以提供元组、生成器、字符串、集合、字典、自制的自定义可迭代类等,并且您知道您仍然可以依靠 self.my_list 有一个方法,因为您自己创建了append它。

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

另一个问题是,如果 my_list 非常大,则副本可能会很昂贵。在那里你必须做出权衡。在这种情况下,也许最好只使用传入的列表,并使用该if my_list is None: my_list = []模式来防止所有默认实例共享一个列表。但是,如果您这样做,则需要在文档或类名称中明确说明,调用者将放弃他们用于初始化实例的列表的所有权。或者,如果您确实想构建一个列表只是为了包装在 的实例中One,也许您应该弄清楚如何将列表的创建封装的初始化中One,而不是首先构建它;毕竟,它确实是实例的一部分,而不是初始化值。但有时这不够灵活。

有时,您确实确实希望使用别名,并通过改变它们都可以访问的值来进行代码通信。然而,在做出这样的设计之前,我会认真思考。这会让其他人(以及当您在 X 个月内返回代码时)感到惊讶,因此文档再次成为您的朋友!

在我看来,对新的 Python 程序员进行有关“可变默认参数”陷阱的教育实际上(稍微)有害。我们应该问他们“你为什么要修改你的论点?” (然后指出默认参数在 Python 中的工作方式)。函数具有合理的默认参数这一事实通常是一个很好的指标,表明它打算接收预先存在的值的所有权,因此无论它是否获得了该参数,它都可能不应该修改该参数默认值。