python的`__slots__`属性

1. 引言

我们先看一个问题: 对象(通过类实例化后的对象)如何储存自己的属性?

1.1. 默认情况下:对象如何储存自己的属性

为了说明这个问题, 我们先在交互式模式中做一个简单的测试:

>>> class Origin:
...     pass
...
>>> o = Origin()
>>> o.__dict__
{}
>>> o.x = 10
>>> o.y = 20
>>> o.color = 'red'
>>> o.__dict__
{'x': 10, 'y': 20, 'color': 'red'}
>>>

上面的测试代码非常简单:

  1. 先定义了一个极为简单的Origin类,类中没有进行任何操作
  2. 再用刚定义的Origin类实例化一个对象, 命名为o
  3. 调用实例o__dict__ 属性(这是一个特殊的属性),发现: 刚开始__dict__属性是一个空字典
  4. 后续对实例o动态增加了几个属性,再次查看实例o__dict__ 属性,可以看到新增的属性

结论:

默认情况下,python中对象的属性是通过字典的形式进行储存的,具体有哪些属性,可以通过特殊的object.__dict__来查看。

关于object.__dict__的含义,python官方介绍:

A dictionary or other mapping object used to store an object’s (writable) attributes.

官方参考文档:

https://docs.python.org/3/library/stdtypes.html#object.dict

1.2. 定义类的__slots__属性前后,类的属性的变化

在这个例子中我们通过对比,观察是否定义类的__slots__属性,类本身的属性是否有变化以及变化规律。
在交互式模式中再做一个测试:

>>> class Origin:
...     pass
...
>>> class Pixel:
...     __slots__ = ('x', 'y')
...
...
>>> Origin.__dict__
mappingproxy({'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Origin' objects>, '__weakref__': <attribute '__weakref__' of 'Origin' objects>, '__doc__': None})
>>> Pixel.__dict__
mappingproxy({'__module__': '__main__', '__slots__': ('x', 'y'), 'x': <member 'x' of 'Pixel' objects>, 'y': <member 'y' of 'Pixel' objects>, '__doc__': None})
>>>
>>> print(Pixel.__dict__)
{'__module__': '__main__', '__slots__': ('x', 'y'), 'x': <member 'x' of 'Pixel' objects>, 'y': <member 'y' of 'Pixel' objects>, '__doc__': None}
>>> print(Origin.__dict__)
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Origin' objects>, '__weakref__': <attribute '__weakref__' of 'Origin' objects>, '__doc__': None}
>>>

通过打印的结果,我们可以看到:

  1. 定义了类的__slots__属性后, 默认生成的类的__dict____weakref__属性没有自动生成。
  2. 定义了类的__slots__属性后, 类中新增了__slots__属性,并新增了__slots__属性中的成员(member)

1.3. 定义类的__slots__属性后,对象如何储存自己的属性

为了说明这个问题: 在交互式模式中,我们定义类时候,使用__slots__属性, 再做一个测试:

>>> class Pixel:
...     __slots__ = ('x', 'y')
...
>>> p = Pixel()
>>> p.__dict__
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    p.__dict__
AttributeError: 'Pixel' object has no attribute '__dict__'
>>>
>>> p.x = 10
>>> p.y = 20
>>> p.color = 'red'
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    p.color = 'red'
AttributeError: 'Pixel' object has no attribute 'color'
>>>

这里就可以看到一些不同:

  1. 定义了一个Pixel类, 除了定义了类属性__slots__, 没有其他操作
  2. 再用刚定义的Pixel类实例化了一个对象, 命名为p
  3. 调用实例p__dict__ 属性,发现实例没有这个属性, 这是第一个重要的区别
  4. 对实例p动态增加了几个属性, 发现: 如果属性名称在类属性__slots__的tuple中,可以增加成功, 比如新增的’x’, ‘y’
  5. 对实例p动态增加了几个属性, 发现: 如果属性名称不在类属性__slots__的tuple中,会抛出异常, 比如新增的’color’, 这是第二个重要的区别

结论:

  1. 使用__slots__后,python中对象的属性不能通过 object.__dict__来查看
  2. 使用__slots__后,如果想要新增对象的属性,属性名称必须在指定的__slots__中, 否则会抛异常

2. 介绍__slots__ 的功能

在引言章节中,我们介绍了使用__slots__后的两个区别,这一章节中:

  1. 我们查看官方文档, 获得更加全面的介绍。
  2. 从书籍中获得一些官网中没有介绍的实际使用场景
  3. 使用__slots__时候的注意事项

2.1. 官方文档中的功能介绍

python官方文档中关于__slots__的介绍:

__slots__ allow us to explicitly declare data members (like properties) and deny the creation of __dict__ and __weakref__ (unless explicitly declared in __slots__ or available in a parent.)

The space saved over using __dict__ can be significant. Attribute lookup speed can be significantly improved as well.

我个人的翻译:

__slots__允许我们显式声明数据成员(如属性),并拒绝创建__dict____weakref__(除非在__slots__中明确声明或在父节点中可用)。

相较于使用__dict__,使用__slots__ 能大幅度节省的内存, 同时属性查找速度也可以显著提高。

python官方文档中关于object.__slots__的单独说明:

This class variable can be assigned a string, iterable, or sequence of strings with variable names used by instances.
__slots__ reserves space for the declared variables and prevents the automatic creation of __dict__ and __weakref__ for each instance.

