python底层设计:列表对象设计

 

目录

1.初识PyListObject

2.PyListObject对象的创建和维护

2.1创建对象

2.2设置元素

2.3 内存分配方式

2.4.常见操作原理:

3.PyListObject对象缓冲池


1.初识PyListObject

    python里的列表不是书上基于链表的列表,而是基于可变长度的数组。PyListObject对象可以有效支持元素插入添加删除等操作,它的定义为:

typedef struct {
    PyObject_VAR_HEAD

    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;

     ob_item指针指向元素列表所在内存块首地址,而allocated中则维护了当前列表中可容纳的元素总数。我们知道PyObject_VAR_HEAD中有一个ob_size,它维护的是当前列表中已容纳的元素数量

 

2.PyListObject对象的创建和维护

2.1创建对象


PyObject *PyList_New(Py_ssize_t size)
{
    PyListObject *op;
    static int initialized = 0;
    if (!initialized) {
        Py_AtExit(show_alloc);
        initialized = 1;
    }
#endif

    if (size < 0) {
        PyErr_BadInternalCall();
        return NULL;
    }
    if (numfree) {
    缓冲池可用
        numfree--;
        op = free_list[numfree];
        _Py_NewReference((PyObject *)op);
        count_reuse++;
    } else {
    缓冲池不可用
        op = PyObject_GC_New(PyListObject, &PyList_Type);
        if (op == NULL)
            return NULL;
        count_alloc++;
    }
    if (size <= 0)
        op->ob_item = NULL;
    else {
        为列表申请内存空间
        op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof(PyObject *));
        if (op->ob_item == NULL) {
            Py_DECREF(op);
            return PyErr_NoMemory();
        }
    }
    Py_SIZE(op) = size;
    op->allocated = size;
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}

    从上面的创建动作我们可以看出,列表对象实际上分为两部分,一是PyListObject对象本身,二是PyListObject对象维护的元素列表,这是两块分离的内存,通过ob_item建立联系。

     另外,创建新的PyListObject对象时我们可以看到非常熟悉的缓冲池技术。在创建PyListObject时,会先检查缓冲池free_list中是否有可用的对象。如果有,则直接使用,否则通过PyObject_GC_New咋系统堆中申请内存,创建新的PyListObject。

    在python3.7.5中,默认情况下,free_list最多只有80个PyListObject对象。

#define PyList_MAXFREELIST 80
#endif
static PyListObject *free_list[PyList_MAXFREELIST];
static int numfree = 0;

 

2.2设置元素

   假设我们创建一个包含6个元素的PyListObject,它的内存图如下:

     注意创建一个list时列表元素不可能是null,这里只是为了演示元素变化。假设我们在第4个位置添加100,添加过程如下:

int
PyList_SetItem(PyObject *op, Py_ssize_t i,
               PyObject *newitem)
{
    PyObject **p;
    if (!PyList_Check(op)) {
        Py_XDECREF(newitem);
        PyErr_BadInternalCall();
        return -1;
    }
    【1】索引检查
    if (i < 0 || i >= Py_SIZE(op)) {
        Py_XDECREF(newitem);
        PyErr_SetString(PyExc_IndexError,
                        "list assignment index out of range");
        return -1;
    }
    【2】设置元素
    p = ((PyListObject *)op) -> ob_item + i;
    Py_XSETREF(*p, newitem);
    return 0;
}

    若索引正常,则【2】处将待加入的PyObject*指针放到有效位置,然后调整引用计数,将这个位置原来的对象引用计数-1.此时内存如下:

 

 

2.3 内存分配方式

 在将元素添加,删除前需要讲讲PyListObject中的allocated的变化情况:

 

static int list_resize(PyListObject *self, Py_ssize_t newsize)
{
    PyObject **items;
    size_t new_allocated, num_allocated_bytes;   #定义新分配内存的大小的变量
    Py_ssize_t allocated = self->allocated;   #已经分配的内存大小

    
   【1】 当前分配内存必须比实际占用的内存要大,如果缩小(pop,del)
    实际占用的内存大小小于分配内存大小的一半,则缩小内存
    
   
    if (allocated >= newsize && newsize >= (allocated >> 1)) {
        assert(self->ob_item != NULL || newsize == 0);
        Py_SIZE(self) = newsize;
        return 0;
    }

    【2】系统会分配足够的内存保证list不会出现溢出情况
     内存的增长方式是:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
     分配的最大值可能是 PY_SSIZE_T_MAX * (9 / 8) + 6 也就是说超预分配的量,大概只有总量的八分之一再加上3或者6
     
    
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
    【3】最大的分配值不可以超过系统定义的最大值
    if (new_allocated > (size_t)PY_SSIZE_T_MAX / sizeof(PyObject *)) {
        PyErr_NoMemory();
        return -1;
    }
    
     【4】实际内存占用为0时将分配内存缩小到0
   
    if (newsize == 0)
        new_allocated = 0;
    num_allocated_bytes = new_allocated * sizeof(PyObject *);
    items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes);
    if (items == NULL) {
        PyErr_NoMemory();
        return -1;
    }
    
    self->ob_item = items;
    Py_SIZE(self) = newsize;
    self->allocated = new_allocated;
    return 0;
}

