目录
一、原理
1.1 说明
Python 作为一门十分灵活的 动态语言,多处设计为顾及灵活而牺牲效率/性能。例如,Python 作为动态语言,类创建好后仍可 动态创建类成员 (如属性/数据成员)。而在静态语言中只能调用类中已有的属性,难以甚至无法添加新属性。
>>> class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
>>> p = Point()
>>> p.z
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
p.z
AttributeError: 'Point' object has no attribute 'z'
>>> p.z = 1
>>> p.z
1
上例构造了一个 Point 类,自带属性 x 和 y。实例化 Point 类对象后,实例 p 虽然本无属性 z,但通过 p.z = 1 的赋值操作却可直接将属性 z 添加至实例 p 中。
这种机制虽然很看起来灵活,但实则存在隐患。因为 用户具有随意添加属性的权限时,可能导致未知的问题,特别是面对复杂系统时。故 有时出于严谨与安全考量,并不希望用户能够随意动态修改。这时,__slots__ 方法应运而生,例如:
>>> class Point_new:
__slots__ = ['x', 'y'] # 限制使用
def __init__(self, x=0, y=0):
self.x = x
self.y = y
>>> q = Point_new()
>>> q.z
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
q.z
AttributeError: 'Point_new' object has no attribute 'z'
>>> q.z = 1
Traceback (most recent call last):
File "<pyshell#28>", line 1, in <module>
q.z = 1
AttributeError: 'Point_new' object has no attribute 'z'
由上例可知,类定义时在构造函数前经过 __slow__ 声明后,实例只能使用关键字 __slots__ 中定义或声明的属性 (数据成员) x 和 y。而原本就不存在于类 Point_new 的属性 z,实例 q 无法再通过赋值的方式动态创建。换言之,对实例属性而言,类属性是只读的,不可以通过实例对实例属性进行增删改 (而只能通过类来增删改类属性,并随之影响实例属性,详见第二节)。
从而, __slots__ 的一个用途是:限制用户随意动态修改成员。
此外,__slots__ 的另一个功能是:减少内存消耗,提升属性访问速度。
在 Python 底层实现中,默认使用一个个的 命名空间字典 (namespace dictionary) __dict__ 来保存类的实例属性,从而 允许在运行时动态创建任意新成员 (如第一个例子所示)。可通过实例调用 __dict__ 属性观察 (承接上例):
# 未使用 __slots__
>>> p.__dict__
{'x': 0, 'y': 0, 'z': 1} # 实例属性字典
# 已使用 __slots__
>>> q.__dict__
Traceback (most recent call last):
File "<pyshell#32>", line 1, in <module>
q.__dict__
AttributeError: 'Point_new' object has no attribute '__dict__'
可见,使用 __slots__ 前,类实例调用 __dict__ 属性可查看实例属性字典;然而,使用 __slots__ 后就不可以了,因为 Traceback 显示类对象没有 __dict__ 属性。
使用 __slots__ 前,Python 无法在创建实例时直接分配⼀个固定量的内存来保存所有的实例属性。因此,对于许多“小”类对象而言,使用 dict 维护实例将额外存储许多数据,占用大量不必要的内存,特别是需要创建成千上万实例等情况下。
使用 __slots__ 后,Python 内部的 __new__ 方法将不再创建一个 dict 来保存实例属性,而是以一个固定大小的数组取而代之,从而节省空间。
事实上,Python 内置的 dict 本质是一个哈希表 (hasht