slot
以后都在 github 更新,请戳 python __slots__
目录
相关位置文件
- cpython/Objects/typeobject.c
- cpython/Objects/clinic/typeobject.c.h
- cpython/Objects/object.c
- cpython/Include/cpython/object.h
- cpython/Objects/descrobject.c
- cpython/Include/descrobject.h
- cpython/Python/structmember.c
- cpython/Include/structmember.h
slot
阅读之前需要了解的知识
示例
class A(object):
__slots__ = ["wing", "leg"]
x = 3
访问实例 a
的 wing
和 x
这两个属性的时候有什么不同 ?
访问类型 A
的 wing
和 x
这两个属性的时候有什么不同 ?
实例属性访问
访问实例属性wing
设置了值之前
>>> a = A()
>>> a.wing
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: wing
根据 descr 中描述的属性访问过程
我们可以画出访问 a.wing
的时候的粗略的流程
[外链图片转存失败(img-qIhkRLsp-1563531170994)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/instance_desc.png)]
根据 descriptor protocol 第一步找到的对象未 descr
, 类型为 member_descriptor
, 如果你执行
repr(descr)
descr: <member 'wing' of 'A' objects>
descr
有一个元素名为 d_member
, 这个元素存储了属性的名称, 属性类型和属性存储对应的位置偏移等信息
通过 d_member
中的信息, 你可以非常快速的定位到对应的属性的位置
在当前的示例中, 如果实例 a
的开始地址为 0x00
, 那么加上这个位置偏移之后的地址为 0x18
, 把 0x18
强制转换为一个 python 对象的类型, 就是你需要的属性
/* Include/structmember.h */
#define T_OBJECT_EX 16 /* 和 T_OBJECT 作用相同, 但是当值为空指针时会抛出 AttributeError, 而不是返回 None */
/* Python/structmember.c */
addr += l->offset;
switch (l->type) {
/* ... */
case T_OBJECT_EX:
v = *(PyObject **)addr;
/* 因为示例中的实例 a 的 wing 属性从来没有设置过其他值, 会进入到抛出 AttributeError 的语句中 */
if (v == NULL)
PyErr_SetString(PyExc_AttributeError, l->name);
Py_XINCREF(v);
break;
/* ... */
}
[外链图片转存失败(img-BKMZ4iew-1563531170995)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/offset.png)]
设置了值之后
>>> a = A()
>>> a.wing = "wingA"
>>> a.wing
'wingA'
过程和上面的过程是一样的, 但是因为实例 a
的 wing 属性已经设置过了一个值, AttributeError
不会被抛出
访问实例属性x
>>> a.x
>>> 3
descr
的类型为 int
, 它并不是一个 data descriptor(没有定义 __get__
或者 __set__
方法), 所以这个 descr
对象会被直接返回
[外链图片转存失败(img-g36XOJPh-1563531170998)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/instance_normal.png)]
如果 A
定义了 __slots__
, 你就不能在在实例 a
中定义任何其他的属性, 我们后面会看一下为什么会这样
>>> a.not_exist = 100
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: 'A' object has no attribute 'not_exist'
类属性访问
访问类属性wing
>>> A.wing
<member 'wing' of 'A' objects>
>>> type(A.wing)
<class 'member_descriptor'>
访问 A.wing
和访问 a.wing
的过程大致相同
[外链图片转存失败(img-kbPS0MPq-1563531170998)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/type_desc.png)]
访问类属性x
>>> A.x
3
访问 A.x
和访问 a.x
的过程大致相同的
[外链图片转存失败(img-XzuDumMS-1563531170999)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/type_normal.png)]
不同
有slots
在创建class A
时属性是如何初始化的 ?
我们在 type->class 的创建 这篇文章里面已经学习了类型的创建过程
type
对象在 C 语言的定义中是一个比较多字段的结构体, 接下来的图片示例只会展示当前文章主题相关的字段
__slots__
中定义的属性名称在类型A的创建过程中会被排序, 并转换为一个元组对象, 之后存储在类型A的 ht_slots
字段中
当前定义的 __slots__
中的两个属性会在新创建的类型A的尾部中预先分配好位置, 并以 PyMemberDef
指针的形式按照 ht_slots
中的顺序存储在其中
对于属性 x
并无特殊处理, 保存在 tp_dict
字段指向的字典中
并且 tp_dict
字段指向的字典中没有 "__dict__"
这个 key (只要定义了 __slots__
的类型都不会有)
[外链图片转存失败(img-l9EuqxaN-1563531171000)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/type_create.png)]
在创建instance a
时属性是如何初始化的 ?
__slots__
中需要存储的属性是在实例创建过程中预先分配的
[外链图片转存失败(img-uAfJ9WhF-1563531171001)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/instance_create.png)]
MRO中的属性搜索过程 ?
遍历 MRO 中的每一个类型, 如果这个被搜索的名称在这个类型的 tp_dict
中, 返回 tp_dict[name]
/* cpython/Objects/typeobject.c */
/* for the instance a, if we access a.wing
type: <class '__main__.A'>
mro: (<class '__main__.A'>, <class 'object'>)
name: 'wing'
*/
mro = type->tp_mro;
n = PyTuple_GET_SIZE(mro);
for (i = 0; i < n; i++) {
base = PyTuple_GET_ITEM(mro, i);
dict = ((PyTypeObject *)base)->tp_dict;
// in python representation: res = dict[name]
res = _PyDict_GetItem_KnownHash(dict, name, hash);
if (res != NULL)
break;
if (PyErr_Occurred()) {
*error = -1;
goto done;
}
}
比如我们尝试获取属性 wing
时
>>> type(A.wing)
<class 'member_descriptor'>
>>> type(a).__mro__
(<class '__main__.A'>, <class 'object'>)
>>> print(a.wing)
wingA
下面的伪代码翻译了 C 语言中搜索过程
res = None
for each_type in type(a).__mro__
if "wing" each_type.__dict__:
res = each_type.__dict__["wing"]
break
# 接下来是另一篇文章提到的属性访问过程
...
# 这是属性访问过程的一个情况, 也是访问当前属性时会发生的情况
if res is a data_descriptor:
# res 在这里是 A.wing, 它的类型是 member_descriptor
# 它存储了这个属性的位置偏移等信息, 实例可以根据这个上面的信息快速的获取到需要的对象
# member_descriptor.__get__ 会找到 a + offset 的地址, 并把这个地址强制转换为 PyObject *, 并返回给调用着
return res.__get__(a, type(a))
...
[外链图片转存失败(img-BHsyRs3w-1563531171002)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/access_slot_attribute.png)]
[外链图片转存失败(img-N9LQaY6s-1563531171003)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/access_slot_attribute2.png)]
如果我们尝试访问或者设置一个不存在的属性
>>> a.not_exist = 33
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: 'A' object has no attribute 'not_exist'
根据 descr 中提到的 descriptor protocol 过程, 我们可以同样写出下面的伪代码
res = None
for each_type in type(a).__mro__:
if "not_exist" in each_type.__dict__:
res = each_type.__dict__["not_exist"]
break
if res is None:
# 尝试在 a.__dict__ 中查找 "not_exist"
if not hasattr(a, "__dict__") or "not_exist" not in a.__dict__:
# 运行到这里
raise AttributeError
return a.__dict__["not_exist"]
当定义了 __slots__
时, type(a)
中的 tp_dictoffset
值为 0, 表示实例 a
并不存在 __dict__
属性, 也就是说没有存储其他任何属性的位置, 上面进入的搜索分支会识别这种情况并报错
所以会抛出 AttributeError
[外链图片转存失败(img-ngOEsQ7l-1563531171006)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/access_slot_not_exist_attribute.png)]
没有slots
class A(object):
x = 3
wing = "wingA"
leg = "legA"
在创建class A
时属性是如何初始化的 ?
tp_dict
指向的字典对象现在有一个名为 __dict__
的 key
[外链图片转存失败(img-oxwQKsxw-1563531171006)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/type_create_no_slot.png)]
在创建instance a
时属性是如何初始化的 ?
[外链图片转存失败(img-DsOmgfmr-1563531171006)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/instance_create_no_slot.png)]
MRO中的属性搜索过程 ?
搜索过程和 有slots 的搜索过程类似
[外链图片转存失败(img-mQIMYeDl-1563531171007)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/access_no_slot_attribute.png)]
如果我们尝试访问或者设置一个不存在的属性
>>> a.not_exist = 33
>>> print(a.not_exist)
根据 descr 中提到的 descriptor protocol, 我们可以同样写出下面的伪代码
res = None
for each_type in type(a).__mro__:
if "not_exist" in each_type.__dict__:
res = each_type.__dict__["not_exist"]
break
if res is None:
# 尝试在 a.__dict__ 中查找 "not_exist"
if not hasattr(a, "__dict__") or "not_exist" not in a.__dict__:
raise AttributeError
# 运行到这里
return a.__dict__["not_exist"]
这一次没有定义 __slots__
, type(a)
中的 tp_dictoffset
值为 16, 表示实例 a
拥有 __dict__
属性, 可以存储任意其他的属性名称, 这个 __dict__
对象的地址为 (char *)a + 16
所以属性名称可以存储在 a.__dict__
中
[外链图片转存失败(img-lDcVdff2-1563531171009)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/access_no_slot_not_exist_attribute.png)]
[外链图片转存失败(img-ENd4MvAP-1563531171009)(https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/slot/access_no_slot_not_exist_attribute2.png)]
内存消耗测试
有 __slots__
./ipython
>>> import ipython_memory_usage.ipython_memory_usage as imu
>>> imu.start_watching_memory()
In [2] used 0.1367 MiB RAM in 3.59s, peaked 0.00 MiB above current, total RAM usage 41.16 MiB
class MyClass(object):
__slots__ = ['name', 'identifier']
def __init__(self, name, identifier):
self.name = name
self.identifier = identifier
num = 1024*256
x = [MyClass(1,1) for i in range(num)]
used 27.5508 MiB RAM in 0.28s, peaked 0.00 MiB above current, total RAM usage 69.18 MiB
没有 __slots__
./ipython
>>> import ipython_memory_usage.ipython_memory_usage as imu
>>> imu.start_watching_memory()
In [2] used 0.1367 MiB RAM in 3.59s, peaked 0.00 MiB above current, total RAM usage 41.16 MiB
class MyClass(object):
def __init__(self, name, identifier):
self.name = name
self.identifier = identifier
num = 1024*256
x = [MyClass(1,1) for i in range(num)]
used 56.0234 MiB RAM in 0.34s, peaked 0.00 MiB above current, total RAM usage 97.63 MiB
没有 __slots__
的情况下内存消耗几乎是有 __slots__
的情况下的两倍, 主要原因是有 __slots__
的时候属性的空间是在实例创建时一次性预分配好的, 存储的是指向 python 元素的指针, 每个 指针占用 8 字节的空间, 而没有 __slots__
的时候需要创建一个额外的 dict 对象 用来存储, 新增, 删除属性, 虽然时间复杂度类似, 但是 dict 对象 本身字典结构存储需要空间, 并且在当前版本下即使创建了空的字典, 本身也会预分配 8 个对象的空间, 即使是一个空字典也至少是需要一打指针的空间来存储, 这些都是额外开销