从上面的算法我们可以看出,元素列表总内存变化算法为:new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6),也就是说超预分配空间是实际占用长度的1/8,如果ob_size小于9,则只需再加3,大于则再加6。

  我们执行以下操作,元素列表内存变化情况如下:

l = list()
l.append(1)
l.append(2)
l.append(3)
l.append(4)
l.insert(1,5)

 

 

    在要添加第一个元素时,根据增长模式,系统先分配4个空间,1,2,3,4元素都可以填充进去,在游标1 插入元素5时,系统判定内存不够,分配空间增加到8个空间,如图

然后进行

l.pop()
l.pop()

此时内存变化情况如下:

当pop到只剩下3个元素时,3 <(8/2),此时需要缩小内存,缩小办法:超预分配的内存是实际占用的1/8,如果《实际占用大小》< 9的话,再添加3个预分配空间,否则加6.

new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

 

2.4.常见操作原理:

1.插入操作:调用PyList_Insert函数,实际是调用内部的ins1函数

static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
    Py_ssize_t i, n = Py_SIZE(self);
    PyObject **items;
    【1】如果插入的是空值则报错返回-1
    if (v == NULL) {
        PyErr_BadInternalCall();
        return -1;
    }
    【2】如果自身内存已经达到系统定义最大值的话,则无法再添加
    if (n == PY_SSIZE_T_MAX) {
        PyErr_SetString(PyExc_OverflowError,
            "cannot add more objects to list");
        return -1;
    }
    【3】如果自身内存已经达到系统定义最大值的话,则无法再添加*/

    if (list_resize(self, n+1) < 0)
        return -1;
    【4】如果游标为负数,表示倒数插入,比如-1表示插在倒数一个位置,-2表示倒数第二个*/

    if (where < 0) {
        where += n;
        if (where < 0)
            where = 0;
    }
    【5】如果游标为大于分配的空间尺寸,则插在倒数第一个*/

    if (where > n)
        where = n;
    items = self->ob_item;
    【5】插入位置右边元素向右移一位*/
    for (i = n; --i >= where; )
        items[i+1] = items[i];
    Py_INCREF(v);
    items[where] = v;
    return 0;
}

int
PyList_Insert(PyObject *op, Py_ssize_t where, PyObject *newitem)
{
    if (!PyList_Check(op)) {
        PyErr_BadInternalCall();
        return -1;
    }
    return ins1((PyListObject *)op, where, newitem);
}

  插入流程:

  1. 检查是否为空值;
  2. 检查allocated是否已经达到系统定义的最大值;
  3. 对负数游标进行转换;
  4. 对插入元素右边元素右移一位;

 

2.追加操作append:跟insert差不多,区别是不用移动右边元素,因为append是直接添加在末尾

3.切片slice操作:切片过程需要进行新建一个数组对象来保存切片数据,也就是说切片过程实质是一个深拷贝过程

static PyObject *
list_slice(PyListObject *a, Py_ssize_t ilow, Py_ssize_t ihigh)
{
    
    ilow切片起始位置,ihigh结束位置
    
    PyListObject *np;
    PyObject **src, **dest;#src指向源切片的起始位置,dest数组指向保存数据的数组地址
    Py_ssize_t i, len;
    
    【1】起始位置情况判断
    

    //起始小于0则默认0
    if (ilow < 0)  
        ilow = 0;
    //起始大于最大长度则默认最大长度作为起始

    else if (ilow > Py_SIZE(a))
        ilow = Py_SIZE(a); 
    if (ihigh < ilow)
        ihigh = ilow;
    else if (ihigh > Py_SIZE(a))
        ihigh = Py_SIZE(a);
    len = ihigh - ilow; //切片长度
    np = (PyListObject *) PyList_New(len); //申请空间克隆
    if (np == NULL)
        return NULL;

    src = a->ob_item + ilow; #源切片起始地址
    dest = np->ob_item;  3目标存放数据的数组地址
    【2】拷贝数据
    for (i = 0; i < len; i++) {
        PyObject *v = src[i];
        Py_INCREF(v);
        dest[i] = v;
    }
    return (PyObject *)np;
}

