类属性和实例属性有什么区别?
- 2024-11-25 08:49:00
- admin 原创
- 147
问题描述:
以下两者之间是否存在有意义的区别:
class A(object):
foo = 5 # some default value
对阵
class B(object):
def __init__(self, foo=5):
self.foo = foo
如果您要创建大量实例,这两种样式在性能或空间要求方面是否存在差异?当您阅读代码时,您是否认为这两种样式的含义存在显著差异?
解决方案 1:
存在显著的语义差异(超出性能考虑):
当属性在实例上定义时(我们通常这样做),可以引用多个对象。每个对象都会获得该属性的完全独立版本。
当在类上定义属性时,只有一个引用的底层对象,因此如果对该类的不同实例的操作都尝试设置/(附加/扩展/插入/等)该属性,则:
如果属性是内置类型(如 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
[]
解决方案 2:
不同之处在于类上的属性由所有实例共享。实例上的属性是该实例独有的。
如果来自 C++,类上的属性更像是静态成员变量。
解决方案 3:
这是一篇非常好的文章,总结如下。
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
并以视觉形式呈现
类属性赋值
如果通过访问类来设置类属性,它将覆盖所有实例的值
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 属性?
存储常量。由于类属性可以作为类本身的属性来访问,因此通常最好使用它们来存储类范围的、类特定的常量
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
解决方案 4:
由于这里的评论和另外两个标记为重复的问题中的人似乎都以同样的方式对此感到困惑,所以我认为值得在Alex Coventry 的答案之上添加一个额外的答案。
Alex 分配可变类型的值(例如列表)这一事实与事物是否共享无关。我们可以通过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()
不能。)
那么,为什么a.foo.append(5)
在 Alex 的例子中会影响b.foo
,而a.foo = 5
在我的例子中却不会呢?好吧,试试Alex 的例子,你会发现它也a.foo = 5
不会影响。b.foo
a.foo = 5
只是将其变成a.foo
的名称5
。这不会影响b.foo
,或过去引用的旧值的任何其他名称a.foo
。 我们正在创建一个隐藏类属性的实例属性,这有点棘手,* 但是一旦您得到它,这里就不会发生任何复杂的事情。
希望现在你能清楚地了解 Alex 使用列表的原因:你可以改变列表这一事实意味着更容易显示两个变量命名同一个列表,也意味着在实际代码中更重要的是要知道你是否有两个列表或同一个列表有两个名称。
对于使用 C++ 等语言的人来说,令人困惑的是,在 Python 中,值不存储在变量中。值本身存在于值域中,变量只是值的名称,赋值只是为值创建一个新名称。如果有帮助,请将每个 Python 变量视为 而
shared_ptr<T>
不是T
。
** 有些人利用这一点,将类属性用作实例属性的“默认值”,而实例属性可以设置也可以不设置。这在某些情况下很有用,但也可能造成混淆,因此要小心使用。
解决方案 5:
还有一种情况。
类和实例属性是Descriptor。
# -*- 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 定义中找到了一篇很棒的文章。
解释
如果在组成对象 MRO 的类的字典中找到该属性,则检查所查找的属性是否指向数据描述符(它只不过是实现
__get__
和__set__
方法的类)。如果是,则通过调用__get__
数据描述符的方法解析属性查找(第 28-33 行)。