目錄
列表的結構
列表操作函數原始碼分析
建立列表
列表append 函數
列表的擴容機制
列表的插入函數insert
列表的刪除函數remove
列表的統計函數 count
列表的拷貝函數copy
列表的清空函数 clear
列表反转函数 reverse
首頁 後端開發 Python教學 Python虛擬機器中列表的實作原理是什麼

Python虛擬機器中列表的實作原理是什麼

May 10, 2023 pm 02:58 PM
python

    列表的結構

    在cpython 實作的python 虛擬機器當中,下面就是cpython 內部列表實作的原始碼:

    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;
     
    #define PyObject_VAR_HEAD      PyVarObject ob_base;
    typedef struct {
        PyObject ob_base;
        Py_ssize_t ob_size; /* Number of items in variable part */
    } PyVarObject;
     
    typedef struct _object {
        _PyObject_HEAD_EXTRA // 这个宏定义为空
        Py_ssize_t ob_refcnt;
        struct _typeobject *ob_type;
    } PyObject;
    登入後複製

    將上面的結構體展開之後,PyListObject 的結構大致如下所示:

    Python虛擬機器中列表的實作原理是什麼

    #現在來解釋一下上面的各個欄位的意義:

    • Py_ssize_t,一個整數資料型別。

    • ob_refcnt,表示物件的參考記數的個數,這個對於垃圾回收很有用處,後面我們分析虛擬機器中垃圾回收部分在深入分析。

    • ob_type,表示這個物件的資料型別是什麼,在python 當中有時候需要對資料的資料型別進行判斷例如isinstance, type 這兩個關鍵字就會使用到這個字段。

    • ob_size,這個欄位表示這個清單當中有多少個元素。

    • ob_item,這是一個指針,指向真正保存python 物件資料的位址,大致的記憶體他們之間大致的記憶體佈局如下所示:

    Python虛擬機器中列表的實作原理是什麼

    • allocated,這個表示在進行記憶體分配的時候,總共分配了多少(PyObject *) ,真實分配的記憶體空間為 allocated * sizeof(PyObject *)

    列表操作函數原始碼分析

    建立列表

    #首先需要了解的是在python 虛擬機器內部為列表創建了一個數組,所有的創建的被釋放的內存空間,並不會直接進行釋放而是會將這些內存空間的首地址保存到這個數組當中,好讓下一次申請創建新的列表的時候不需要再申請內存空間,而是直接將之前需要釋放的記憶體直接進行重複使用即可。

    /* Empty list reuse scheme to save calls to malloc and free */
    #ifndef PyList_MAXFREELIST
    #define PyList_MAXFREELIST 80
    #endif
    static PyListObject *free_list[PyList_MAXFREELIST];
    static int numfree = 0;
    登入後複製
    • free_list,保存被釋放的記憶體空間的首位址。

    • numfree,目前 free_list 當中有多少位址是可以使用的,事實上是 free_list 前 numfree 個首位址是可以被使用的。

    建立鍊錶的程式碼如下所示(為了精簡刪除了一些程式碼只保留核心部分):

    PyObject *
    PyList_New(Py_ssize_t size)
    {
        PyListObject *op;
        size_t nbytes;
     
        /* 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 *);
      // 如果 numfree 不等于 0 那么说明现在 free_list 有之前使用被释放的内存空间直接使用这部分即可
        if (numfree) {
            numfree--;
            op = free_list[numfree]; // 将对应的首地址返回
            _Py_NewReference((PyObject *)op); // 这条语句的含义是将 op 这个对象的 reference count 设置成 1
        } else {
          // 如果没有空闲的内存空间 那么就需要申请内存空间 这个函数也会对对象的 reference count 进行初始化 设置成 1
            op = PyObject_GC_New(PyListObject, &PyList_Type);
            if (op == NULL)
                return NULL;
        }
      /* 下面是申请列表对象当中的 ob_item 申请内存空间,上面只是给列表本身申请内存空间,但是列表当中有许多元素
      	保存这些元素也是需要内存空间的 下面便是给这些对象申请内存空间
      */
        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();
            }
          // 对元素进行初始化操作 全部赋值成 0
            memset(op->ob_item, 0, nbytes);
        }
      // Py_SIZE 是一个宏
        Py_SIZE(op) = size; // 这条语句会被展开成 (PyVarObject*)(ob))->ob_size = size
      // 分配数组的元素个数是 size
        op->allocated = size;
      // 下面这条语句对于垃圾回收比较重要 主要作用就是将这个列表对象加入到垃圾回收的链表当中
      // 后面如果这个对象的 reference count 变成 0 或者其他情况 就可以进行垃圾回收了
        _PyObject_GC_TRACK(op);
        return (PyObject *) op;
    }
    登入後複製

    在cpython 當中,建立鍊錶的字節碼為BUILD_LIST,我們可以在檔案ceval.c 當中找到對應的字節碼對應的執行步驟:

    TARGET(BUILD_LIST) {
        PyObject *list =  PyList_New(oparg);
        if (list == NULL)
            goto error;
        while (--oparg >= 0) {
            PyObject *item = POP();
            PyList_SET_ITEM(list, oparg, item);
        }
        PUSH(list);
        DISPATCH();
    }
    登入後複製

    從上面BUILD_LIST 字節碼對應的解釋步驟可以知道,在解釋執行字節碼BUILD_LIST 的時候確實呼叫了函數PyList_New 建立一個新的列表。

    列表append 函數

    static PyObject *
    // 这个函数的传入参数是列表本身 self 需要 append 的元素为 v
      // 也就是将对象 v 加入到列表 self 当中
    listappend(PyListObject *self, PyObject *v)
    {
        if (app1(self, v) == 0)
            Py_RETURN_NONE;
        return NULL;
    }
     
    static int
    app1(PyListObject *self, PyObject *v)
    {
      // PyList_GET_SIZE(self) 展开之后为 ((PyVarObject*)(self))->ob_size
        Py_ssize_t n = PyList_GET_SIZE(self);
     
        assert (v != NULL);
      // 如果元素的个数已经等于允许的最大的元素个数 就报错
        if (n == PY_SSIZE_T_MAX) {
            PyErr_SetString(PyExc_OverflowError,
                "cannot add more objects to list");
            return -1;
        }
    	// 下面的函数 list_resize 会保存 ob_item 指向的位置能够容纳最少 n+1 个元素(PyObject *)
      // 如果容量不够就会进行扩容操作
        if (list_resize(self, n+1) == -1)
            return -1;
    		
      // 将对象 v 的 reference count 加一 因为列表当中使用了一次这个对象 所以对象的引用计数需要进行加一操作
        Py_INCREF(v);
        PyList_SET_ITEM(self, n, v); // 宏展开之后 ((PyListObject *)(op))->ob_item[i] = 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.
        */
      // 如果列表已经分配的元素个数大于需求个数 newsize 的就直接返回不需要进行扩容
        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 表示新增的数组大小
        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 这是一个宏定义 会申请 new_allocated 个数元素并且将原来数组的元素拷贝到新的数组当中
            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;
    }
    登入後複製

    在上面的擴容機制下,數組的大小變化大致如下所示:

    newsize&asymp ;size⋅(size 1)1/8

    Python虛擬機器中列表的實作原理是什麼

    列表的插入函數insert

    在列表當中插入一個資料比較簡單,只需要將插入位置和其後面的元素往後移動一個位置即可,整個過程如下所示:

    Python虛擬機器中列表的實作原理是什麼

    #在cpython 當中列表的插入函數的實作如下所示:

    • 參數op 表示往哪個鍊錶當中插入元素。

    • 參數 where 表示在鍊錶的哪個位置插入元素。

    • 參數 newitem 表示新插入的元素。

    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;
        }
      // 确保列表能够容纳 n + 1 个元素
        if (list_resize(self, n+1) == -1)
            return -1;
      // 这里是 python 的一个小 trick 就是下标能够有负数的原理
        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];
      // 因为链表应用的对象,因此对象的 reference count 需要进行加一操作
        Py_INCREF(v);
      // 在列表当中保存对象 v 
        items[where] = v;
        return 0;
    }
    登入後複製

    列表的刪除函數remove

    對於陣列ob_item 來說,刪除一個元素就需要將這個元素後面的元素往前移動,因此整個過程如下所顯示:

    Python虛擬機器中列表的實作原理是什麼

    static PyObject *
    listremove(PyListObject *self, PyObject *v)
    {
        Py_ssize_t i;
      	// 编译数组 ob_item 查找和对象 v 相等的元素并且将其删除
        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;
        }
      	// 如果没有找到这个元素就进行报错处理 在下面有一个例子重新编译 python 解释器 将这个错误内容修改的例子
        PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
        return NULL;
    }
    登入後複製

    執行的python 程式內容為:

    data = []
    data.remove(1)
    登入後複製

    以下是整個修改內容和報錯結果:

    Python虛擬機器中列表的實作原理是什麼

    #從上面的結果我們可以看到的是,我們修改的錯誤訊息正確地印出來了。

    列表的統計函數 count

    這個函數的主要功能是統計列表 self 當中有多少個元素和 v 相等。

    static PyObject *
    listcount(PyListObject *self, PyObject *v)
    {
        Py_ssize_t count = 0;
        Py_ssize_t i;
     
        for (i = 0; i < Py_SIZE(self); i++) {
            int cmp = PyObject_RichCompareBool(self->ob_item[i], v, Py_EQ);
          // 如果相等则将 count 进行加一操作
            if (cmp > 0)
                count++;
          // 如果出现错误就返回 NULL
            else if (cmp < 0)
                return NULL;
        }
      // 将一个 Py_ssize_t 的变量变成 python 当中的对象
        return PyLong_FromSsize_t(count);
    }
    登入後複製

    列表的拷貝函數copy

    這是列表的淺拷貝函數,它只拷貝了真實python 對象的指針,並沒有拷貝真實的python 對象,從下面的程式碼可以知道列表的拷貝是淺拷貝,當b 對列表當中的元素進行修改時,列表a 當中的元素也改變了。如果需要進行深拷貝可以使用 copy 模組當中的 deepcopy 函數。

    >>> a = [1, 2, [3, 4]]
    >>> b = a.copy()
    >>> b[2][1] = 5
    >>> b
    [1, 2, [3, 5]]
    登入後複製

    copy 函数对应的源代码(listcopy)如下所示:

    static PyObject *
    listcopy(PyListObject *self)
    {
        return list_slice(self, 0, Py_SIZE(self));
    }
     
    static PyObject *
    list_slice(PyListObject *a, Py_ssize_t ilow, Py_ssize_t ihigh)
    {
      // Py_SIZE(a) 返回列表 a 当中元素的个数(注意不是数组的长度 allocated)
        PyListObject *np;
        PyObject **src, **dest;
        Py_ssize_t i, len;
        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;
      // 可以看到这里循环拷贝的是指向真实 python 对象的指针 并不是真实的对象
        for (i = 0; i < len; i++) {
            PyObject *v = src[i];
          // 同样的因为并没有创建新的对象,但是这个对象被新的列表使用到啦 因此他的 reference count 需要进行加一操作 Py_INCREF(v) 的作用:将对象 v 的 reference count 加一
            Py_INCREF(v);
            dest[i] = v;
        }
        return (PyObject *)np;
    }
    登入後複製

    下图就是使用 a.copy() 浅拷贝的时候,内存的布局的示意图,可以看到列表指向的对象数组发生了变化,但是数组中元素指向的 python 对象并没有发生变化。

    Python虛擬機器中列表的實作原理是什麼

    下面是对列表对象进行深拷贝的时候内存的大致示意图,可以看到数组指向的 python 对象也是不一样的。

    Python虛擬機器中列表的實作原理是什麼

    列表的清空函数 clear

    当我们在使用 list.clear() 的时候会调用下面这个函数。清空列表需要注意的就是将表示列表当中元素个数的 ob_size 字段设置成 0 ,同时将列表当中所有的对象的 reference count 设置进行 -1 操作,这个操作是通过宏 Py_XDECREF 实现的,这个宏还会做另外一件事就是如果这个对象的引用计数变成 0 了,那么就会直接释放他的内存。

    static PyObject *
    listclear(PyListObject *self)
    {
        list_clear(self);
        Py_RETURN_NONE;
    }
     
    static int
    list_clear(PyListObject *a)
    {
        Py_ssize_t i;
        PyObject **item = a->ob_item;
        if (item != NULL) {
            /* Because XDECREF can recursively invoke operations on
               this list, we make it empty first. */
            i = Py_SIZE(a);
            Py_SIZE(a) = 0;
            a->ob_item = NULL;
            a->allocated = 0;
            while (--i >= 0) {
                Py_XDECREF(item[i]);
            }
            PyMem_FREE(item);
        }
        /* Never fails; the return value can be ignored.
           Note that there is no guarantee that the list is actually empty
           at this point, because XDECREF may have populated it again! */
        return 0;
    }
    登入後複製

    列表反转函数 reverse

    在 python 当中如果我们想要反转类表当中的内容的话,就会使用这个函数 reverse 。

    >>> a = [i for i in range(10)]
    >>> a.reverse()
    >>> a
    [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
    登入後複製

    其对应的源程序如下所示:

    static PyObject *
    listreverse(PyListObject *self)
    {
        if (Py_SIZE(self) > 1)
            reverse_slice(self->ob_item, self->ob_item + Py_SIZE(self));
        Py_RETURN_NONE;
    }
     
    static void
    reverse_slice(PyObject **lo, PyObject **hi)
    {
        assert(lo && hi);
     
        --hi;
        while (lo < hi) {
            PyObject *t = *lo;
            *lo = *hi;
            *hi = t;
            ++lo;
            --hi;
        }
    }
    登入後複製

    上面的源程序还是比较容易理解的,给 reverse_slice 传递的参数就是保存数据的数组的首尾地址,然后不断的将首尾数据进行交换(其实是交换指针指向的地址)。

    以上是Python虛擬機器中列表的實作原理是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

    熱AI工具

    Undresser.AI Undress

    Undresser.AI Undress

    人工智慧驅動的應用程序,用於創建逼真的裸體照片

    AI Clothes Remover

    AI Clothes Remover

    用於從照片中去除衣服的線上人工智慧工具。

    Undress AI Tool

    Undress AI Tool

    免費脫衣圖片

    Clothoff.io

    Clothoff.io

    AI脫衣器

    Video Face Swap

    Video Face Swap

    使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

    熱工具

    記事本++7.3.1

    記事本++7.3.1

    好用且免費的程式碼編輯器

    SublimeText3漢化版

    SublimeText3漢化版

    中文版,非常好用

    禪工作室 13.0.1

    禪工作室 13.0.1

    強大的PHP整合開發環境

    Dreamweaver CS6

    Dreamweaver CS6

    視覺化網頁開發工具

    SublimeText3 Mac版

    SublimeText3 Mac版

    神級程式碼編輯軟體(SublimeText3)

    PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

    PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

    在PHP和Python之間進行選擇:指南 在PHP和Python之間進行選擇:指南 Apr 18, 2025 am 12:24 AM

    PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

    Python vs. JavaScript:學習曲線和易用性 Python vs. JavaScript:學習曲線和易用性 Apr 16, 2025 am 12:12 AM

    Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

    PHP和Python:深入了解他們的歷史 PHP和Python:深入了解他們的歷史 Apr 18, 2025 am 12:25 AM

    PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

    vs code 可以在 Windows 8 中運行嗎 vs code 可以在 Windows 8 中運行嗎 Apr 15, 2025 pm 07:24 PM

    VS Code可以在Windows 8上運行,但體驗可能不佳。首先確保系統已更新到最新補丁,然後下載與系統架構匹配的VS Code安裝包,按照提示安裝。安裝後,注意某些擴展程序可能與Windows 8不兼容,需要尋找替代擴展或在虛擬機中使用更新的Windows系統。安裝必要的擴展,檢查是否正常工作。儘管VS Code在Windows 8上可行,但建議升級到更新的Windows系統以獲得更好的開發體驗和安全保障。

    visual studio code 可以用於 python 嗎 visual studio code 可以用於 python 嗎 Apr 15, 2025 pm 08:18 PM

    VS Code 可用於編寫 Python,並提供許多功能,使其成為開發 Python 應用程序的理想工具。它允許用戶:安裝 Python 擴展,以獲得代碼補全、語法高亮和調試等功能。使用調試器逐步跟踪代碼,查找和修復錯誤。集成 Git,進行版本控制。使用代碼格式化工具,保持代碼一致性。使用 Linting 工具,提前發現潛在問題。

    notepad 怎麼運行python notepad 怎麼運行python Apr 16, 2025 pm 07:33 PM

    在 Notepad 中運行 Python 代碼需要安裝 Python 可執行文件和 NppExec 插件。安裝 Python 並為其添加 PATH 後,在 NppExec 插件中配置命令為“python”、參數為“{CURRENT_DIRECTORY}{FILE_NAME}”,即可在 Notepad 中通過快捷鍵“F6”運行 Python 代碼。

    vscode 擴展是否是惡意的 vscode 擴展是否是惡意的 Apr 15, 2025 pm 07:57 PM

    VS Code 擴展存在惡意風險,例如隱藏惡意代碼、利用漏洞、偽裝成合法擴展。識別惡意擴展的方法包括:檢查發布者、閱讀評論、檢查代碼、謹慎安裝。安全措施還包括:安全意識、良好習慣、定期更新和殺毒軟件。

    See all articles