【问题标题】:Python class function default variables are class objects? [duplicate]Python类函数默认变量是类对象吗? [复制]
【发布时间】:2011-10-13 20:58:28
【问题描述】:

可能重复:
“Least Astonishment” in Python: The Mutable Default Argument

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

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 类是结构化的,以便跨类的实例保存默认值?

提前致谢!

【问题讨论】:

  • 奇怪,让我想起了 JavaScript 中的原型链
  • 这是python函数的一个方面,而不是类。无论如何,this post 可以帮助您弄清楚为什么 Python 是这样设计的。
  • 好像最近几天我不断看到这个问题的新版本......
  • Python 函数(无论是方法还是普通函数)本身就是对象。默认参数绑定到参数名称(如果调用提供显式值,则隐藏);它的可见性是函数体。除了方法是定义类的成员这一事实之外,在类级别根本没有发生任何事情。

标签: python


【解决方案1】:

这是 Python 默认值工作方式的一种已知行为,粗心的人通常会感到惊讶。空数组对象[] 在函数定义时创建,而不是在调用时创建。

要修复它,请尝试:

def __init__(self, my_list=None):
    if my_list is None:
        my_list = []
    self.my_list = my_list

【讨论】:

  • 请注意,您的解决方案有一个潜在的错误:如果您将一个空列表传递给您的函数,打算让对象复制对该列表的引用,您的 my_list or [] 表达式将选择 new 空列表[] 而不是my_list(因为空列表是假的)。
  • 我个人认为if foo is None: foo = mutable_default 在大多数情况下是一种反模式。现在该函数只是意外地改变了从外部显式传入的值。另外,您将失去实际传递None 的能力,这可能有意义,也可能没有意义。
  • @Ben +1,如果可以的话。我更喜欢def func(arg=()): arg = list(arg); proceed()。假设首先需要一个可变值。考虑到我们还应该让用户传入一个生成器,而没有令人信服的理由来禁止它......通常我们需要在这种情况下复制数据,如果我们正在做任何事情而不是迭代它- 变异目的。
  • @S.Lott:我不是那种说“永远不要使用可变默认参数”的人。就我个人而言,我只是使用空列表作为我的默认参数,并且根本不改变函数的参数,除非这是可调用文件的功能的一部分。我的观点是,如果由于可变的默认参数而遇到错误,那么在接收到非默认值的情况下也可能是错误,if foo is None: foo = [] 没有解决这个问题,只会使错误更多微妙。
  • @S.Lott:假设我有一个函数,它接受字符串列表并将它们与其他格式一起写入文件。我允许列表默认为空。我发现多次调用它会改变默认值,所以我应用了None 默认技巧。但这只有在函数改变列表时才有必要。如果是这种情况,如果我传递一个我想再次用于其他用途的字符串列表会发生什么?它被破坏了。真正的问题不是默认值,而是函数不应该修改其参数作为其真正目的的副作用。
【解决方案2】:

Python 函数是对象。函数的默认参数是该函数的属性。因此,如果参数的默认值是可变的并且在您的函数内部进行了修改,则更改会反映在对该函数的后续调用中。

【讨论】:

    【解决方案3】:

    这是 Python 中任何地方的默认参数的标准行为,而不仅仅是在类中。
    更多解释见Mutable defaults for function/method arguments

    【讨论】:

      【解决方案4】:

      基本上,python 函数对象存储一个默认参数元组,这对于像整数这样的不可变对象来说很好,但列表和其他可变对象通常会就地修改,从而导致您观察到的行为。

      【讨论】:

        【解决方案5】:

        其他一些人指出这是 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']
        

        10 次中有 9 次,如果您发现自己使用 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 的实例中,也许您应该弄清楚如何将列表的创建封装在 inside 的初始化中One,而不是先构造它;毕竟,它实际上是实例的一部分,而不是初始化值。但有时这不够灵活。

        有时你真的很想继续使用别名,并通过改变他们都可以访问的值来进行代码通信。然而,在我承诺这样的设计之前,我会非常努力地思考。它会让其他人感到惊讶(当你在 X 个月后回到代码中时你也会感到惊讶),所以文档再次成为你的朋友!

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

        【讨论】:

        • 我同意你关于对象所有权的建议,但你会得到你所描述的那种行为,只要你传递对可变对象的引用,这对课程来说几乎是一样的用任何语言——你习惯了。可变默认陷阱是阴险的,因为它不直观,其他语言不这样做。
        • 但这只是我的观点。它会咬你,因为你对默认参数不小心。但是,如果您要改变传入的值,那么函数的目的几乎总是应该改变传入的值。在这种情况下,拥有默认值。如果有一个错误导致您不小心改变了默认值,那么可能还有一个更微妙的错误,您不小心改变了某人关心的传入值。
        • @Ben:我喜欢你的回答,但我有一个问题。我的代码的意图确实是一个工厂函数。在制作我应该遵循的工厂功能方面是否有良好的做法?比如使用__init__?
        • @TorelTwiddler:我添加了一个部分,说明我将如何处理您的 One 课程,以及其他要考虑的事情(不幸的是,这是一个权衡)。我希望它有帮助!我也去掉了关于工厂函数的评论,这可能有点令人困惑。我指的是,如果您希望您的参数每次都提供一个新值,那么 参数本身 可能是一个工厂函数(默认值为lambda: [])。不过,这很少是您真正想要做的,因此将其从我的答案中编辑出来。
        • @Ben:感谢您详细说明您的答案!阅读您的最新编辑后,我相信我的代码中没有明显的理由允许您将可变对象传递给它(谁的所有权将被接管)。我将在我的类初始化之后填充我的列表和字典,而不是完全避免更改传递的对象的任何问题。再次感谢您的详尽回答!
        【解决方案6】:

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

        例子:

        >>> class one:
        ...     myList = []
        ...
        >>>
        >>> one1 = one()
        >>> one1.myList
        []
        >>> one2 = one()
        >>> one2.myList.append("Hello Thar!")
        >>>
        >>> one1.myList
        ['Hello Thar!']
        >>>
        

        请注意,不仅myList 的值保持不变,而且myList每个实例 都指向相同的列表

        我自己遇到了这个错误/功能,并花了大约 3 个小时试图弄清楚发生了什么。在获取有效数据时进行调试是相当具有挑战性的,但它不是来自本地计算,而是来自以前的计算。

        这变得更糟了,因为这不仅仅是一个默认参数。您不能只将myList 放在类定义中,它必须 设置为等于某个值,尽管它设置为等于的值只评估一次

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

        【讨论】:

        • 这是一种类变量的定义。它在类中定义,并为该类保存一个值。而 instance variable 是在实例中定义的,并为该实例保存一个值。 -- 至于“它必须设置为等于某物”,每个 Python标签必须设置为等于某物。如果您现在不需要其他值,请将其设置为等于None。 -- “错误”是您期望 Python 的行为与您使用过的其他语言一样。 Python 不是其他语言,它是 Python。
        猜你喜欢
        • 2014-08-04
        • 2017-11-17
        • 1970-01-01
        • 2012-09-15
        • 2013-02-22
        • 1970-01-01
        • 1970-01-01
        • 2021-08-26
        • 2020-09-17
        相关资源
        最近更新 更多