我个人的翻译:

这个类变量可以分配给字符串、可迭代或具有实例使用的变量名称的字符串序列。
__slots__为声明的变量保留空间,并防止为每个实例自动创建__dict____weakref__

2.2. 使用__slots__, 以显著的节省内存

关于这点有很多不错的参考文档:

博客:Saving 9 GB of RAM with Python’s slots
书籍:中文版《流程的python, 第一版》的 “9.8 使用__slots__类属性节省空间”
书籍:英文版《Fluent Python, 2nd Edition》的 “Saving Memory with slots

下面是摘自英文版《Fluent Python, 2nd Edition》中的内容:

By default, Python stores the attributes of each instance in a dict named __dict__. A dict has a signficant memory overhead—even with the optimizations mentioned in that section. But if you define a class attribute named __slots__ holding sequence of attribute names, Python uses an alternative storage model for the instance attributes: the attributes named in __slots__ are stored in a hidden array or references that uses less memory than a dict.

下面是摘自中文版《流程的python》中的内容:

默认情况下,Python 在各个实例中名为 __dict__ 的字典里存储实例属性。为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过 __slots__ 类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。

切记: 不要过早的使用__slots__, 只有当你的实例化对象达到数百万个的时候,才有必要使用__slots__进行优化。

2.3. 注意事项

官方文档:

https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots

使用__slots__的好处很多,但是要想更好的利用这些功能,有一些注意事项:

  • 每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属性。
  • 实例只能拥有 __slots__ 中列出的属性, 除非把 __dict__ 加入 __slots__ 中(这样做就失去了节省内存的功效) 。
  • 如果不把 __weakref__ 加入 __slots__,实例就不能作为弱引用的目标

3. 举例说明

3.1. 通过__slots__限制创建非法的属性

其实载第一小节中已经介绍了这种方法,我们再将demo代码简化后做说明。

【示例代码3-1】,实例化后的对象动态创建已经在__slots__中定义的属性:

class A:
    __slots__ = ('f', 'g')

o = A()
o.f = 2

运行这段示例代码,可以看到会正常工作,不会抛异常

【示例代码3-2】,作为对照组,实例化后的对象动态创建没有在__slots__中定义的属性:

class A:
    __slots__ = ('f', 'g')


o = A()
o.b = 2

如果运行这段代码,会报错, 错误信息类似:

Traceback (most recent call last):
  File "/home/xd/project/learn_python/test/test.py", line 6, in <module>
    o.b = 2
AttributeError: 'A' object has no attribute 'b'

3.2. 通过__slots__ 节省内存

为了验证节省内存,而且为了更加的可信,我们使用了《Fluent Python》在github上示例代码:

https://github.com/fluentpython/example-code/tree/master/09-pythonic-obj

第一轮测试:在我自己的MacBookPro上的测试:

(venv)  ~/Code/Read/FluentPython/example-code/09-pythonic-obj/ [master] time python3 mem_test.py vector2d_v3.py
Selected Vector2d type: vector2d_v3.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage:      6,770,688
  Final RAM usage:  1,713,745,920
python3 mem_test.py vector2d_v3.py  13.22s user 1.05s system 95% cpu 14.976 total
(venv)  ~/Code/Read/FluentPython/example-code/09-pythonic-obj/ [master] time python3 mem_test.py vector2d_v3_slots.py
Selected Vector2d type: vector2d_v3_slots.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage:      6,873,088
  Final RAM usage:    586,170,368
python3 mem_test.py vector2d_v3_slots.py  8.97s user 0.37s system 99% cpu 9.376 total
(venv)  ~/Code/Read/FluentPython/example-code/09-pythonic-obj/ [master]

结论:

  1. 优化前内存使用: 1713745920 - 6770688 = 1706975232
  2. 优化后内存使用: 586170368 - 6873088 = 579297280
  3. 相差比例: 1706975232 / 579297280 = 2.946

可以明显看到使用slots优化后的内存消耗要小的多。

第二轮测试:在我的阿里云上的测试:

(venv) xd@wxd:~/Code/Read/FluentPython/example-code/09-pythonic-obj$ time python3 mem_test.py vector2d_v3
Selected Vector2d type: vector2d_v3.Vector2d
Creating 10,000,000 Vector2d instances
Killed

real	1m23.563s
user	0m6.873s
sys	0m0.956s
(venv) xd@wxd:~/Code/Read/FluentPython/example-code/09-pythonic-obj$ time python3 mem_test.py vector2d_v3_slots.py
Selected Vector2d type: vector2d_v3_slots.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage:          8,828
  Final RAM usage:        563,264

real	0m7.788s
user	0m7.505s
sys	0m0.240s
(venv) xd@wxd:~/Code/Read/FluentPython/example-code/09-pythonic-obj$

没有使用slots优化前,程序直接崩溃。。。应该是OOM了。

4. 源码实现

关于__slots__ 部分的源码实现,可以参考B站的视频:

【python】__slots__是什么东西?什么?它还能提升性能?它是如何做到的!?

__slots__ 这种特殊效果的实现,是在创建类对象时候完成的,我们需要先看函数 type_new的实现。

type_new 的实现在Cpython源码的:Objects/typeobject.c文件中。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值