PyObject *
PyList_GetSlice(PyObject *a, Py_ssize_t ilow, Py_ssize_t ihigh)
{
    if (!PyList_Check(a)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    return list_slice((PyListObject *)a, ilow, ihigh);
}

4.列表拼接extend:拼接操作需要循环遍历2个列表且需新建一个数组对象来保存数据,其实质也是深拷贝。

static PyObject *
list_concat(PyListObject *a, PyObject *bb)
{
    Py_ssize_t size;
    Py_ssize_t i;
    PyObject **src, **dest;
    PyListObject *np;
    if (!PyList_Check(bb)) {
        PyErr_Format(PyExc_TypeError,
                  "can only concatenate list (not \"%.200s\") to list",
                  bb->ob_type->tp_name);
        return NULL;
    }
#define b ((PyListObject *)bb)
    【1】拼接后长度不能大于系统定义的最大长度
    if (Py_SIZE(a) > PY_SSIZE_T_MAX - Py_SIZE(b))
        return PyErr_NoMemory();
    
    size = Py_SIZE(a) + Py_SIZE(b);//拼接后的长度大小
    np = (PyListObject *) PyList_New(size); //新建长度为size的数组对象
    if (np == NULL) {
        return NULL;
    }
    src = a->ob_item; //列表1的数据
    dest = np->ob_item; //指向拼接后的列表数据地址
   【2】 拷贝列表1数据
    for (i = 0; i < Py_SIZE(a); i++) {
        PyObject *v = src[i];
        Py_INCREF(v);
        dest[i] = v;
    }
    src = b->ob_item;//列表2的数据
    dest = np->ob_item + Py_SIZE(a);//列表2数据在dest的插入点地址
    【3】拷贝列表2数据
    for (i = 0; i < Py_SIZE(b); i++) {
        PyObject *v = src[i];
        Py_INCREF(v);
        dest[i] = v;
    }
    return (PyObject *)np;
#undef b
}

5.pop操作:可以看出pop某个位置(非末尾)的数据也需要循环遍历数组。

static PyObject *
list_pop_impl(PyListObject *self, Py_ssize_t index)
{
    PyObject *v;
    int status;

    if (Py_SIZE(self) == 0) {
        PyErr_SetString(PyExc_IndexError, "pop from empty list");
        return NULL;
    }
    【1】pop位置小于0则表示从右边倒数第几pop,pop前提是不得超过实际长度
    if (index < 0)
        index += Py_SIZE(self);
    if (index < 0 || index >= Py_SIZE(self)) {
        PyErr_SetString(PyExc_IndexError, "pop index out of range");
        return NULL;
    }
    v = self->ob_item[index];//需要pop的元素
    【2】如果是最后一个元素,直接缩容
    if (index == Py_SIZE(self) - 1) {
        status = list_resize(self, Py_SIZE(self) - 1);
        if (status >= 0)
            return v; /* and v now owns the reference the list had */
        else
            return NULL;
    }
    Py_INCREF(v);
    【3】进行切片合并
    status = list_ass_slice(self, index, index+1, (PyObject *)NULL);
    if (status < 0) {
        Py_DECREF(v);
        return NULL;
    }
    //返回删除的对象
    return v;
}

另外需要说明的是,由于py里的列表维护了指针数组,元素可以是任意对象,所以每个元素大小不确定,这就导致了它不能像c数组一样连续排在内存里,,所以假若元素大小都一样,列表就有点吃亏了,它离散的对象位置不能很好的利用cpu高速缓存,造成了遍历需要更多的cpu周期。

list和tuple在c实现上是很相似的,都是一个指针数组,对于小对象来说,tuple会有一个对象池,所以小的、重复的使用tuple还有益处的。

 

3.PyListObject对象缓冲池

    free_list中所缓冲的PyListObject是从哪里获取的,是何时创建的,答案就在PyListObject被销毁过程:

static void
list_dealloc(PyListObject *op)
{
    Py_ssize_t i;
    PyObject_GC_UnTrack(op);
    Py_TRASHCAN_SAFE_BEGIN(op)
    【1】销毁PyListObject维护的元素列表
    if (op->ob_item != NULL) {
       
        i = Py_SIZE(op);
        while (--i >= 0) {
            Py_XDECREF(op->ob_item[i]);
        }
        PyMem_FREE(op->ob_item);
    }
     【2】释放PyListObject自身
    if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))
        free_list[numfree++] = op;
    else
        Py_TYPE(op)->tp_free((PyObject *)op);
    Py_TRASHCAN_SAFE_END(op)
}

     在创建一个新list时,我们可以看到创建过程分为两部,PyListObject对象和维护的元素列表。与之对应,在销毁一个list时,销毁过程也分离,先销毁PyListObject维护的元素列表,再释放PyListObject自身。

     【1】处的工作没有特别之处,而在【2】出现了有趣的东西,在删除PyListObject自身时,python会先检查缓冲池free_list,查看其中缓冲的PyListObject对象是否满了,如果没有,就将该PyListObject对象放在缓冲池中,以备后用

     现在一切真相大白了,缓冲池被本应该死去的PyListObject对象给填充了,在以后创建新的PyListObject时,python会首先唤醒这些死去的PyListObject,又给他们一个生的机会。这里缓冲仅仅是PyListObject对象,不包括它曾经拥有的元素列表。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值