Python __slots__ and __dict__

Python的__slots__特性用于减少内存开销,避免动态添加属性时使用__dict__。当定义__slots__,对象不再有__dict__和__weakref__,无法动态赋属性。可通过在__slots__中加入__dict__实现动态赋值,但会导致__slots__失效。类变量在实例中被转换为只读描述器,理解这一机制需要了解Python描述器。__slots__与__dict__的内存比较和使用技巧值得深入研究。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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?

### Python `__slots__` 的用法和作用 #### 定义与基本概念 在Python类定义中,可以通过设置特殊属性`__slots__`来限定实例可以拥有的属性名称列表。这不仅有助于减少内存占用,还能提高属性访问速度。 当声明了一个包含`__slots__`变量的类之后,该类创建的对象将无法动态添加新的成员变量[^1]。 ```python class MyClassWithSlots: __slots__ = ['name', 'identifier'] def __init__(self, name, identifier): self.name = name self.identifier = identifier ``` 上述代码展示了如何通过指定`__slots__`仅允许对象拥有特定的名字(name)和标识符(identifier),任何尝试给此类实例增加其他未列于`__slots__`中的属性都会引发异常。 对于不使用`__slots__`的情况: ```python class RegularClassWithoutSlots: def __init__(self, value): self.value = value instance_without_slots = RegularClassWithoutSlots(42) instance_without_slots.new_attribute = "This works fine" print(instance_without_slots.new_attribute) # 输出: This works fine ``` 而如果试图向具有`__slots__`约束的类实例分配额外属性,则会抛出错误: ```python my_instance_with_slots = MyClassWithSlots('example_name', 123456) try: my_instance_with_slots.unexpected_attr = True except AttributeError as e: print(e) # 输出类似于:'MyClassWithSlots' object has no attribute 'unexpected_attr' ``` #### 使用场景及其优势 - **节省空间**: 对于大量小型类实例的应用程序来说非常重要;因为每个实例不再需要字典(dict)存储其状态,而是直接利用固定大小的数据结构。 - **性能提升**: 访问受控字段的速度更快,由于这些字段的位置是预先确定好的,在某些情况下能带来显著效率增益。 - **防止误操作**: 可以有效阻止开发者无意间修改不该被改变的状态信息,增强了代码的安全性和可维护性。 需要注意的是,一旦启用了`__slots__`特性,默认情况下子类不会继承父类所设定的槽位集合。因此,若希望保持此功能需显式重申或扩展之。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值