python之__slots__

一,先谈谈不使用__slots__,通常定义完class,给实例绑定属性与方法,对另一个实例没有作用的

from types import MethodType

class Hero (object):
    pass

def set_elem(self,elem):
    self.elem = elem

hero = Hero()
hero.name = 'Ani'

hero.set_elem = MethodType(set_elem, hero)            # 给实例绑定一个方法

hero.set_elem('magic')
print(hero.name,hero.elem)
结果:
Ani magic
herotester = Hero()
herotester.set_elem('tester')                             #创建一个新的实例调用set_elem
结果:
AttributeError: type object 'herotester' has no attribute 'set_elem'

这时候可以给class绑定方法

Hero.set_elem = set_elem

执行上一步,发现所有实例均可调用

herotester = Hero()
Hero.set_elem('tester')                             #创建一个新的实例调用set_elem
print(herotester.elem)
结果:
Ani magic
tester

通常情况下,上面的set_elem方法可以直接定义在class中,但动态绑定允许我们在程序运行的过程中动态给class加上功能,静态语言却很难实现

二,回到正文,对于Hero类在动态语言中可以随时随意的修改其属性及方法,所以为了达到限制目的,引入__slots__,使得上述的案例动态绑定对应的属性或者方法

class Heroobject:
          __slots__ = (‘name’,‘elem’,‘set_zhuangbei’)                           #tuple定义绑定的属性名称

再进行对方法和属性的测试

hero.set_elem = MethodType(set_elem, hero) # 给实例绑定一个方法
此时报错:
AttributeError: 'Hero' object has no attribute 'set_elem'                             #方法报错
herotester = Hero()
Herotester.sex = woman 
此时报错
AttributeError: 'Hero' object has no attribute 'sex'                                 #属性报错

而对__slots__添加方法set_elem则可以

class Hero(object):
    __slots__ = ('name','elem','set_elem')
    
def set_elem(self,elem):
    self.elem = elem
    
hero.set_elem('magic')
print(hero.elem)

**但是此处有一个特例,如果set_elem方法中不在__slots__中的属性,那么依然会报错如下,如下对set_elem稍加修改

class Hero(object):
    __slots__ = ('name','elem','set_elem')

def set_elem(self,elemm):
    self.elemm = elemm

结果:

AttributeError: 'Hero' object has no attribute 'elemm'

由此可见__slots__做了一个绑定类的方法(方法里的属性也同样做了限制)与属性的限制
同时注意,在__init__方法中定义的属性都是动态的,都受到__slots__的限制。在__init__ 等方法的执行过程中,定义的属性跟终端通过实例增加属性是一样的,都是动态的。
在终端通过实例增加方法,受到__slots__的限制,但是在类的声明中通过def实现的方法是不受__slots__限制的,因为在类的声明中实现的方法是静态的。

三,如果父类做了__slots__的限制,而子类没有进行限制,那么子类依旧可以绑定任意的方法及属性,如下:

class Dog(Hero):
    pass

dog = Dog()
dog.fav = 'eat'
print(dog.fav)

可见父类__slots__定义的属性仅对当前类起作用,对继承的子类不起作用
但是如果子类也加上了__slots__,那么子类此时不仅使用自己的__slots__,同时也继承了父类的__slots__

见如下例:

class Dog(Hero):
    __slots__ = ('fav','run')

dog = Dog()
dog.name = 'hashiqi'
print(dog.name)
dog.fav = 'eat'
dog.color = 'blue'

结果:

AttributeError: 'Dog' object has no attribute 'color'
hashiqi

四,浅谈__slots__的好处及源码剖析

(1)更省内存。 ————更省内存是因为实例的属性不以字典的形式存储,而是以更紧凑的格式。
(2)访问属性更高效。 ————更高效是因为实例在做属性查找的时候,节省了一次hash查找,改为以计算属性内存的偏移量直接读写内存。

定义类时、创建实例为其分配内存时、以及从实例访问属性时分别讨论__slots__的影响
(1)定义类
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所记录的偏移值和类型,访问属性内存的代码了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值