【问题标题】:Python class and instance attribute confusion [duplicate]Python类和实例属性混淆[重复]
【发布时间】:2012-05-31 12:09:23
【问题描述】:

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

上课

class ValidationResult():
    def __init__(self, passed=True, messages=[], stop=False):
        self.passed = passed
        self.messages = messages
        self.stop = stop

运行

foo = ValidationResult()
bar = ValidationResult() 
foo.messages.append("Foos message")  
print foo.messages
print bar.messages

产生

['Foos message']
['Foos message']

这个

foo = ValidationResult()
bar = ValidationResult(messages=["Bars message"]) 
foo.messages.append("Foos message")  
print foo.messages
print bar.messages

生产

['Foos message']
['Bars message']

我想我在这里错过了理解实例属性的机会。在第一个示例中,我预计 Foos message 仅适用于 foo。什么是声明对象属性只能通过其实例可变的正确方法?

使用 Python 2.7.1

【问题讨论】:

标签: python


【解决方案1】:

你可以在这里看到发生了什么:

>>> class ValidationResult():
...     def __init__(self, passed=True, messages=[], stop=False):
...         self.passed = passed
...         self.messages = messages
...         print id(self.messages)
...         self.stop = stop
... 
>>> foo = ValidationResult()
4564756528
>>> bar = ValidationResult()
4564756528

默认参数始终是内存中的同一个对象。列表的一种快速解决方法是为每个实例创建列表的副本:

>>> class ValidationResult():
...     def __init__(self, passed=True, messages=[], stop=False):
...         self.passed = passed
...         self.messages = messages[:]
...         print id(self.messages)
...         self.stop = stop
... 
>>> foo = ValidationResult()
4564756312
>>> bar = ValidationResult()
4564757032

【讨论】:

  • 我从没想过制作副本——这是一个好主意,除非用户希望传递的列表与 ValidationResult 中的列表相同。例如mylist=['foo'];v=ValidationResult(messages=mylist); mylist is v.messages #False, Huh? -- 但这可能不是通常预期的行为......
【解决方案2】:

用作参数messages 的默认值的空列表是一个全局变量。因此,在您的第一个示例中,foo.messages is bar.messages 为真,而在您的第二个示例中,您的messages=["Bars message"] 导致bar.messages is not foo.messages 为真。这是最经典的一个陷阱!

【讨论】:

    【解决方案3】:

    这是 python 函数中的一个小怪癖,没有类也可以看到:

    def foo(bar=[]):
        bar.append('boo')
        print bar
    
    foo()
    foo()
    

    “问题”是加载模块时创建默认参数(bar)。如果您没有显式传递其他内容,相同的对象将继续作为 foo 的默认参数传递。

    使用可变默认参数的规范方法是使用标记值(通常是None),可以使用is 运算符对其进行测试,以表明用户没有传递任何东西(除非改变默认参数当然在你的功能中是需要的)。例如:

    def foo(bar=None):
        if(bar is None):
           bar=[]
        bar.append('boo')
        print bar
    

    这是文档的link——请密切注意“重要警告”部分。

    【讨论】:

    • 如果我打电话给bar = ValidationResult(messages=foo.messaged)会发生什么?根本问题是获取参数的所有权,而不是默认参数。如果你不改变你的输入,使用[] 作为默认值是非常安全的。
    • @Ben -- 我想不出任何好的情况,您可以将一个空列表作为函数的默认参数,然后不对其执行任何操作。即使你没有在函数中修改它,而是返回它,那么你以后可能会得到非常令人惊讶的行为。如果用户想要传递另一个列表并且对同一列表有多个引用。
    • @Ben -- 我添加了一个子句,说如果你不使用if mutable_arg is None 技巧,如果你真的想改变mutable_arg -- 但我认为这很明显。
    • 看,我的结论是防止改变默认参数并不是一个完整的解决方案,因此if foo is not None: foo = [] 技巧只是掩盖了根本问题。我的大多数使用列表的函数for 列表中的每个项目,而不是 to 列表。如果我确实需要对列表做些什么,我会复制一份(通常在列表理解中)。
    • @Ben -- 为空列表中的每个项目做某事是没有意义的。在这种情况下不应该调用该函数——如果您不知道列表是空的,这是一回事,但在这种情况下,您肯定会这样做,因为您没有向函数传递任何参数。您所描述的情况似乎应该mutable_arg 应该是一个参数,而不是默认参数。如果你想改变参数的副本并返回它,那没关系 - 但它仅限于可能希望他们的参数改变的用户。
    【解决方案4】:

    不,与类属性和实例属性无关。

    问题在于,Python 中的所有赋值都只是简单地绑定对对象的引用,因此任何用作 messages 参数到 __init__ 的值最终都会成为存储为 messages 属性的值;它没有被复制。当该值也在其他地方使用并且可以改变时,这是一个问题,因为随后对 messages 属性引用的对象的更改最终会影响对同一对象的其他引用。

    由于您将messages 参数的默认值设置为__init__,因此每次在不提供值的情况下调用它时,您都会得到填充的相同对象。默认值是默认,不是用于创建新值的方法,因此每次使用默认值时都会得到相同的列表。

    人们将此称为“可变默认参数”问题,但我认为它比这更普遍。如果你也明确传递messages,它很容易咬你。真正的问题是你正在改变一个输入参数(通过将一个对它的引用存储在一个实例属性中,然后你改变它),这绝不是一个好主意,除非改变对象是目的的功能。这通常不适用于构造函数,因此如果您打算对其进行变异,则应该复制该列表。

    【讨论】:

      【解决方案5】:

      这个self.messages 是默认参数的别名。默认参数是由函数构造的,而不是调用者构造的。

      class ValidationResult():
          def __init__(self, passed=True, messages=None, stop=False):
              self.passed = passed
              self.messages = messages if messages is not None else []
              self.stop = stop
      
      >>> foo = ValidationResult()
      >>> bar = ValidationResult() 
      >>> foo.messages.append("Foos message")  
      >>> print foo.messages
      ['Foos message']
      >>> print bar.messages
      []
      

      【讨论】:

      • 更准确地说,默认参数属于类中的函数。
      • 次要,而不是测试messages != None,您的测试可能应该是messages is not None
      • 为 cmets 干杯。进行了两项更改。
      猜你喜欢
      • 2015-07-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-24
      • 2023-01-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多