zoukankan      html  css  js  c++  java
  • 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/

  • 相关阅读:
    帝国 标签模板 使用程序代码 去除html标记 并 截取字符串
    iis6 伪静态 iis配置方法 【图解】
    您来自的链接不存在 帝国CMS
    帝国cms Warning: Cannot modify header information headers already sent by...错误【解决方法】
    .fr域名注册 51元注册.fr域名
    帝国网站管理系统 恢复栏目目录 建立目录不成功!请检查目录权限 Godaddy Windows 主机
    星外虚拟主机管理平台 开通数据库 出现Microsoft OLE DB Provider for SQL Server 错误 '8004' 从字符串向 datetime 转换失败
    ASP.NET 自定义控件学习研究
    CSS层叠样式表之CSS解析机制的优先级
    ASP.NET程序员工作面试网络收藏夹
  • 原文地址:https://www.cnblogs.com/adinosaur/p/7414782.html
Copyright © 2011-2022 走看看