python slots源码分析
上次总结Python3的字典实现后的某一天,突然开窍Python的__slots__的实现应该也是类似,于是翻了翻CPython的源码,果然如此!
关于在自定义类里面添加__slots__的效果,网上已经有很多资料了,其中优点大致有:
(1)更省内存。
(2)访问属性更高效。
而本文讲的是,为什么更省内存?为什么更高效?当然为了弄明白这些,深入到CPython的源码是必不可少的。不过,心里有个猜想之后再去看源码效果或许更好,这样目的性更强,清楚自己需要关注的是什么以免在其中迷失!
我先稍微解释一下:
(1)更省内存是因为实例的属性不以字典的形式存储,而是以更紧凑的格式。
(2)更高效是因为实例在做属性查找的时候,节省了一次hash查找,改为以计算属性内存的偏移量直接读写内存。
接下来本文会从三方面分析定义了slots的作用以及影响,分别是:定义类时、创建实例为其分配内存时、以及从实例访问属性时。
1、定义类
先说一下在类定义时使用__slots__会有哪些影响
typeobject.c:
static PyObject *
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
...
/* Check for a __slots__ sequence variable in dict, and count it */
slots = PyDict_GetItemString(dict, "__slots__");
nslots = ;
if (slots == NULL) {
/* 类定义中没有__slots__,不需要关注 */
}
else {
/* Have slots */ /* Make it into a tuple */
if (PyString_Check(slots) || PyUnicode_Check(slots))
slots = PyTuple_Pack(, slots);
else
slots = PySequence_Tuple(slots);
if (slots == NULL) {
Py_DECREF(bases);
return NULL;
}
assert(PyTuple_Check(slots)); /* Copy slots into a list, mangle names and sort them.
Sorted names are needed for __class__ assignment.
Convert them back to tuple at the end.
*/
newslots = PyList_New(nslots - add_dict - add_weak);
if (newslots == NULL)
goto bad_slots;
for (i = j = ; i < nslots; i++) {
char *s;
tmp = PyTuple_GET_ITEM(slots, i);
s = PyString_AS_STRING(tmp);
if ((add_dict && strcmp(s, "__dict__") == ) ||
(add_weak && strcmp(s, "__weakref__") == ))
continue;
tmp =_Py_Mangle(name, tmp);
if (!tmp) {
Py_DECREF(newslots);
goto bad_slots;
}
PyList_SET_ITEM(newslots, j, tmp);
j++;
} nslots = j;
Py_DECREF(slots);
if (PyList_Sort(newslots) == -) {
Py_DECREF(bases);
Py_DECREF(newslots);
return NULL;
}
slots = PyList_AsTuple(newslots);
Py_DECREF(newslots);
if (slots == NULL) {
Py_DECREF(bases);
return NULL;
}
} /* Allocate the type object */
/* 为类对象申请内存,这里分配内存时也考虑了存储slots需要的内存 */
type = (PyTypeObject *)metatype->tp_alloc(metatype, nslots);
if (type == NULL) {
Py_XDECREF(slots);
Py_DECREF(bases);
return NULL;
} /* Add descriptors for custom slots from __slots__, or for __dict__ */
/* 将slots的数据作为member存储在类对象上,后续将会根据这个member创建具体的descriptior
* 而实际上读写这个属性都是通过descriptior实现的
*/
mp = PyHeapType_GET_MEMBERS(et);
slotoffset = base->tp_basicsize;
if (slots != NULL) {
for (i = ; i < nslots; i++, mp++) {
mp->name = PyString_AS_STRING(
PyTuple_GET_ITEM(slots, i));
mp->type = T_OBJECT_EX;
mp->offset = slotoffset; /* __dict__ and __weakref__ are already filtered out */
assert(strcmp(mp->name, "__dict__") != );
assert(strcmp(mp->name, "__weakref__") != ); slotoffset += sizeof(PyObject *);
}
} /* 类的type->tp_basicsize这个值描述了实例所占内存的大小(当然只是内存的一部分)
* 而从上面的代码可以看出,slotoffset这个值包含了nslots个指针大小。没错!这个指针就是实际存储属性用的
* 因此slots是直接存储在实例内存上面的,而属性的具体位置的偏移值信息则以member存储在类对象上
*/
type->tp_basicsize = slotoffset;
type->tp_itemsize = base->tp_itemsize;
type->tp_members = PyHeapType_GET_MEMBERS(et); /* Always override allocation strategy to use regular heap */
type->tp_alloc = PyType_GenericAlloc; /* 调用PyType_Ready这个函数时会为类身上的每个member创建一个descriptor
* 当实例访问属性时,会需要借助这个descriptor的力量:P
*/
if (PyType_Ready(type) < ) {
Py_DECREF(type);
return NULL;
} return (PyObject *)type;
}
当我们定义一个类的时候,最后会调用到上面type_new这个函数。由于只关注slots,因此我省略掉了一部分的代码。可以看出,如果有定义slots,那么会将其信息以member的形式存储在类的身上。观察初始化member的代码,可以发现关于访问属性的最重要的两个数据都在其中,一个是属性的内存位置,由相对于实例的偏移值mp->offset描述。通过这个偏移值,我们能拿到属性数据在内存起始地址,但却不知道如何解释这块内存,因此还需要一个类型信息,这个信息由mp->type来补充。
剩下的工作便是在调用函数PyType_Ready时,根据member中存储的信息,创建出执行访问操作的descriptor对象。
int
PyType_Ready(PyTypeObject *type)
{
/* Add type-specific descriptors to tp_dict */
if (type->tp_members != NULL) {
if (add_members(type, type->tp_members) < )
goto error;
}
return ; error:
type->tp_flags &= ~Py_TPFLAGS_READYING;
return -;
} static int
add_members(PyTypeObject *type, PyMemberDef *memb)
{
PyObject *dict = type->tp_dict; for (; memb->name != NULL; memb++) {
PyObject *descr;
if (PyDict_GetItemString(dict, memb->name))
continue;
descr = PyDescr_NewMember(type, memb);
if (descr == NULL)
return -;
if (PyDict_SetItemString(dict, memb->name, descr) < ) {
Py_DECREF(descr);
return -;
}
Py_DECREF(descr);
}
return ;
}
同样的,省略了很多其它不相关的代码。可以看出,最终根据member创建出的descriptor是存储在type对象上的tp_dict中的。
2、创建实例
当创建一个类的实例时,会为其分配内存。如果这个类定义了slots,那么会申请更多的内存,slots定义的属性便是存储在这部分内存中。直接看为实例申请内存的代码:
PyObject *
PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)
{
PyObject *obj;
const size_t size = _PyObject_VAR_SIZE(type, nitems+);
/* note that we need to add one, for the sentinel */ if (PyType_IS_GC(type))
obj = _PyObject_GC_Malloc(size);
else
obj = (PyObject *)PyObject_MALLOC(size); if (obj == NULL)
return PyErr_NoMemory(); memset(obj, '\0', size); if (type->tp_flags & Py_TPFLAGS_HEAPTYPE)
Py_INCREF(type); if (type->tp_itemsize == )
(void)PyObject_INIT(obj, type);
else
(void) PyObject_INIT_VAR((PyVarObject *)obj, type, nitems); if (PyType_IS_GC(type))
_PyObject_GC_TRACK(obj);
return obj;
} #define _PyObject_VAR_SIZE(typeobj, nitems) \
(size_t) \
( ( (typeobj)->tp_basicsize + \
(nitems)*(typeobj)->tp_itemsize + \
(SIZEOF_VOID_P - ) \
) & ~(SIZEOF_VOID_P - ) \
)
从代码可知,实例的内存大小与其type对象的tp_basicsize是相关联的。回看之前定义类时的type_new函数,会发现tp_basicsize这个值已经是包含了slots所需的内存了(详见计算member偏移值那部分代码)。type_new为slots中的每一项都分配一个指针长度的内存,而日后实例的属性便是存储在这个位置上。这也正是slots更省内存的原因!
3、访问属性
最后来看从实例上访问slots的属性是怎样的,以读属性的值为例
/* Generic GetAttr functions - put these in your tp_[gs]etattro slot */ PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict)
{
PyTypeObject *tp = Py_TYPE(obj);
PyObject *descr = NULL;
PyObject *res = NULL;
descrgetfunc f;
Py_ssize_t dictoffset;
PyObject **dictptr; if (tp->tp_dict == NULL) {
if (PyType_Ready(tp) < )
goto done;
} descr = _PyType_Lookup(tp, name); Py_XINCREF(descr); f = NULL;
if (descr != NULL &&
PyType_HasFeature(descr->ob_type, Py_TPFLAGS_HAVE_CLASS)) {
f = descr->ob_type->tp_descr_get;
if (f != NULL && PyDescr_IsData(descr)) {
res = f(descr, obj, (PyObject *)obj->ob_type);
Py_DECREF(descr);
goto done;
}
} if (dict == NULL) {
/* Inline _PyObject_GetDictPtr */
dictoffset = tp->tp_dictoffset;
if (dictoffset != ) {
if (dictoffset < ) {
Py_ssize_t tsize;
size_t size; tsize = ((PyVarObject *)obj)->ob_size;
if (tsize < )
tsize = -tsize;
size = _PyObject_VAR_SIZE(tp, tsize); dictoffset += (long)size;
assert(dictoffset > );
assert(dictoffset % SIZEOF_VOID_P == );
}
dictptr = (PyObject **) ((char *)obj + dictoffset);
dict = *dictptr;
}
}
if (dict != NULL) {
Py_INCREF(dict);
res = PyDict_GetItem(dict, name);
if (res != NULL) {
Py_INCREF(res);
Py_XDECREF(descr);
Py_DECREF(dict);
goto done;
}
Py_DECREF(dict);
} if (f != NULL) {
res = f(descr, obj, (PyObject *)Py_TYPE(obj));
Py_DECREF(descr);
goto done;
} if (descr != NULL) {
res = descr;
/* descr was already increfed above */
goto done;
} PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no attribute '%.400s'",
tp->tp_name, PyString_AS_STRING(name));
done:
Py_DECREF(name);
return res;
}
当从实例身上访问一个属性时,首先尝试从类对象的tp_dict查找,是否存在对应的descriptor。若是(查找slots的属性正是如此),调用descriptor身上的tp_descr_get方法,并将方法的返回值作为这次属性查找的结果返回。
从中也可以看出,如果是访问正常的属性时,还要根据type对象的dictoffset偏移值找到实例的属性字典,然后再在这个字典中执行hash查找属性。这就是为什么定义了slots后属性查找理论上会更高效。
看看tp_descr_get方法长啥样:
PyTypeObject PyMemberDescr_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, )
"member_descriptor",
sizeof(PyMemberDescrObject),
,
(destructor)descr_dealloc, /* tp_dealloc */
, /* tp_print */
, /* tp_getattr */
, /* tp_setattr */
, /* tp_compare */
(reprfunc)member_repr, /* tp_repr */
, /* tp_as_number */
, /* tp_as_sequence */
, /* tp_as_mapping */
, /* tp_hash */
, /* tp_call */
, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
, /* tp_setattro */
, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
, /* tp_doc */
descr_traverse, /* tp_traverse */
, /* tp_clear */
, /* tp_richcompare */
, /* tp_weaklistoffset */
, /* tp_iter */
, /* tp_iternext */
, /* tp_methods */
descr_members, /* tp_members */
member_getset, /* tp_getset */
, /* tp_base */
, /* tp_dict */
(descrgetfunc)member_get, /* tp_descr_get */
(descrsetfunc)member_set, /* tp_descr_set */
}; static PyObject *
member_get(PyMemberDescrObject *descr, PyObject *obj, PyObject *type)
{
PyObject *res; if (descr_check((PyDescrObject *)descr, obj, &res))
return res;
return PyMember_GetOne((char *)obj, descr->d_member);
}
原来最后是通过函数PyMember_GetOne来获取属性。好!继续深入:
PyObject *
PyMember_GetOne(const char *addr, PyMemberDef *l)
{
PyObject *v;
if ((l->flags & READ_RESTRICTED) &&
PyEval_GetRestricted()) {
PyErr_SetString(PyExc_RuntimeError, "restricted attribute");
return NULL;
}
addr += l->offset;
switch (l->type) {
case T_BOOL:
v = PyBool_FromLong(*(char*)addr);
break;
case T_BYTE:
v = PyInt_FromLong(*(char*)addr);
break;
case T_UBYTE:
v = PyLong_FromUnsignedLong(*(unsigned char*)addr);
break;
case T_SHORT:
v = PyInt_FromLong(*(short*)addr);
break;
case T_USHORT:
v = PyLong_FromUnsignedLong(*(unsigned short*)addr);
break;
case T_INT:
v = PyInt_FromLong(*(int*)addr);
break;
case T_UINT:
v = PyLong_FromUnsignedLong(*(unsigned int*)addr);
break;
case T_LONG:
v = PyInt_FromLong(*(long*)addr);
break;
case T_ULONG:
v = PyLong_FromUnsignedLong(*(unsigned long*)addr);
break;
case T_PYSSIZET:
v = PyInt_FromSsize_t(*(Py_ssize_t*)addr);
break;
case T_FLOAT:
v = PyFloat_FromDouble((double)*(float*)addr);
break;
case T_DOUBLE:
v = PyFloat_FromDouble(*(double*)addr);
break;
case T_STRING:
if (*(char**)addr == NULL) {
Py_INCREF(Py_None);
v = Py_None;
}
else
v = PyString_FromString(*(char**)addr);
break;
case T_STRING_INPLACE:
v = PyString_FromString((char*)addr);
break;
case T_CHAR:
v = PyString_FromStringAndSize((char*)addr, );
break;
case T_OBJECT:
v = *(PyObject **)addr;
if (v == NULL)
v = Py_None;
Py_INCREF(v);
break;
case T_OBJECT_EX:
/* slots对应的member->type是T_OBJECT_EX */
v = *(PyObject **)addr;
if (v == NULL)
PyErr_SetString(PyExc_AttributeError, l->name);
Py_XINCREF(v);
break;
#ifdef HAVE_LONG_LONG
case T_LONGLONG:
v = PyLong_FromLongLong(*(PY_LONG_LONG *)addr);
break;
case T_ULONGLONG:
v = PyLong_FromUnsignedLongLong(*(unsigned PY_LONG_LONG *)addr);
break;
#endif /* HAVE_LONG_LONG */
default:
PyErr_SetString(PyExc_SystemError, "bad memberdescr type");
v = NULL;
}
return v;
}
终于都看到了,根据member所记录的偏移值和类型,访问属性内存的代码了!
推荐阅读:http://code.activestate.com/recipes/532903-how-__slots__-are-implemented/
python slots源码分析的更多相关文章
- eos源码分析和应用(一)调试环境搭建
转载自 http://www.limerence2017.com/2018/09/02/eos1/#more eos基于区块链技术实现的开源引擎,开发人员可以基于该引擎开发DAPP(分布式应用).下面 ...
- python multiprocessing 源码分析
1. 文档是最先需要了解的,读完文档可能会有很多的意外的收获同时也会留下疑惑,对于一般的使用我觉得读完文档就差不多了,除非一些很有疑惑的地方你可能需要再深入的了解一下.我读文档的目的第一个就是为了找出 ...
- Python SocketServer源码分析
1 XXXServer 1.1 BaseSever 提供基础的循环等待请求的处理框架.使用serve_forever启动服务,使用shutdown停止.同时提供了一些可自行扩展的方 ...
- python SocketServer 源码分析
附上原文链接: http://beginman.cn/python/2015/04/06/python-SocketServer/
- selenium3 + python - action_chains源码分析
ActionChains简介 actionchains是selenium里面专门处理鼠标相关的操作如:鼠标移动,鼠标按钮操作,按键和上下文菜单(鼠标右键)交互.这对于做更复杂的动作非常有用,比如悬停和 ...
- python ironicclient源码分析
ironicclient是一个cli工具,用来和用户交互的. 首先写一个简单的例子,获取ironic所有的node节点: from ironicclient import client if __na ...
- Python之美[从菜鸟到高手]--urlparse源码分析
urlparse是用来解析url格式的,url格式如下:protocol :// hostname[:port] / path / [;parameters][?query]#fragment,其中; ...
- [python] 基于词云的关键词提取:wordcloud的使用、源码分析、中文词云生成和代码重写
1. 词云简介 词云,又称文字云.标签云,是对文本数据中出现频率较高的“关键词”在视觉上的突出呈现,形成关键词的渲染形成类似云一样的彩色图片,从而一眼就可以领略文本数据的主要表达意思.常见于博客.微博 ...
- Python源码分析(二) - List对象
python中的高级特性之一就是内置了list,dict等.今天就先围绕列表(List)进行源码分析. Python中的List对象(PyListObject) Python中的的PyListObje ...
随机推荐
- SQL中 ALL 和 ANY 区别的
在select中我们可能会认为all和any应该表达的意思差不多.其实他们的意思完全不一样: all: 是将后面的内容看成一个整体,如: >all (select age from studen ...
- 【hdu5306】Gorgeous Sequence 线段树区间最值操作
题目描述 给你一个序列,支持三种操作: $0\ x\ y\ t$ :将 $[x,y]$ 内大于 $t$ 的数变为 $t$ :$1\ x\ y$ :求 $[x,y]$ 内所有数的最大值:$2\ x\ y ...
- 解析php addslashes()与addclashes()函数的区别和比较
一. addslashes() 函数 addslashes(string) 函数在指定的预定义字符前添加反斜杠.这些预定义字符是:•单引号 (')•双引号 (")•反斜杠 (\)•NULL ...
- Javascript面向对象三大特性(封装性、继承性、多态性)详解及创建对象的各种方法
Javascript基于对象的三大特征和C++,Java面向对象的三大特征一样,都是封装(encapsulation).继承(inheritance )和多态(polymorphism ).只不过实现 ...
- [CQOI2011]动态逆序对 CDQ分治
洛谷上有2道相同的题目(基本是完全相同的,输入输出格式略有不同) ---题面--- ---题面--- CDQ分治 首先由于删除是很不好处理的,所以我们把删除改为插入,然后输出的时候倒着输出即可 首先这 ...
- 【bzoj4011】落忆枫音
Portal --> bzoj4011 Solution 这题..看了一眼之后深陷矩阵树定理然后我看了一眼数据范围== 注意到是有向无环图,DAG有十分多优秀的性质所以,这题需要充分利用这个 ...
- Python3 字典 update() 方法
Python3 字典 描述 Python 字典 update() 函数把字典dict2的键/值对更新到dict里. 语法 update()方法语法: dict.update(dict2) 参数 di ...
- Java之基础20160806
注意这里介绍的JAVA基础是指你对C语言已经比较熟练或者有一定基础了,再学习如下这知识就会比较快. 1.JAVA也是从MAIN开始执行,但是要先定义类,文件名要与类名一致并且类名首字母要大写,同时JA ...
- H5禁止手机虚拟键盘弹出
点击输入框弹出自定义弹窗,输入框是input标:但是在移动端,input会默认触发手机的虚拟键盘,如何阻止手机虚拟键盘弹起呢?目前我试过有两个方案,一个是给input添加readonly属性,另一个就 ...
- [大数据可视化]-saiku的源码打包运行/二次开发构建
Saiku构建好之后,会将项目的各个模块达成jar包,整个项目也会打成war包 saiku目录结构: 我们选中saiku-server/target/ 下面的zip压缩包.这是个打包后的文件,进行 ...