Python对象中一般会有一个__dict__属性,负责存储对象的属性,当为对象动态地赋予属性时,也是在__dict__中添加该属性:
In [44]: class PythonObject(object):
...: pass
...:
In [45]: o = PythonObject()
In [46]: o.__dict__
Out[46]: {}
In [47]: o.new_attr = 'new attr'
In [48]: o.__dict__
Out[48]: {'new_attr': 'new attr'}
__dict__属性是通过Python dict来实现的,原理上即hash table,因此默认会保留多余的空间(hash table装载因子超过2/3的时候需要扩容)。如果一个Python对象不需要动态赋值属性,并且该Python对象所属的类需要很多实例化对象的时候,那么__dict__造成的内存开销就会比较大,此时可以考虑使用__slots__属性来减小内存的开销。
可以在__slots__属性中定义该Python对象需要的属性,此时如果为Python对象动态赋予未在__slots__定义的属性,就会raise AttributeError。
In [49]: class SlotClass(object):
...: __slots__ = ('attr1', 'attr2')
...:
In [50]: s = SlotClass()
In [51]: s.attr1 = 'attr1'
In [52]: s.attr1
Out[52]: 'attr1'
In [53]: s.attr3 = 'can not assign'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-53-901ed201a4ae> in <module>()
----> 1 s.attr3 = 'can not assign'
AttributeError: 'SlotClass' object has no attribute 'attr3'
当定义了__slots__属性之后,Python对象将不再拥有__dict__属性和__weakref__属性,因此无法动态赋予对象属性,但是在Python2的官方文档中提供了在定义了__slots__属性的情况下还能动态赋予属性的方法,就是将__dict__属性定义在__slots__中:
In [1]: class DynamicSlots(object):
...: __slots__ = ('__dict__')
...:
In [2]: d = DynamicSlots()
In [3]: d.attr1 = "attr1"
In [4]: d.attr1
Out[4]: 'attr1'
__slots__属性是通过Python的描述器机制实现的,因此如果在已经定义了__slots__属性的情况下,重新定义Python类中相同命名的类变量,会覆盖描述器导致slots属性失效。
In [5]: class Slots(object):
...: __slots__=('descriptor')
...:
In [6]: s1 = Slots()
In [7]: s1.descriptor = 's1 descriptor'
In [8]: Slots.descriptor
Out[8]: <member 'descriptor' of 'Slots' objects>
In [9]: type(s1).descriptor.__get__(s1, type(s1))
Out[9]: 's1 descriptor'
## 重复定义同名的类变量
In [10]: class SlotsWithClassAttr(object):
...: __slots__=('descriptor')
...: descriptor='class variable descriptor'
...:
In [11]: s2 = SlotsWithClassAttr()
In [12]: s2.descriptor
Out[12]: 'class variable descriptor'
In [13]: s2.descriptor = 'not work'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-13-643045f8e8f2> in <module>()
----> 1 s2.descriptor = 'not work'
AttributeError: 'SlotsWithClassAttr' object attribute 'descriptor' is read-only
In [14]: type(s2).descriptor.__get__(s2, type(s2))
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-14-3cd24adb4d44> in <module>()
----> 1 type(s2).descriptor.__get__(s2, type(s2))
AttributeError: 'str' object has no attribute '__get__'
当在类里面定义了相同命名的的变量时,并试图从实例中修改的时候,可以看到报出了一个Read Only Attribute的Error。为什么无法改变类变量的值呢?在StackOverflow上有关于这个问题的讨论:Python, __slots__, inheritance, and class variables ==> attribute is read-only bug,其中一个回答认为覆盖的类变量被转换成了一个read-only的描述器,但是我认为真正的原因却是这一个回答下面的评论:因为类变量没有__set__方法以及实例没有__dict__属性,所以该属性才会变成一个read-only attribute。
可以做一个小实验来实践一下:
In [6]: class A(object):
...: __slots__ = ('B', )
...: C = 1
...:
In [7]: a = A()
In [8]: a.C
Out[8]: 1
In [9]: a.C = 2
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-9-cd2c6aafa1c5> in <module>()
----> 1 a.C = 2
AttributeError: 'A' object attribute 'C' is read-only
要真正的理解这一个问题,还需要清楚描述器的机制,建议去看一下这一个知乎问答:如何理解 Python 的 Descriptor?
在Google关于slots属性的用法时,还学习了一些比较有趣的知识点: 不能直接给对象设置属性?
关于slots属性与dict属性的内存开销比较可以参考:__slots__魔法。
回到__dict__属性,其实实例上的__dict__属性也是作为一个描述器来实现的,不过它有点特殊,instance.__dict__ == type(instance).__dict__ [‘__dict__’].__get__(instance, type(instance))。
看看下面的例子就明白了:
In [27]: class PythonObject(object):
...: class_attr = 'class_attr'
...: def __init__(self):
...: self.instance_attr = 'instance attr'
...:
...:
In [28]: p = PythonObject()
In [29]: p.__dict__
Out[29]: {'instance_attr': 'instance attr'}
In [30]: type(p).__dict__['__dict__'].__get__(p, type(p))
Out[30]: {'instance_attr': 'instance attr'}
In [31]: type(PythonObject).__dict__['__dict__'].__get__(PythonObject, type(PythonObject))
Out[31]:
dict_proxy({'__dict__': <attribute '__dict__' of 'PythonObject' objects>,
'__doc__': None,
'__init__': <function __main__.__init__>,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'PythonObject' objects>,
'class_attr': 'class_attr'})
这个例子表明,实例的属性通常都是通过描述器机制存储在类中,既然类也是一个对象,那么类属性也会存储在元类(type)中,而PythonObject.__dict__只是一个dict proxy对象。
参考:What is the __dict__.__dict__ attribute of a Python class?