python slots源码分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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所记录的偏移值和类型,访问属性内存的代码了!

以上是关于python slots源码分析的主要内容,如果未能解决你的问题,请参考以下文章

redis hashtag一文搞懂,源码解析

python+spark程序代码片段

vue3源码分析——实现slots

vue3源码分析——实现slots

vue3源码分析——实现slots

Vue.js 源码分析—— Slots 是如何实现的