1. 程式人生 > >Python列表物件實現原理

Python列表物件實現原理

Python中的列表基於PyListObject實現,列表支援元素的插入、刪除、更新操作,因此PyListObject是一個變長物件(列表的長度隨著元素的增加和刪除而變長和變短),同時它還是一個可變物件(列表中的元素根據列表的操作而發生變化,記憶體大小動態的變化),PyListObject的定義:

typedef struct {
    # 列表物件引用計數
    int ob_refcnt;  
    # 列表型別物件      
    struct _typeobject *ob_type;
    # 列表元素的長度
    int ob_size; /* Number
of items in variable part */ # 真正存放列表元素容器的指標,list[0] 就是 ob_item[0] PyObject **ob_item; # 當前列表可容納的元素大小 Py_ssize_t allocated; } PyListObject;

咋一看PyListObject物件的定義非常簡單,除了通用物件都有的引用計數(ob_refcnt)、型別資訊(ob_type),以及變長物件的長度(ob_size)之外,剩下的只有ob_item,和allocated,ob_item是真正存放列表元素容器的指標,專門有一塊記憶體用來儲存列表元素,這塊記憶體的大小就是allocated所能容納的空間。alloocated是列表所能容納的元素大小,而且滿足條件:

  • 0 <= ob_size <= allocated
  • len(list) == ob_size
  • ob_item == NULL 時 ob_size == allocated == 0

pylistobject

列表物件的建立

PylistObject物件的是通過函式PyList_New建立而成,接收引數size,該引數用於指定列表物件所能容納的最大元素個數。

// 列表緩衝池, PyList_MAXFREELIST80
static PyListObject *free_list[PyList_MAXFREELIST];
//緩衝池當前大小
static int numfree = 0;

PyObject
*PyList_New(Py_ssize_t size) { PyListObject *op; //列表物件 size_t nbytes; //建立列表物件需要分配的記憶體大小 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); } else { op = PyObject_GC_New(PyListObject, &PyList_Type); if (op == NULL) return NULL; } 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); } # 設定ob_size Py_SIZE(op) = size; op->allocated = size; _PyObject_GC_TRACK(op); return (PyObject *) op; }

建立過程大致是:

  1. 檢查size引數是否有效,如果小於0,直接返回NULL,建立失敗
  2. 檢查size引數是否超出Python所能接受的大小,如果大於PY_SIZE_MAX(64位機器為8位元組,在32位機器為4位元組),記憶體溢位。
  3. 檢查緩衝池free_list是否有可用的物件,有則直接從緩衝池中使用,沒有則建立新的PyListObject,分配記憶體。
  4. 初始化ob_item中的元素的值為Null
  5. 設定PyListObject的allocated和ob_size。

PyListObject物件的緩衝池

free_list是PyListObject物件的緩衝池,其大小為80,那麼PyListObject物件是什麼時候加入到緩衝池free_list的呢?答案在list_dealloc方法中:

static void
list_dealloc(PyListObject *op)
{
    Py_ssize_t i;
    PyObject_GC_UnTrack(op);
    Py_TRASHCAN_SAFE_BEGIN(op)
    if (
        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物件被銷燬的時候,首先將列表中所有元素的引用計數減一,然後釋放ob_item佔用的記憶體,只要緩衝池空間還沒滿,那麼就把該PyListObject加入到緩衝池中(此時PyListObject佔用的記憶體並不會正真正回收給系統,下次建立PyListObject優先從緩衝池中獲取PyListObject),否則釋放PyListObject物件的記憶體空間。

列表元素插入

設定列表某個位置的值時,如“list[1]=0”,列表的記憶體結構並不會發生變化,而往列表中插入元素時會改變列表的記憶體結構:

static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
    // n是列表元素長度
    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;
}

相比設定某個列表位置的值來說,插入操作要多一次PyListObject容量大小的調整,邏輯是list_resize,其次是挪動where之後的元素位置。

// newsize 列表新的長度
static int  
list_resize(PyListObject *self, Py_ssize_t newsize)
{
    PyObject **items;
    size_t new_allocated;
    Py_ssize_t allocated = self->allocated;


    if (allocated >= newsize && newsize >= (allocated >> 1)) {
        assert(self->ob_item != NULL || newsize == 0);
        Py_SIZE(self) = newsize;
        return 0;
    }

    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;
}

滿足 allocated >= newsize && newsize >= (allocated /2)時,簡單改變list的元素長度,PyListObject物件不會重新分配記憶體空間,否則重新分配記憶體空間,如果newsize<allocated/2,那麼會減縮記憶體空間,如果newsize>allocated,就會擴大記憶體空間。當newsize==0時記憶體空間將縮減為0。 !python_list_resize

總結

  • PyListObject緩衝池的建立發生在列表銷燬的時候。
  • PyListObject物件的建立分兩步:先建立PyListObject物件,然後初始化元素列表為NULL。
  • PyListObject物件的銷燬分兩步:先銷燬PyListObject物件中的元素列表,然後銷燬PyListObject本身。
  • PyListObject物件記憶體的佔用空間會根據列表長度的變化而調整。

參考:


關注公眾號「Python之禪」(id:vttalk)獲取最新文章 python之禪