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'}
>>>
上面的测试代码非常简单:
- 先定义了一个极为简单的
Origin
类,类中没有进行任何操作 - 再用刚定义的
Origin
类实例化一个对象, 命名为o
- 调用实例
o
的__dict__
属性(这是一个特殊的属性),发现: 刚开始__dict__
属性是一个空字典 - 后续对实例
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}
>>>
通过打印的结果,我们可以看到:
- 定义了类的
__slots__
属性后, 默认生成的类的__dict__
与__weakref__
属性没有自动生成。 - 定义了类的
__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'
>>>
这里就可以看到一些不同:
- 定义了一个
Pixel
类, 除了定义了类属性__slots__
, 没有其他操作 - 再用刚定义的
Pixel
类实例化了一个对象, 命名为p
- 调用实例
p
的__dict__
属性,发现实例没有这个属性, 这是第一个重要的区别 - 对实例
p
动态增加了几个属性, 发现: 如果属性名称在类属性__slots__
的tuple中,可以增加成功, 比如新增的’x’, ‘y’ - 对实例
p
动态增加了几个属性, 发现: 如果属性名称不在类属性__slots__
的tuple中,会抛出异常, 比如新增的’color’, 这是第二个重要的区别
结论:
- 使用
__slots__
后,python中对象的属性不能通过object.__dict__
来查看 - 使用
__slots__
后,如果想要新增对象的属性,属性名称必须在指定的__slots__
中, 否则会抛异常
2. 介绍__slots__
的功能
在引言章节中,我们介绍了使用__slots__
后的两个区别,这一章节中:
- 我们查看官方文档, 获得更加全面的介绍。
- 从书籍中获得一些官网中没有介绍的实际使用场景
- 使用
__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]
结论:
- 优化前内存使用: 1713745920 - 6770688 = 1706975232
- 优化后内存使用: 586170368 - 6873088 = 579297280
- 相差比例: 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站的视频:
__slots__
这种特殊效果的实现,是在创建类对象时候完成的,我们需要先看函数 type_new
的实现。
type_new
的实现在Cpython源码的:Objects/typeobject.c
文件中。