• 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, '', 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所记录的偏移值和类型,访问属性内存的代码了!

    推荐阅读:http://code.activestate.com/recipes/532903-how-__slots__-are-implemented/

  • 相关阅读:
    一些常用的Ant标签
    c++ 精简版 scope_guard
    c++ 精简版 fps限制
    用c++11封装win32界面库
    c++ 精简版 signal
    SQL Server 数据库中的 MD5 和 SHA1加密算法
    不同服务器数据库之间的数据操作
    MSSQL行专列
    JS倒计时代码
    破解网页中限制的《七种武器》
  • 原文地址:https://www.cnblogs.com/adinosaur/p/7414782.html
Copyright © 2020-2023  润新知