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 = 0;
    if (slots == NULL) {
        /* 类定义中没有__slots__,不需要关注 */
    }
    else {
        /* Have slots */

        /* Make it into a tuple */
        if (PyString_Check(slots) || PyUnicode_Check(slots))
            slots = PyTuple_Pack(1, 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 = 0; i < nslots; i++) {
            char *s;
            tmp = PyTuple_GET_ITEM(slots, i);
            s = PyString_AS_STRING(tmp);
            if ((add_dict && strcmp(s, "__dict__") == 0) ||
                (add_weak && strcmp(s, "__weakref__") == 0))
                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) == -1) {
            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 = 0; 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__") != 0);
            assert(strcmp(mp->name, "__weakref__") != 0);

            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) < 0) {
        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) < 0)
            goto error;
    }
    return 0;

  error:
    type->tp_flags &= ~Py_TPFLAGS_READYING;
    return -1;
}

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 -1;
        if (PyDict_SetItemString(dict, memb->name, descr) < 0) {
            Py_DECREF(descr);
            return -1;
        }
        Py_DECREF(descr);
    }
    return 0;
}

同样的,省略了很多其它不相关的代码。可以看出,最终根据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+1);
    /* 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 == 0)
        (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 - 1)                           ) & ~(SIZEOF_VOID_P - 1)                      )

从代码可知,实例的内存大小与其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) < 0)
            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 != 0) {
            if (dictoffset < 0) {
                Py_ssize_t tsize;
                size_t size;

                tsize = ((PyVarObject *)obj)->ob_size;
                if (tsize < 0)
                    tsize = -tsize;
                size = _PyObject_VAR_SIZE(tp, tsize);

                dictoffset += (long)size;
                assert(dictoffset > 0);
                assert(dictoffset % SIZEOF_VOID_P == 0);
            }
            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, 0)
    "member_descriptor",
    sizeof(PyMemberDescrObject),
    0,
    (destructor)descr_dealloc,                  /* tp_dealloc */
    0,                                          /* tp_print */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_compare */
    (reprfunc)member_repr,                      /* tp_repr */
    0,                                          /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    0,                                          /* tp_hash */
    0,                                          /* tp_call */
    0,                                          /* tp_str */
    PyObject_GenericGetAttr,                    /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
    0,                                          /* tp_doc */
    descr_traverse,                             /* tp_traverse */
    0,                                          /* tp_clear */
    0,                                          /* tp_richcompare */
    0,                                          /* tp_weaklistoffset */
    0,                                          /* tp_iter */
    0,                                          /* tp_iternext */
    0,                                          /* tp_methods */
    descr_members,                              /* tp_members */
    member_getset,                              /* tp_getset */
    0,                                          /* tp_base */
    0,                                          /* 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, 1);
        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所记录的偏移值和类型,访问属性内存的代码了!

时间: 2024-11-08 22:17:19

python slots源码分析的相关文章

python SocketServer 源码分析

附上原文链接: http://beginman.cn/python/2015/04/06/python-SocketServer/

Python源码分析(一)

最近想学习下Python的源码,希望写个系列博客,记录的同时督促自己学习. Python源码目录 从Python.org中下载源代码压缩包并解压,我下载的是Python2.7.12,解压后: 对于主要的文件夹做出介绍: Include:包含Python提供的所有头文件,如果需要自己使用C或者C++编写自定义模块扩展Python,就需要用到这里的头文件: Lib: 包含Python自带的所有标准库,全部由Python语言编写: Modules:包含了所有使用C语言编写的模块: Parser:Pyt

K-近邻算法的Python实现 : 源码分析

网上介绍K-近邻算法的例子很多,其Python实现版本基本都是来自于机器学习的入门书籍<机器学习实战>,虽然K-近邻算法本身很简单,但很多初学者对其Python版本的源代码理解不够,所以本文将对其源代码进行分析. 什么是K-近邻算法? 简单的说,K-近邻算法采用不同特征值之间的距离方法进行分类.所以它是一个分类算法. 优点:无数据输入假定,对异常值不敏感 缺点:复杂度高 好了,直接先上代码,等会在分析:(这份代码来自<机器学习实战>) def classify0(inx, data

Python之美[从菜鸟到高手]--浅拷贝、深拷贝完全解读(copy源码分析)

可悲的我一直以为copy模块是用C写的,有时候需要深入了解deepcopy,文档描述的实在太简单,还是不知所云. 比如说最近看sqlmap源码中AttribDict的_deepcopy__有些疑惑, def __deepcopy__(self, memo): retVal = self.__class__() memo[id(self)] = retVal for attr in dir(self): if not attr.startswith('_'): value = getattr(se

zg手册 之 python2.7.7源码分析(5)-- python的作用域和名空间

在 python 中, module,作用域,名空间这几个概念与虚拟机的运行机制有紧密的联系, 这里先了解 module,作用域,和名空间,为后面分析虚拟机的运行做准备. module 在python中一个文件对应是一个module,每个py文件被导入后都对应一个module对象. 这个对象包含有一个dict对象,保存着本py文件中对应的变量和函数的引用, 也保存从其他python文件(module)导入的变量或函数的引用. 名空间和作用域 python 有三个独立的名空间, local, gl

Python:线程、进程与协程(3)——Queue模块及源码分析

Queue模块是提供队列操作的模块,队列是线程间最常用的交换数据的形式.该模块提供了三种队列: Queue.Queue(maxsize):先进先出,maxsize是队列的大小,其值为非正数时为无线循环队列 Queue.LifoQueue(maxsize):后进先出,相当于栈 Queue.PriorityQueue(maxsize):优先级队列. 其中LifoQueue,PriorityQueue是Queue的子类.三者拥有以下共同的方法: qsize():返回近似的队列大小.为什么要加"近似&q

zg手册 之 python2.7.7源码分析(2)-- python 的整数对象和字符串对象

python 中的内置对象 python 中常用的内置对象有:整数对象,字符串对象,列表对象,字典对象.这些对象在python中使用最多,所以在实现上提供缓存机制,以提高运行效率. 整数对象 (PyIntObject) python 中的整数对象是不可变对象(immutable),即创建了一个 python 整数对象之后,不能再改变该对象的值. python 为创建整数对象提供了下面三种方法,其中 PyInt_FromString 和 PyInt_FromUnicode 内部也是调用 PyInt

zg手册 之 python2.7.7源码分析(1)-- python中的对象

源代码主要目录结构 Demo: python 的示例程序 Doc: 文档 Grammar: 用BNF的语法定义了Python的全部语法,提供给解析器使用 Include: 头文件,在用c/c++编写扩展模块时使用 Lib: Python自带的标准库,用python编写的 Modules: 用c编写的内建模块的实现,zlib,md5 等 Objects: 内建对象类型的实现 list,dict 等 PC:      windows 平台相关文件 PCbuild: Microsoft Visual

[python] 词云:wordcloud包的安装、使用、原理(源码分析)、中文词云生成、代码重写

词云,又称文字云.标签云,是对文本数据中出现频率较高的“关键词”在视觉上的突出呈现,形成关键词的渲染形成类似云一样的彩色图片,从而一眼就可以领略文本数据的主要表达意思.常见于博客.微博.文章分析等. 除了网上现成的Wordle.Tagxedo.Tagul.Tagcrowd等词云制作工具,在python中也可以用wordcloud包比较轻松地实现(官网.github项目): from wordcloud import WordCloud import matplotlib.pyplot as pl