美文网首页
【Python】列表

【Python】列表

作者: lndyzwdxhs | 来源:发表于2018-11-11 16:28 被阅读5次

    0x01 PyListObject

    typedef struct {
        PyObject_VAR_HEAD
        /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
        PyObject **ob_item;
    
        /* ob_item contains space for 'allocated' elements.  The number
         * currently in use is ob_size.
         * Invariants:
         *     0 <= ob_size <= allocated
         *     len(list) == ob_size
         *     ob_item == NULL implies ob_size == allocated == 0
         * list.sort() temporarily sets allocated to -1 to detect mutations.
         *
         * Items must normally not be NULL, except during construction when
         * the list is not yet visible outside the function that builds it.
         */
        Py_ssize_t allocated;
    } PyListObject;
    
    • PyListObject结构图示(内部元素以int为例)
    • PyListObject是一个可变长对象,也是一个可变对象。

    • ob_item是指向元素列表的指针,即Python中的list[0]就是ob_item[0]

    • ob_item指针和allocated数量正是维护元素列表(PyObjecy *列表)的关键。ob_item指针指向了元素列表所在内存块的首地址,allocated维护了当前列表中可容纳的元素总数。

    • ob_sizeallocated的关系是什么?

      • PyListObject对象并不是使用多少内存就申请多少内存(这样操作的效率会很低),而是每次申请一大块内存。
      • 这一大块的总内存就是由allocated来维护
      • 实际使用的内存数量由ob_size来管理

    0x02 创建PyListObject对象

    PyObject *
    PyList_New(Py_ssize_t size)
    {
        PyListObject *op;
        size_t nbytes;
    #ifdef SHOW_ALLOC_COUNT
        static int initialized = 0;
        if (!initialized) {
            Py_AtExit(show_alloc);
            initialized = 1;
        }
    #endif
    
        if (size < 0) {
            PyErr_BadInternalCall();
            return NULL;
        }
        /* Check for overflow without an actual overflow,
         *  which can cause compiler to optimise out */
        if ((size_t)size > PY_SIZE_MAX / sizeof(PyObject *))
            return PyErr_NoMemory();
        nbytes = size * sizeof(PyObject *);
        if (numfree) {
            numfree--;
            op = free_list[numfree];
            _Py_NewReference((PyObject *)op);
    #ifdef SHOW_ALLOC_COUNT
            count_reuse++;
    #endif
        } else {
            op = PyObject_GC_New(PyListObject, &PyList_Type);
            if (op == NULL)
                return NULL;
    #ifdef SHOW_ALLOC_COUNT
            count_alloc++;
    #endif
        }
        if (size <= 0)
            op->ob_item = NULL;
        else {
            op->ob_item = (PyObject **) PyMem_MALLOC(nbytes);
            if (op->ob_item == NULL) {
                Py_DECREF(op);
                return PyErr_NoMemory();
            }
            memset(op->ob_item, 0, nbytes);
        }
        Py_SIZE(op) = size;
        op->allocated = size;
        _PyObject_GC_TRACK(op);
        return (PyObject *) op;
    }
    
    • 只提供了PyList_New这一个方法来创建PyListObject对象。
    • PyListObject对象实际上是分为两部分的:一是PyListObject对象本身;二是PyListObject对象维护的元素列表。
    • 首先会根据传入的size计算所需内存数量,进行溢出检查;
    • 创建PyListObject对象本身
      • 如果缓冲池可用,使用缓冲池,如果缓冲池没有可用对象,会在系统堆上申请内存创建PyListObject对象;
    • 创建PyListObject对象维护的PyObject *对象列表
      • 根据size计算出来的总内存大小,来为PyObject *对象列表申请内存空间。
      • 每一个元素都会被初始化为NULL
    • 最后设置ob_sizeallocated的大小
    • 当然在创建第一个PyListObject对象时,缓冲池还是空的,所以调用PyObject_GC_New函数创建PyListObject对象;假设我们创建6个元素的PyListObject对象,如下图所示
    • 新创建的PyListObject对象

    设置元素

    int
    PyList_SetItem(register PyObject *op, register Py_ssize_t i,
                   register PyObject *newitem)
    {
        register PyObject *olditem;
        register PyObject **p;
        if (!PyList_Check(op)) {
            Py_XDECREF(newitem);
            PyErr_BadInternalCall();
            return -1;
        }
        if (i < 0 || i >= Py_SIZE(op)) {
            Py_XDECREF(newitem);
            PyErr_SetString(PyExc_IndexError,
                            "list assignment index out of range");
            return -1;
        }
        p = ((PyListObject *)op) -> ob_item + i;
        olditem = *p;
        *p = newitem;
        Py_XDECREF(olditem);
        return 0;
    }
    
    • 先是类型检查和索引有效性检查
    • 然后是新值取代旧值
    • 在此期间ob_item指向的内存没有发生变化

    插入元素

    插入元素可能会导致ob_item指向的内存发生变化

    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);
    }
    
    static int
    ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
    {
        Py_ssize_t i, n = Py_SIZE(self);
        PyObject **items;
        if (v == NULL) {
            PyErr_BadInternalCall();
            return -1;
        }
        if (n == PY_SSIZE_T_MAX) {
            PyErr_SetString(PyExc_OverflowError,
                "cannot add more objects to list");
            return -1;
        }
    
        if (list_resize(self, n+1) == -1)
            return -1;
    
        if (where < 0) {
            where += n;
            if (where < 0)
                where = 0;
        }
        if (where > n)
            where = n;
        items = self->ob_item;
        for (i = n; --i >= where; )
            items[i+1] = items[i];
        Py_INCREF(v);
        items[where] = v;
        return 0;
    }
    
    static int
    list_resize(PyListObject *self, Py_ssize_t newsize)
    {
        PyObject **items;
        size_t new_allocated;
        Py_ssize_t allocated = self->allocated;
    
        /* Bypass realloc() when a previous overallocation is large enough
           to accommodate the newsize.  If the newsize falls lower than half
           the allocated size, then proceed with the realloc() to shrink the list.
        */
        if (allocated >= newsize && newsize >= (allocated >> 1)) {
            assert(self->ob_item != NULL || newsize == 0);
            Py_SIZE(self) = newsize;
            return 0;
        }
    
        /* This over-allocates proportional to the list size, making room
         * for additional growth.  The over-allocation is mild, but is
         * enough to give linear-time amortized behavior over a long
         * sequence of appends() in the presence of a poorly-performing
         * system realloc().
         * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
         */
        new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
    
        /* check for integer overflow */
        if (new_allocated > PY_SIZE_MAX - newsize) {
            PyErr_NoMemory();
            return -1;
        } else {
            new_allocated += newsize;
        }
    
        if (newsize == 0)
            new_allocated = 0;
        items = self->ob_item;
        if (new_allocated <= (PY_SIZE_MAX / sizeof(PyObject *)))
            PyMem_RESIZE(items, PyObject *, new_allocated);
        else
            items = NULL;
        if (items == NULL) {
            PyErr_NoMemory();
            return -1;
        }
        self->ob_item = items;
        Py_SIZE(self) = newsize;
        self->allocated = new_allocated;
        return 0;
    }
    
    • 实际调用的是内部的ins1函数;
    • 为了完成插入元素的工作,需要先确保PyListObject对象的列表容量是否充足,所以调用list_resize函数来保证容量一定充足。
      • 先判断总大小(allocated)是否大于等于ob_size+1,并且ob_size+1大于等于总大小(allocated)的一半(allocated >= newsize && newsize >= (allocated >> 1)),如果是的话就不需要重新分配内存块
      • 如果不是第一种情况,就需要重新分配内存;按照规则计算出需要另外申请的内存大小,最终调用Crealloc函数(作用:就是在之前使用malloc申请的内存后面再增加一些内存空间)申请内存空间
      • 另外需要注意的是:如果ob_size+1的大小小于总大小的一半,还需要对之前的内存大小进行收缩,以免浪费空间(真是锱铢必较啊,大师就是大师,细节拿捏的这么精确)
    • 元素列表内存空间调整完以后,接下来是实际的插入元素。
      • 确定元素的插入点。因为Python支持负索引,所以需要考虑负数的情况

    还有PyListObject对象的append方法,和insert差不多,只是它添加元素的位置是ob_size+1,而不是allocated+1的位置。

    删除元素

    static PyObject *
    listremove(PyListObject *self, PyObject *v)
    {
        Py_ssize_t i;
    
        for (i = 0; i < Py_SIZE(self); i++) {
            int cmp = PyObject_RichCompareBool(self->ob_item[i], v, Py_EQ);
            if (cmp > 0) {
                if (list_ass_slice(self, i, i+1,
                                   (PyObject *)NULL) == 0)
                    Py_RETURN_NONE;
                return NULL;
            }
            else if (cmp < 0)
                return NULL;
        }
        PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
        return NULL;
    }
    
    • 遍历列表内的每一个元素,通过PyObject_RichCompareBool函数判断元素是否与待删除元素相同,相同的话返回值大于0
    • 如果相同调用list_ass_slice函数删除该元素

    0x03 缓冲池

    PyIntObjectPyStringObject对象都有相应的缓冲池机制,PyListObject也不例外。

    #define PyList_MAXFREELIST 80
    
    static PyListObject *free_list[PyList_MAXFREELIST];
    static int numfree = 0;
    
    • Python初始化的时候,free_list数组内的指针都是NULL(数组内最多缓存的对象个数为80个),numfree也是0;即没有任何PyListObject对象缓存,那么是何时对PyListObject对象进行缓存的呢?
    static void
    list_dealloc(PyListObject *op)
    {
        Py_ssize_t i;
        PyObject_GC_UnTrack(op);
        Py_TRASHCAN_SAFE_BEGIN(op)
        if (op->ob_item != NULL) {
            /* Do it backwards, for Christian Tismer.
               There's a simple test case where somehow this reduces
               thrashing when a *very* large list is created and
               immediately deleted. */
            i = Py_SIZE(op);
            while (--i >= 0) {
                Py_XDECREF(op->ob_item[i]);
            }
            PyMem_FREE(op->ob_item);
        }
        if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))
            free_list[numfree++] = op;
        else
            Py_TYPE(op)->tp_free((PyObject *)op);
        Py_TRASHCAN_SAFE_END(op)
    }
    
    • 其实是在销毁一个PyListObject对象的时候进行了缓存处理
      • 第一步:销毁了PyListObject对象维护的PyObject *列表的内存。
      • 第二步:如果缓冲池大小没有超过80,将PyListObject对象本身加入到free_list缓冲池中,以供下次使用,;否则直接释放内存给系统。
    • 思考:为什么不对Pyobject *对象列表进行缓存呢?
      • 如果真的想对PyObject *对象列表进行缓存,是完全可以的。
      • 但是缓存的PyObject *对象内存只能PyListObject对象来使用,其他对象无法使用。
      • 所以虽然这样做可以节省创建元素列表时的开销,但是收益不是很高,而且会过多消耗系统内存
      • 所以Python采用将元素列表内存归还给系统堆,以时间换取空间。
    • PyListObject对象缓冲池

    欢迎关注微信公众号(coder0x00)或扫描下方二维码关注,我们将持续搜寻程序员必备基础技能包提供给大家。


    相关文章

      网友评论

          本文标题:【Python】列表

          本文链接:https://www.haomeiwen.com/subject/baoxfqtx.html