类属性和实例属性有什么区别?

158

这两者之间是否存在实质性的区别:

class A(object):
    foo = 5   # some default value

对比。

class B(object):
    def __init__(self, foo=5):
        self.foo = foo

如果你正在创建大量实例,那么这两种样式在性能或空间要求方面有什么区别吗?当你阅读代码时,你是否认为这两种样式的含义有显著不同?


1
我刚意识到有一个类似的问题在这里被问答了: https://dev59.com/k3VC5IYBdhLWcg3wtzgQ我应该删除这个问题吗? - Dan Homerick
3
这是你的问题,随意删除它。既然这是你的问题,为什么要问其他人的意见呢? - S.Lott
5个回答

168

在定义类属性与实例属性时,语义上存在显著差异(除了性能方面的考虑):

  • 当该属性定义在实例上(通常情况下),可以引用多个对象。 每个对象都有自己的该属性副本。
  • 当该属性定义在类上时,只存在一个被引用的基础对象,因此,如果对该类的不同实例进行操作,并尝试设置/(追加/扩展/插入等)该属性,则会发生以下情况:
    • 如果该属性是内置类型(如int、float、boolean、string),则对一个对象的操作将覆盖(破坏)该值
    • 如果属性是可变类型(如列表或字典),则可能会发生意外泄漏。

例如:

>>> class A: foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo
[5]
>>> class A:
...  def __init__(self): self.foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo    
[]

4
只有可变类型是共享的。比如对于intstr这样的类型,它们仍然与每个实例相关联,而不是与类相关联。 - Babu
14
@Babu:不,intstr也是以完全相同的方式共享的。您可以使用isid轻松检查。或者只需查看每个实例的__dict__和类的__dict__即可。通常,不可变类型是否共享并不太重要。 - abarnert
21
请注意,如果您执行 a.foo = 5,那么在两种情况下都将看到 b.foo 返回 []。 这是因为在第一种情况下,您正在用同名的新实例属性覆盖类属性 a.foo - Konstantin Schubert
“泄漏”实际上可能是设计中故意为之的。 - NeilG

45

区别在于类上的属性被所有实例共享,而实例上的属性则是唯一的。

如果从C++转来,类上的属性更像静态成员变量。


1
难道只有可变类型才是共享的吗? 接受的答案显示了一个列表,这很有效,但如果它是int,则似乎与实例属性相同:>>> class A(object): foo = 5 >>> a, b = A(), A() >>> a.foo = 10 >>> b.foo 5 - Rafe
8
@Rafe: 不是的,所有类型都是共享的。你感到困惑的原因是a.foo.append(5)所做的是改变a.foo引用的值,而a.foo = 5则是将a.foo变成一个新名称,指向值5。因此,你最终得到了一个实例属性,它隐藏了类属性。在Alex的版本中尝试使用相同的a.foo = 5,你会发现b.foo没有改变。 - abarnert

43
这是一篇非常好的文章,以下是它的摘要。
class Bar(object):
    ## No need for dot syntax
    class_var = 1

    def __init__(self, i_var):
        self.i_var = i_var

## Need dot syntax as we've left scope of class namespace
Bar.class_var
## 1
foo = Bar(2)

## Finds i_var in foo's instance namespace
foo.i_var
## 2

## Doesn't find class_var in instance namespace…
## So look's in class namespace (Bar.__dict__)
foo.class_var
## 1

以视觉形式呈现

enter image description here

类属性赋值

  • 如果通过访问类来设置类属性,它将覆盖所有实例的值。

      foo = Bar(2)
      foo.class_var
      ## 1
      Bar.class_var = 2
      foo.class_var
      ## 2
    
  • 如果通过访问实例来设置类变量,它将仅覆盖该实例的值。这实质上覆盖了类变量,并将其转换为一个只对该实例有效的实例变量。

      foo = Bar(2)
      foo.class_var
      ## 1
      foo.class_var = 2
      foo.class_var
      ## 2
      Bar.class_var
      ## 1
    

