列表切片賦值給另一個變數,淺拷貝原理解析

darklight發表於2019-09-23

把一個列表的切片賦值給另一個變數,Python內部發生了什麼?

如下所示,建立列表a,然後把它的一段切片賦值給b:

>>> 
>>> a = [2, "Hello", 5, [3,4,7],{1:2}]    # 建立列表a
>>> 
>>> a
[2, 'Hello', 5, [3, 4, 7], {1: 2}]
>>> 
>>> 
>>> a[2:5]          # 訪問a的切片
[5, [3, 4, 7], {1: 2}]
>>> 
>>> 
>>> b = a[2:5]    # 把a切片賦值給b
>>> 
>>> b
[5, [3, 4, 7], {1: 2}]
>>> 

列表在C語言當中是一個PyListObject結構體,這個結構體當中有一個成員ob_item,它是一個指標,指向一塊記憶體,而這塊記憶體儲存了一系列指標,每一個指標分別指向列表的每一個成員。如下圖所示:
列表切片賦值給另一個變數的原理解析

在訪問a的切片時,會新建立一個列表物件,也就是PyListObject結構體,併為它的ob_item指標分配一塊記憶體,然後把a中的ob_item所指向的,切片所需的內容拷貝過去。

把切片賦值給b,實際上就是讓b指向了這一個新建立的列表物件。

由於ob_item所指向的記憶體所儲存的是一系列指標,拷貝時僅拷貝了這些指標,所以b和部分a成員實際上指向相同的物件,這些物件並沒有重新建立一份,這也就是所謂的淺拷貝。下圖所示紅色部分就是拷貝的內容。

列表切片賦值給另一個變數的原理解析

C程式碼:

把切片賦值給b時,呼叫瞭如下所示函式建立新列表,並根據傳入的引數把對應的切片內容拷貝到新列表當中。

static PyObject *
list_slice(PyListObject *a, Py_ssize_t ilow, Py_ssize_t ihigh)
{
    PyListObject *np;
    PyObject **src, **dest;
    Py_ssize_t i, len;
    len = ihigh - ilow;
    np = (PyListObject *) list_new_prealloc(len);   // 申請了一塊記憶體用來儲存新列表
    if (np == NULL)
        return NULL;

    src = a->ob_item + ilow;
    dest = np->ob_item;
    for (i = 0; i < len; i++) {        // 將列表a的ob_item中對應的值複製到新列表中
        PyObject *v = src[i];
        Py_INCREF(v);
        dest[i] = v;
    }
    Py_SIZE(np) = len;
    return (PyObject *)np;
}

上面程式碼中的申請記憶體的函式如下所示:

static PyObject *
list_new_prealloc(Py_ssize_t size)
{
    PyListObject *op = (PyListObject *) PyList_New(0);  // 引數0是指PyListObject中的ob_item成員不申請記憶體
    if (size == 0 || op == NULL) {                      // 它在下面程式碼中單獨申請
        return (PyObject *) op;
    }
    assert(op->ob_item == NULL);
    op->ob_item = PyMem_New(PyObject *, size); // 為ob_item申請記憶體
    if (op->ob_item == NULL) {
        Py_DECREF(op);
        return PyErr_NoMemory();
    }
    op->allocated = size;
    return (PyObject *) op;
}

相關文章