【问题标题】:Python unintuitive member variable behaviourPython 不直观的成员变量行为
【发布时间】:2011-09-30 06:14:06
【问题描述】:

这个脚本:

class testa():
    a = []

class testb():
    def __init__(self):
        self.a = []

ta1 = testa(); ta1.a.append(1); ta2 = testa(); ta2.a.append(2)
tb1 = testb(); tb1.a.append(1); tb2 = testb(); tb2.a.append(2)

print ta1.a, ta2.a, tb1.a, tb2.a

产生这个输出:

[1, 2] [1, 2] [1] [2]

但我期待

[1] [2] [1] [2]

为什么我错了? testa 和 testb 的定义似乎和我是等价的,那为什么行为会发生如此巨大的变化?!

编辑: 这似乎不直观,因为它与其他类型(如 int 和 str)的行为方式不同。由于某些原因,列表在 init 中未初始化时被创建为类变量,但无论如何 int 和 strs 都被创建为对象变量。

【问题讨论】:

  • 字符串、整数和列表之间的区别在于列表是可变的,而字符串和整数是不可变的。您不能更改字符串的值。您可以绑定到不同的字符串。
  • 字符串和整数也被创建为类变量。只是你不能在 int 上调用 append,你必须用赋值重新绑定它。这会创建一个 new int 对象作为实例变量(除非您在类上重新绑定它),并让类变量隐藏(对于该特定实例)但保持不变。
  • 似乎我们最近已经得到了这个问题的几十个变体......

标签: python class variables


【解决方案1】:

testa 中,变量a 是一个类变量,在所有实例之间共享。 ta1.ata2.a 指的是同一个列表。

testb 中,变量a 是一个对象变量。每个实例都有自己的价值。

更多详情请见Class and Object Variables

【讨论】:

    【解决方案2】:

    一个是类变量,另一个是实例变量。
    类变量在类的所有成员之间共享,实例变量对每个实例都是唯一的。
    http://docs.python.org/tutorial/classes.html

    【讨论】:

      【解决方案3】:

      记住,Python 中的 class 语句比 C++ 等语言更接近于任何其他块语句。

      在 Python 中,class 语句包含一堆要执行的语句,就像 defifwhile。不同之处在于解释器对块的处理方式。对于像ifwhile这样的流控制块语句,解释器按照流控制语句的含义执行块。在def 中,解释器保存块并在调用函数对象时执行它。

      class 块的情况下,Python 立即在新范围内执行该块,然后在执行完成后使用该范围内剩余的内容作为类的内容。

      所以对于这个:

      class testa():
          a = []
      

      Python 执行块 a = []。然后在最后,范围包含绑定到一个空列表对象的a。这就是您的中的内容。不是类的任何特定实例,而是类本身。

      它从object 继承了一个什么都不做的构造函数,所以当你用ta1 = testa() 实例化类时,你会得到一个空实例。然后当你请求ta1.a 时,Python 找不到名为a 的实例变量(因为ta1 根本没有实例变量),所以它在testa 类中查找a。这当然会找到;但它会为testa 的每个实例找到相同的实例,因此您会得到观察到的行为。

      另一方面:

      class testb():
          def __init__(self):
              self.a = []
      

      完全不同。在这里,一旦执行了类块,类范围的内容又是一个名称,但这次是__init__ 绑定到一个函数。这成为课程的内容。

      现在,当您使用 testb() 实例化 this 类时,Python 会在该类中找到 __init__ 并使用新实例调用该函数。该函数的执行会在新实例中创建一个a instance 变量。所以testb() 的每个实例都有自己的变量,并且您可以观察到行为。

      重要信息:Python 中的 class只是该类的实例中包含的一组事物的声明,这与 C++ 和 Java 等传统的面向对象语言不同.它是实际执行以定义类的内容的实际代码。这真的很方便:您可以使用 if 语句、函数执行的结果,以及您将在任何其他上下文中使用的任何内容,在您的类主体中决定在您的类中定义什么。

      (注意:为了简单起见,我之前撒了谎。即使testa 的实例也会有一些实例变量,因为默认情况下会为所有实例自动创建一些变量,但你没有在日常 Python 中看到它们)。

      【讨论】:

      • 值得强调的是,类 Python 中的对象。反射是内置的,不需要库。
      【解决方案4】:

      testa 和 testb 的定义似乎和我是等价的,那为什么行为会发生如此巨大的变化呢?!

      Python 的行为是完全合乎逻辑的 - 比 Java、C++ 或 C# 更合乎逻辑,因为 您在 class 范围内声明的所有内容都属于该类。

      在 Python 中,类在逻辑上是对象的模板,但在物理上并不是真正的模板。最接近指定“实例恰好具有这组成员”的方法是从object 继承,在类'__slots__ 中列出它们并在__init__ 中初始化它们。但是,Python 不会强制您设置 __init__ 中的所有值,您未设置的值不会获得任何默认值(尝试访问它们会导致 AttributeError),您甚至可以显式删除稍后实例的属性(通过del)。 __init__ 所做的所有事情都是在每个新实例上运行(这是为属性设置初始值的一种方便且惯用的方法)。 __slots__ 所做的只是防止添加其他属性。

      这似乎不直观,因为它与其他类型(如 int 和 str)的行为方式不同。由于某些原因,列表在 init 中未初始化时被创建为类变量,但 int 和 strs 无论如何都被创建为对象变量。

      事实并非如此。

      如果你把intstr 放在那里,它仍然是类的成员。如果您可以以某种方式修改intstr,那么这些更改将反映在每个对象中,就像它们在列表中一样,因为在对象中查找属性会在类中找到它。 (粗略地说,属性访问首先检查对象,然后检查类。)

      但是,您实际上不能这样做。您认为等效的测试根本不等效,因为您使用intstr 的测试会导致对对象属性的赋值,从而隐藏了类属性。 ta1.a.append 调用列表的一个方法,这会导致列表的内部结构发生变化。这对于 Python 的字符串和整数是不可能的。即使你写了ta1.n += 1 之类的东西,它也会转换为ta.n = ta.n + 1,在其中查找属性(并在类中找到),计算总和然后分配回ta.n(分配将始终更新对象,除非类'__slots__ 明确禁止,否则会引发异常)。

      您可以通过直接从类中访问它来简单地证明该值属于类,例如testa.a.

      【讨论】:

        猜你喜欢
        • 2020-09-23
        • 2017-02-17
        • 1970-01-01
        • 1970-01-01
        • 2012-10-05
        • 2019-08-10
        • 1970-01-01
        • 1970-01-01
        • 2018-08-06
        相关资源
        最近更新 更多