何时使用类属性?

  • 存储常量。由于类属性可以作为类本身的属性访问,因此通常可以将其用于存储整个类范围内、类特定的常量。

      class Circle(object):
           pi = 3.14159
    
           def __init__(self, radius):
                self.radius = radius   
          def area(self):
               return Circle.pi * self.radius * self.radius
    
      Circle.pi
      ## 3.14159
      c = Circle(10)
      c.pi
      ## 3.14159
      c.area()
      ## 314.159
    
  • 定义默认值。作为一个简单的例子,我们可以创建一个有界列表(即只能容纳一定数量的元素或更少),并选择将默认容量设置为10个项目。

      class MyClass(object):
          limit = 10
    
          def __init__(self):
              self.data = []
          def item(self, i):
              return self.data[i]
    
          def add(self, e):
              if len(self.data) >= self.limit:
                  raise Exception("Too many elements")
              self.data.append(e)
    
       MyClass.limit
       ## 10
    

你链接的那篇文章太棒了! - Vipul

22

由于在这里的评论和其他两个被标记为重复的问题中,所有人似乎都以同样的方式感到困惑,因此我认为值得在Alex Coventry's之上添加一个额外的答案。

像列表一样的可变类型是赋值一个值与是否共享无关。我们可以通过id函数或is操作符来证明这一点:

>>> class A: foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
True
>>> class A:
...     def __init__(self): self.foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
False

(如果你想知道为什么我使用了object()而不是5,那是为了避免遇到另外两个问题,这里不想深入讨论;由于两个完全分开创建的5实例可能会成为数字5的同一实例。但是完全分开创建的object()则不会。)


那么,为什么Alex的示例中a.foo.append(5)会影响b.foo,而我的示例中a.foo = 5却不会呢?好吧,在Alex的示例中尝试a.foo = 5,注意到它也没有影响b.foo

a.foo = 5只是将a.foo变成5的名称。这不会影响b.foo或任何其他名称,用于引用旧值的a.foo。* 这有点棘手,因为我们正在创建一个隐藏类属性的实例属性,**但是一旦你理解了这一点,这里就没有复杂的事情发生。


希望现在很明显为什么Alex使用了列表:可以改变列表意味着更容易显示两个变量命名了同一个列表,并且在实际代码中更重要的是知道是否有两个列表或两个名称指向同一个列表。


* 对于像C ++这样的语言来说,人们的困惑在于在Python中,值不存储在变量中。值独立存在于自己的value-land中,变量只是值的名称,赋值只是为值创建新的名称。如果有帮助,请将每个Python变量视为shared_ptr<T>而不是T

** 一些人利用这一点,使用类属性作为实例属性的“默认值”,实例可能设置也可能不设置。这在某些情况下可能很有用,但也可能会令人困惑,因此请小心使用。


0

还有一种情况。

类和实例属性是描述符

# -*- encoding: utf-8 -*-


class RevealAccess(object):
    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        return self.val


class Base(object):
    attr_1 = RevealAccess(10, 'var "x"')

    def __init__(self):
        self.attr_2 = RevealAccess(10, 'var "x"')


def main():
    b = Base()
    print("Access to class attribute, return: ", Base.attr_1)
    print("Access to instance attribute, return: ", b.attr_2)

if __name__ == '__main__':
    main()

以上将输出:

('Access to class attribute, return: ', 10)
('Access to instance attribute, return: ', <__main__.RevealAccess object at 0x10184eb50>)

通过类或实例访问相同类型的实例返回不同的结果!

我在c.PyObject_GenericGetAttr definition中发现了一个很棒的post

解释

如果在对象MRO中组成的类的字典中找到属性,则检查要查找的属性是否指向数据描述符(即实现__get____set__方法的类)。如果是,则通过调用数据描述符的__get__方法(第28-33行)来解析属性查找。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接