精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

列表都有哪些自定義方法,它們是怎么實現(xiàn)的?

開發(fā) 前端
到此關(guān)于列表的內(nèi)容就介紹完了,作為 Python 中的萬能容器,我們可以自由地添加、修改和刪除元素。但在使用的時候要了解它的底層結(jié)構(gòu)以及元素是如何存儲的,應(yīng)該在什么場景下使用列表,它的每個方法的時間復(fù)雜度是多少。

楔子

上一篇文章我們介紹了列表作為序列型對象支持的方法,但列表還有很多的自定義方法。作為一名優(yōu)秀的 Python 工程師,我們必須要知道這些方法的實現(xiàn)過程,以及相應(yīng)的時間復(fù)雜度。

實例對象能夠調(diào)用的方法都定義在類型對象中,類型對象無一例外都是 PyTypeObject 結(jié)構(gòu)體實例,該結(jié)構(gòu)體有一個 tp_methods 字段,負(fù)責(zé)維護實例對象能夠調(diào)用的方法。

// Include/pytypedefs.h
typedef struct _typeobject PyTypeObject;
// Include/cpython/object.h
struct _typeobject {
    // ...
    struct PyMethodDef *tp_methods;
    // ...    
}
/* tp_methods 指向 PyMethodDef 結(jié)構(gòu)體類型的數(shù)組
 * 所以一個 PyMethodDef 結(jié)構(gòu)體實例,就是 Python 實例對象能夠調(diào)用的一個方法
 */

// Include/methodobject.h
struct PyMethodDef {
    // 暴露給 Python 的方法名
    const char  *ml_name;   
    // 承載了具體邏輯的 C 函數(shù)
    PyCFunction ml_meth;  
    // 指示函數(shù)的調(diào)用方式和傳遞參數(shù)的方式,比如 
    /* METH_NOARGS: 表示函數(shù)不接收任何參數(shù)
     * METH_O: 函數(shù)只接收一個參數(shù)
     * METH_VARARGS: 函數(shù)支持以元組的形式接收多個位置參數(shù)
     * METH_KEYWORDS: 函數(shù)支持關(guān)鍵字參數(shù)
     * METH_CLASS: 函數(shù)是一個類方法,等價于 Python 里的 @classmethod
     * METH_STATIC: 函數(shù)是一個靜態(tài)方法,即 @staticmethod
     * METH_FASTCALL: 函數(shù)使用優(yōu)化的快速調(diào)用協(xié)議,Python 3.7 及以上版本可用
                      傳遞參數(shù)時使用 C 數(shù)組,而不是 Python 元組
     * METH_COEXIST: 如果希望存在兩個同名函數(shù),但類和實例分別調(diào)用不同的函數(shù)
                     那么便可以指定 METH_COEXIST
     */
    int         ml_flags;   
    // 函數(shù)的 docstring
    const char  *ml_doc;    
};

而 list 在底層對應(yīng) PyList_Type,它的 tp_methods 字段被賦值為 list_method。

圖片圖片

里面定義了列表可以調(diào)用的方法,我們隨便看一個,比如 append。

// Objects/clinic/listobject.c.h
#define LIST_APPEND_METHODDEF    \
    {"append", (PyCFunction)list_append, \
     METH_O, list_append__doc__},
// 列表調(diào)用時使用的方法名為 append
// 內(nèi)部會執(zhí)行 list_append 函數(shù)
// 只接收一個參數(shù)

相信當(dāng)你以后想查看某個對象的方法的底層實現(xiàn)時,已經(jīng)知道該怎么定位了,下面我們就來看看這些方法的實現(xiàn)過程。

append:在尾部追加元素

append 方法在底層對應(yīng) list_append,我們上面已經(jīng)看到了,那么它的實現(xiàn)細(xì)節(jié)是怎樣的呢。

// Objects/listobject.c
static PyObject *
list_append(PyListObject *self, PyObject *object)
{
    // 調(diào)用 _PyList_AppendTakeRef 添加元素
    if (_PyList_AppendTakeRef(self, Py_NewRef(object)) < 0) {
        return NULL;
    }
    // 該函數(shù)返回 None
    Py_RETURN_NONE;
}


// Include/internal/pycore_list.h
static inline int
_PyList_AppendTakeRef(PyListObject *self, PyObject *newitem)
{
    assert(self != NULL && newitem != NULL);
    assert(PyList_Check(self));
    // 獲取列表的長度
    Py_ssize_t len = PyList_GET_SIZE(self);
    // 獲取列表的容量,即底層數(shù)組的長度
    Py_ssize_t allocated = self->allocated;
    assert((size_t)len + 1 < PY_SSIZE_T_MAX);
    // 如果長度沒有達到容量
    if (allocated > len) {
        // 那么將 newitem 設(shè)置在 ob_item 中索引為 len 的位置
        PyList_SET_ITEM(self, len, newitem);
        // 將列表的 ob_size 加 1
        Py_SET_SIZE(self, len + 1);
        return 0;
    }
    // 如果長度達到容量了,那么會調(diào)用下面這個函數(shù)
    return _PyList_AppendTakeRefListResize(self, newitem);
}

// Objects/listobject.c
int
_PyList_AppendTakeRefListResize(PyListObject *self, PyObject *newitem)
{
    Py_ssize_t len = PyList_GET_SIZE(self);
    assert(self->allocated == -1 || self->allocated == len);
    // 邏輯是類似的,但這里會多一步擴容操作
    if (list_resize(self, len + 1) < 0) {
        Py_DECREF(newitem);
        return -1;
    }
    PyList_SET_ITEM(self, len, newitem);
    return 0;
}

所謂往尾部追加元素,本質(zhì)上就是將元素設(shè)置在索引為 len 的位置。

insert:在任意位置插入元素

接下來是列表的 insert 方法。

// Objects/clinic/listobject.c.h
#define LIST_INSERT_METHODDEF    \
    {"insert", _PyCFunction_CAST(list_insert), \
     METH_FASTCALL, list_insert__doc__},

它由 list_insert 函數(shù)負(fù)責(zé)實現(xiàn)。

// Objects/listobject.c
static PyObject *
list_insert(PyListObject *self, PyObject *const *args, Py_ssize_t nargs)
{   
    // 函數(shù)返回值,但只會返回 None
    PyObject *return_value = NULL;
    // 插入的位置
    Py_ssize_t index;
    // 插入的元素
    PyObject *object;
    // insert 方法精確接收兩個參數(shù)
    if (!_PyArg_CheckPositional("insert", nargs, 2, 2)) {
        goto exit;
    }
    {   
        // 參數(shù) args 是一個元組,里面包含了插入位置和元素
        Py_ssize_t ival = -1;
        // 插入位置,對象必須實現(xiàn) __index__
        PyObject *iobj = _PyNumber_Index(args[0]);
        if (iobj != NULL) {
            // 轉(zhuǎn)成 Py_ssize_t
            ival = PyLong_AsSsize_t(iobj);
            Py_DECREF(iobj);
        }
        if (ival == -1 && PyErr_Occurred()) {
            goto exit;
        }
        // 賦值給 index
        index = ival;
    }
    // 拿到待插入的元素
    object = args[1];
    // 調(diào)用 list_insert_impl 執(zhí)行元素插入邏輯
    return_value = list_insert_impl(self, index, object);

exit:
    // 雖然這里返回了 return_value,但我們知道 insert 方法是沒有返回值的
    // 或者說返回值為 None,所以上面的 list_insert_impl 一定返回了 None
    return return_value;
}

static PyObject *
list_insert_impl(PyListObject *self, Py_ssize_t index, PyObject *object)
{
    // 調(diào)用 ins1 插入元素,插入成功之后返回 None
    if (ins1(self, index, object) == 0)
        Py_RETURN_NONE;
    return NULL;
}

static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
    // 初始化循環(huán)變量 i
    // n 為列表長度
    Py_ssize_t i, n = Py_SIZE(self);
    // 指向 ob_item 數(shù)組的首元素
    PyObject **items;
    if (v == NULL) {
        PyErr_BadInternalCall();
        return -1;
    }

    assert((size_t)n + 1 < PY_SSIZE_T_MAX);
    // 只要涉及到元素個數(shù)的改變,比如添加和刪除元素
    // 都會先調(diào)用 list_resize,在里面檢測一下容量
    // 比如這里,如果發(fā)現(xiàn) (容量 >= n + 1) && (容量 / 2 <= n + 1)
    // 那么說明容量目前是合理的,不需要做任何的擴容或縮容操作(如果條件不滿足則需要)
    // 然后將列表的 ob_size 修改為 n + 1,直接返回
    if (list_resize(self, n+1) < 0)
        return -1;
    // 判斷插入位置,如果 where 小于 0,那么加上列表長度
    if (where < 0) {
        where += n;
        // 加上列表長度之后如果還小于 0,那么讓其等于 0
        if (where < 0)
            where = 0;
    }
    // 如果插入位置大于列表長度 n,那么讓其等于 n
    // 此時相當(dāng)于 append
    if (where > n)
        where = n;
    items = self->ob_item;
    // 將 where 以及之后的元素依次向右移動一個位置
    for (i = n; --i >= where; )
        items[i+1] = items[i];
    // 將待插入元素 v 的引用計數(shù)加 1,并設(shè)置在底層數(shù)組中索引為 where 的位置
    items[where] = Py_NewRef(v);
    return 0;
}

以上就是 insert 函數(shù)的底層邏輯,列表在插入數(shù)據(jù)的時候是非常靈活的,不管你在什么位置插入,都是合法的。它會自己調(diào)整,在確定待插入位置 where 之后,會將 where 以及之后的所有元素都向后挪動一個位置,空出來的地方設(shè)置為待插入的值。

另外我們看到 append 和 insert 其實非常像,都是基于索引設(shè)置元素。只不過對于 append 來說,索引就是列表長度,而對于 insert 來說,索引是由外界指定的,但函數(shù)內(nèi)部會進行邊界調(diào)整。

并且由于 insert 會涉及元素的移動,所以它的時間復(fù)雜度是 O(n),而 append 則不會,所以它的時間復(fù)雜度是 O(1)。當(dāng)然在極端情況下(發(fā)生擴容),append 也會退化成 O(n),只不過這個過程不會頻繁發(fā)生,所以 append 的復(fù)雜度仍然是 O(1) 的。

pop:從尾部彈出一個元素

pop 默認(rèn)會從尾部彈出一個元素,當(dāng)然我們也可以指定索引,彈出指定索引對應(yīng)的元素。如果不指定索引,那么默認(rèn)是 -1。

// Objects/clinic/listobject.c.h
#define LIST_POP_METHODDEF    \
    {"pop", _PyCFunction_CAST(list_pop), \
    METH_FASTCALL, list_pop__doc__},

它由 list_pop 函數(shù)負(fù)責(zé)實現(xiàn)。

// Objects/clinic/listobject.c.h
static PyObject *
list_pop(PyListObject *self, PyObject *const *args, Py_ssize_t nargs)
{
    // 返回值
    PyObject *return_value = NULL;
    Py_ssize_t index = -1;
    // pop 接收 0 ~ 1 個參數(shù)
    if (!_PyArg_CheckPositional("pop", nargs, 0, 1)) {
        goto exit;
    }
    // 如果參數(shù)個數(shù)小于 1,說白了就是沒有傳參,直接跳轉(zhuǎn)到 skip_optional 標(biāo)簽
    if (nargs < 1) {
        goto skip_optional;
    }
    {
        // 如果傳參了,那么拿到指定的索引
        Py_ssize_t ival = -1;
        PyObject *iobj = _PyNumber_Index(args[0]);
        if (iobj != NULL) {
            ival = PyLong_AsSsize_t(iobj);
            Py_DECREF(iobj);
        }
        if (ival == -1 && PyErr_Occurred()) {
            goto exit;
        }
        index = ival;
    }
skip_optional:
    // 將列表和 index 作為參數(shù)傳進去,如果不指定索引,那么 index 默認(rèn)為 -1
    return_value = list_pop_impl(self, index);

exit:
    // 返回彈出的元素
    return return_value;
}

// Objects/listobject.c
static PyObject *
list_pop_impl(PyListObject *self, Py_ssize_t index)
{
    PyObject *v;
    int status;
    // 如果列表為空,那么拋出 IndexError: pop from empty list
    if (Py_SIZE(self) == 0) {
        /* Special-case most common failure cause */
        PyErr_SetString(PyExc_IndexError, "pop from empty list");
        return NULL;
    }
    // 如果 index 小于 0,那么加上列表長度
    if (index < 0)
        index += Py_SIZE(self);
    // 檢測索引是否合法,如果索引小于 0 或大于等于列表長度
    // 那么拋出 IndexError: pop index out of range
    if (!valid_index(index, Py_SIZE(self))) {
        PyErr_SetString(PyExc_IndexError, "pop index out of range");
        return NULL;
    }
    // 獲取 ob_item 數(shù)組
    PyObject **items = self->ob_item;
    // 拿到索引為 index 的元素,這也是一會兒要返回的元素
    v = items[index];
    // pop 之后,列表的長度(ob_size)要減去 1
    const Py_ssize_t size_after_pop = Py_SIZE(self) - 1;
    // 如果減 1 之后長度為 0,說明列表沒有元素了
    if (size_after_pop == 0) {
        // 增加 v 的引用計數(shù)
        Py_INCREF(v);
        // 將列表清空掉,注意這里的清空不僅僅是將元素清空,因為列表已經(jīng)為空了
        // _list_clear 還會將列表中每個元素指向的對象的引用計數(shù)減 1
        // 并將 ob_item 設(shè)置為 NULL,將 ob_size 和 allocated 設(shè)置為 0
        // 等于讓列表回歸到初始狀態(tài),因為 Python 認(rèn)為當(dāng)列表為空時,
        // 你可能已經(jīng)不用這個列表了,因此不會再讓它占用額外內(nèi)存
        status = _list_clear(self);
    }
    else {
        // 否則檢測彈出的是否是最后一個元素
        // 如果不是,那么要將 index 之后的元素依次向前移動一個位置
        if ((size_after_pop - index) > 0) {
            memmove(&items[index], &items[index+1], 
                    (size_after_pop - index) * sizeof(PyObject *));
        }
        // 檢測容量的大小是否合理,如果合理,則不做任何操作
        // 直接將 ob_size 設(shè)置為 size_after_pop,然后返回即可
        status = list_resize(self, size_after_pop);
        // 可能有人好奇為什么這里沒有增加引用計數(shù)呢?
        // 首先元素從列表中彈出時,應(yīng)該減少引用計數(shù),但它又返回了,所以還要增加引用計數(shù)
        // 因此兩個操作相互抵消,不需要操作引用計數(shù)。但問題來了,為什么上面要執(zhí)行加 1 操作呢
        // 很簡單,因為列表為空時會調(diào)用 _list_clear,將引用計數(shù)減 1,所以在調(diào)用之前要先加 1
    }
    // status 表示 list_resize 的執(zhí)行結(jié)果
    // 在 C 中一般約定,執(zhí)行成功返回 0,失敗返回 -1
    if (status >= 0) {
        // 返回彈出的對象
        return v; 
    }
    else {
        // list resize failed, need to restore
        memmove(&items[index+1], &items[index], 
                (size_after_pop - index)* sizeof(PyObject *));
        items[index] = v;
        return NULL;
    }
}

在 3.8 的時候,pop 彈出元素是這么做的。

list_ass_slice(self, index, index+1, (PyObject *)NULL

等價于 self[index: index + 1] = [],但在 3.12 的時候,調(diào)用了 C 標(biāo)準(zhǔn)庫的 memmove 函數(shù),將 index + 1 以及之后的元素拷貝到 index 的位置。

index:查詢元素首次出現(xiàn)的位置

index 方法可以接收一個元素,然后返回該元素首次出現(xiàn)的位置。當(dāng)然還可以額外指定一個 start 和 end,表示查詢的范圍。

// Objects/clinic/listobject.c.h
#define LIST_INDEX_METHODDEF    \
    {"index", _PyCFunction_CAST(list_index), \
    METH_FASTCALL, list_index__doc__},

它由 list_index 負(fù)責(zé)實現(xiàn)。

// Objects/clinic/listobject.c.h
static PyObject *
list_index(PyListObject *self, PyObject *const *args, Py_ssize_t nargs)
{
    PyObject *return_value = NULL;
    PyObject *value;
    Py_ssize_t start = 0;
    Py_ssize_t stop = PY_SSIZE_T_MAX;
    // index 方法接收 1 ~ 3 個參數(shù)
    if (!_PyArg_CheckPositional("index", nargs, 1, 3)) {
        goto exit;
    }
    // args[0] 表示查找的元素
    value = args[0];
    if (nargs < 2) {
        goto skip_optional;
    }
    // args[1] 表示查找的起始位置
    if (!_PyEval_SliceIndexNotNone(args[1], &start)) {
        goto exit;
    }
    if (nargs < 3) {
        goto skip_optional;
    }
    // args[2] 表示查找的結(jié)束位置
    if (!_PyEval_SliceIndexNotNone(args[2], &stop)) {
        goto exit;
    }
skip_optional:
    // 調(diào)用 list_index_impl 查找元素
    return_value = list_index_impl(self, value, start, stop);

exit:
    // 返回
    return return_value;
}


// Objects/listobject.c
static PyObject *
list_index_impl(PyListObject *self, PyObject *value, Py_ssize_t start,
                Py_ssize_t stop)
{
    Py_ssize_t i;
    // 如果 start 小于 0,那么加上列表長度
    if (start < 0) {
        start += Py_SIZE(self);
        // 如果相加之后還小于 0,那么等于 0
        if (start < 0)
            start = 0;
    }
    // 如果結(jié)束位置小于 0,那么加上列表長度,所以它們都支持負(fù)數(shù)索引
    if (stop < 0) {
        stop += Py_SIZE(self);
        // 如果相加之后還小于 0,那么等于 0
        if (stop < 0)
            stop = 0;
    }
    // 從 start 開始遍歷
    for (i = start; i < stop && i < Py_SIZE(self); i++) {
        // 獲取對應(yīng)元素
        PyObject *obj = self->ob_item[i];
        Py_INCREF(obj);
        // 然后進行比較,這個函數(shù)我們之前說過
        // 它會先比較地址是否相同,如果地址相同,那么直接判定為相等
        // 如果地址不同,那么比較值是否相等
        int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
        Py_DECREF(obj);
        // 相等返回 1,不相等返回 0,比較失敗返回 -1
        // 如果 cmp 大于 0,表示兩者相等,返回索引
        if (cmp > 0)
            return PyLong_FromSsize_t(i);
        else if (cmp < 0)
            return NULL;
    }
    // 到這里說明元素不存在,那么拋出 ValueError: x is not in list
    PyErr_Format(PyExc_ValueError, "%R is not in list", value);
    return NULL;
}

所以列表 index 方法的時間復(fù)雜度為 O(n),因為它在底層要循環(huán)整個列表,如果運氣好,可能第一個元素就是;運氣不好,就只能循環(huán)整個列表了。

然后需要注意的是,在比較的時候,會先判斷地址是否相同,然后再比較值是否相等。

class A:

    def __eq__(self, other):
        return False


a = A()
data = [a]

print(a == data[0])  # False
print(data.index(a))  # 0

a 和 data[0] 指向的對象不相等,但 data.index(a) 卻返回了相應(yīng)的索引,因為兩者保存的地址是相同的。

同理 if v in data 這種也是類似的,先比較地址,地址不同再比較維護的值。

count:查詢元素出現(xiàn)的次數(shù)

列表有一個 count 方法,可以計算出某個元素出現(xiàn)的次數(shù)。

// Objects/clinic/listobject.c.h
#define LIST_COUNT_METHODDEF    \
    {"count", (PyCFunction)list_count, \
    METH_O, list_count__doc__},

它由 list_count 函數(shù)負(fù)責(zé)實現(xiàn)。

// Objects/listobject.c
static PyObject *
list_count(PyListObject *self, PyObject *value)
{
    Py_ssize_t count = 0;
    Py_ssize_t i;
    // 遍歷每一個元素
    for (i = 0; i < Py_SIZE(self); i++) {
        PyObject *obj = self->ob_item[i];
        // 如果地址相同,直接判定為相等,count 自增 1
        if (obj == value) {
           count++;
           continue;
        }
        Py_INCREF(obj);
        // 地址不同(a is b 不成立),則比較維護的值是否相等(看 a == b 是否成立)
        int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
        Py_DECREF(obj);
        if (cmp > 0)
            count++;
        else if (cmp < 0)
            return NULL;
    }
    // 返回元素出現(xiàn)的次數(shù)
    return PyLong_FromSsize_t(count);
}

毫無疑問,count 方法無論在什么情況下,它都是一個時間復(fù)雜度為 O(n) 的操作,因為列表必須要從頭遍歷到尾。

但還是要注意里面判斷相等的方式,因為變量只是一個指針,所以 C 的 == 相當(dāng)于 Python 的 is,但 Python 的 == 則對應(yīng) PyObject_RichCompare 函數(shù)。而源碼里面在比較的時候先調(diào)用 ==,所以會先判斷兩者是不是同一個對象。

class A:

    def __eq__(self, other):
        return False

a = A()
data = [a, a, a]
print(data[0] == a)  # False
print(data[1] == a)  # False
print(data[2] == a)  # False

print(data.count(a))  # 3

我們看到列表里的三個元素和 a 都不相等,但計算數(shù)量的時候,結(jié)果是 3。原因就是比較的時候是先比較地址,如果地址一樣,那么認(rèn)為元素相同。

當(dāng)然 PyObject_RichCompareBool 函數(shù)里面已經(jīng)包含了比較地址的邏輯,該函數(shù)會先比較地址是否一樣,如果一樣則認(rèn)為相等,不一樣再比較對象維護的值是否相等。但在 count 方法里面,將比較地址的邏輯又單獨拿了出來,可以理解為快分支。當(dāng)然即遍沒有也無所謂,因為在函數(shù) PyObject_RichCompareBool 里面還是會先對地址進行比較。

remove:刪除指定元素

除了根據(jù)索引刪除元素之外,也可以根據(jù)值來刪除元素,會刪除第一個出現(xiàn)的元素。

// Objects/clinic/listobject.c.h
#define LIST_REMOVE_METHODDEF    \
    {"remove", (PyCFunction)list_remove, \
    METH_O, list_remove__doc__},

它由 list_remove 函數(shù)實現(xiàn)。

// Objects/listobject.c
static PyObject *
list_remove(PyListObject *self, PyObject *value)
{
    Py_ssize_t i;
    // 遍歷每一個元素
    for (i = 0; i < Py_SIZE(self); i++) {
        PyObject *obj = self->ob_item[i];
        Py_INCREF(obj);
        // 比較是否相等,如果地址相同,那么認(rèn)為相等
        int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
        Py_DECREF(obj);
        // 如果相等,那么進行刪除
        if (cmp > 0) {
            // 可以看到在刪除元素的時候,調(diào)用了 list_ass_slice
            // 等價于 self[i: i + 1] = []
            if (list_ass_slice(self, i, i+1,
                               (PyObject *)NULL) == 0)
                Py_RETURN_NONE;
            return NULL;
        }
        else if (cmp < 0)
            return NULL;
    }
    // 否則說明元素不在列表中,拋出 ValueError: list.remove(x): x not in list
    PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
    return NULL;
}

以上就是 remove 函數(shù)的底層實現(xiàn),說白了就是一層 for 循環(huán),依次比較列表的每個元素和待刪除元素是否相等。如果出現(xiàn)了相等的元素,則刪除,然后直接返回,因為只刪除一個;但如果整個循環(huán)遍歷結(jié)束也沒有發(fā)現(xiàn)滿足條件的元素,那么報錯,待刪除元素不存在。

所以背后的邏輯并沒有我們想象中的那么神秘。

reverse:翻轉(zhuǎn)列表

如果是你的話,你會怎么對列表進行翻轉(zhuǎn)呢?顯然是采用雙指針,頭指針指向列表的第一個元素,尾指針指向列表的最后一個元素,然后兩兩交換。

交換完畢之后,頭指針后移一位、尾指針前移一位,繼續(xù)交換。當(dāng)兩個指針相遇時,停止交換,而 Python 底層也是這么做的。

// Objects/clinic/listobject.c.h
#define LIST_REVERSE_METHODDEF    \
    {"reverse", (PyCFunction)list_reverse, \
    METH_NOARGS, list_reverse__doc__},

它由 list_reverse 負(fù)責(zé)實現(xiàn)。

// Objects/clinic/listobject.c.h
static PyObject *
list_reverse(PyListObject *self, PyObject *Py_UNUSED(ignored))
{
    return list_reverse_impl(self);
}


// Objects/listobject.c
static PyObject *
list_reverse_impl(PyListObject *self)
{
    // 如果列表長度不大于 1,那么什么也不做,直接返回 None 即可
    if (Py_SIZE(self) > 1)
        // 大于 1 的話,執(zhí)行 reverse_slice,傳遞了兩個參數(shù)
        // 第一個參數(shù)顯然是底層數(shù)組首元素的地址
        // 而第二個參數(shù)則是底層數(shù)組中索引為 ob_size 的元素的地址
        // 但很明顯能訪問的最大索引應(yīng)該是 ob_size - 1 才對啊
        // 別急,我們繼續(xù)往下看,看一下 reverse_slice 函數(shù)的實現(xiàn)
        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);
    // 我們看到又執(zhí)行了一次 --hi
    // 讓二級指針 hi 指向了索引為 ob_size - 1 的元素
    --hi;
    // 數(shù)組元素的地址,從左往右是依次增大的
    // 如果 lo < hi,證明 lo 依舊在 hi 的左邊,那么交換指向的元素
    // 如果 lo > hi,證明兩者相遇了,交換結(jié)束
    while (lo < hi) {
        // 交換指向的元素,下面三步等價于 *lo, *hi = *hi, *lo
        // 但 C 不支持這么寫,它需要借助一個中間變量
        PyObject *t = *lo;
        *lo = *hi;
        *hi = t;
        // 兩個指針繼續(xù)靠近,指向的元素繼續(xù)交換,直到兩個指針相遇
        ++lo;
        --hi;
    }
}

所以到現(xiàn)在,你還認(rèn)為 Python 的列表神秘嗎?雖然我們很難自己寫出一個 Python 解釋器,但是底層的一些思想其實并沒有那么難,作為一名程序猿很容易想的到。

clear:清空列表

將列表中的元素全部清空,讓列表回到初始狀態(tài)。

// Objects/clinic/listobject.c.h
#define LIST_CLEAR_METHODDEF    \
    {"clear", (PyCFunction)list_clear, \
    METH_NOARGS, list_clear__doc__},

它由 list_clear 負(fù)責(zé)實現(xiàn)。

// Objects/clinic/listobject.c.h
static PyObject *
list_clear(PyListObject *self, PyObject *Py_UNUSED(ignored))
{
    return list_clear_impl(self);
}


// Objects/listobject.c
static PyObject *
list_clear_impl(PyListObject *self)
{
    // 在介紹 pop 方法的時候我們提到過這個函數(shù)
    _list_clear(self);
    Py_RETURN_NONE;
}


static int
_list_clear(PyListObject *a)
{
    Py_ssize_t i;
    PyObject **item = a->ob_item;
    if (item != NULL) {
        // 獲取列表的長度
        i = Py_SIZE(a);
        // 將 ob_size 設(shè)置為 0
        Py_SET_SIZE(a, 0);
        // ob_item 設(shè)置為 NULL
        a->ob_item = NULL;
        // 將容量設(shè)置為 0
        a->allocated = 0;
        // 將列表中每個元素指向的對象的引用計數(shù)減 1
        while (--i >= 0) {
            Py_XDECREF(item[i]);
        }
        // 釋放底層數(shù)組所占的內(nèi)存
        PyMem_Free(item);
    }
    return 0;
}

過程非常簡單,當(dāng)列表為空時,除了將 ob_size 和 allocated 設(shè)置為 0 之外,還會將底層數(shù)組釋放掉,減少內(nèi)存占用。

copy:列表的拷貝

調(diào)用列表的 copy 方法,可以將列表拷貝一份。

// Objects/clinic/listobject.c.h
#define LIST_COPY_METHODDEF    \
    {"copy", (PyCFunction)list_copy, \
    METH_NOARGS, list_copy__doc__},

它由 list_copy 負(fù)責(zé)實現(xiàn)。

// Objects/clinic/listobject.c.h
static PyObject *
list_copy(PyListObject *self, PyObject *Py_UNUSED(ignored))
{
    return list_copy_impl(self);
}

// Objects/listobject.c
static PyObject *
list_copy_impl(PyListObject *self)
{
    // 調(diào)用 list_slice,也就是基于切片獲取元素
    // 所以 data.copy() 等價于 data[:]
    return list_slice(self, 0, Py_SIZE(self));
}

static PyObject *
list_slice(PyListObject *a, Py_ssize_t ilow, Py_ssize_t ihigh)
{
    // 指向創(chuàng)建的新列表
    PyListObject *np;
    // 指向列表的底層數(shù)組的首元素
    PyObject **src, **dest;
    Py_ssize_t i, len;
    // 創(chuàng)建的新列表的長度
    len = ihigh - ilow;
    if (len <= 0) {
        return PyList_New(0);
    }
    // 創(chuàng)建底層數(shù)組長度為 len 的列表
    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++) {
        PyObject *v = src[i];
        dest[i] = Py_NewRef(v);
    }
    // 將新列表的 ob_size 設(shè)置為 len
    Py_SET_SIZE(np, len);
    // 轉(zhuǎn)成泛型指針之后返回
    return (PyObject *)np;
}

過程非常簡單,但列表的 copy 方法或者說 data[:] 這種叫做列表的淺拷貝。關(guān)于列表的深淺拷貝也是初學(xué)者容易犯的錯誤之一,我們看一個 Python 的例子。

data = [[]]

# 默認(rèn)是淺拷貝,這個過程會創(chuàng)建一個新列表
# 但我們說列表里面的元素都是指針,因此只會將里面的指針拷貝一份
# 而指針指向的內(nèi)存并沒有拷貝
data_cp = data.copy()

# 兩個對象的地址是一樣的
print(id(data[0]), id(data[0]))  
"""
1338344668224 1338344668224
"""

# 操作 data[0], 會改變 data_cp[0]
data[0].append(123)
print(data, data_cp)
"""
[[123]] [[123]]
"""

# 操作 data_cp[0],會改變 data[0]
data_cp[0].append(456)
print(data, data_cp)
"""
[[123, 456]] [[123, 456]]
"""

之所以會有這樣的現(xiàn)象,是因為 Python 的變量、容器里面的元素都是一個泛型指針 PyObject *,在傳遞的時候會傳遞指針, 但是在操作的時候會操作指針指向的內(nèi)存。

所以 data.copy() 就是創(chuàng)建了一個新列表,然后把元素拷貝了過去,只不過元素都是指針。因為只是拷貝指針,沒有拷貝指針指向的對象(內(nèi)存),所以它們指向的是同一個對象。

但如果我們就想在拷貝指針的同時也拷貝指針指向的對象呢?答案是使用一個叫 copy 的模塊。

import copy

data = [[]]
# 此時拷貝的時候,會把指針指向的對象也給拷貝一份
data_cp1 = copy.deepcopy(data)
data_cp2 = data[:]

data[0].append(123)
print(data_cp1)  # [[]]
print(data_cp2)  # [[123]]

# data[:] 這種方式也是淺拷貝,所以修改 data[0],會影響 data_cp2[0]
# 但是沒有影響 data_cp1[0],證明它們是相互獨立的,因為指向的是不同的對象

淺拷貝示意圖如下:

圖片圖片

里面的兩個指針數(shù)組存儲的元素是一樣的,都是同一個對象的地址。

深拷貝示意圖如下:

圖片圖片

里面的兩個指針數(shù)組存儲的元素是不一樣的,因為是不同對象的地址。

注意:copy.deepcopy 雖然在拷貝指針的同時會將指針指向的對象也拷貝一份,但這僅僅是針對可變對象,而不可變對象是不會拷貝的。

import copy

data = [[], "古明地覺"]
data_cp = copy.deepcopy(data)

print(data[0] is data_cp[0])  # False
print(data[1] is data_cp[1])  # True

為什么會這樣,其實原因很簡單。因為不可變對象是不支持本地修改的,你若想修改只能創(chuàng)建新的對象并指向它。但這對其它的變量而言則沒有影響,其它變量該指向誰就還指向誰。

因為 b = a 只是將 a 存儲的對象的指針拷貝一份給 b,然后 a 和 b 都指向了同一個對象,至于 a 和 b 本身則是沒有任何關(guān)系的。如果此時 a 指向了新的對象,是完全不會影響 b 的,b 還是指向原來的對象。

因此,如果一個指針指向的對象不支持本地修改,那么深拷貝不會拷貝對象本身,因為指向的是不可變對象,所以不會有修改一個影響另一個的情況出現(xiàn)。

關(guān)于列表還有一些陷阱:

data = [[]] * 5
data[0].append(1)
print(data)  # [[1], [1], [1], [1], [1]]
# 列表乘上一個 n,等于把列表里面的元素重復(fù) n 次
# 但列表里面存儲的是指針,也就是將指針重復(fù) n 次
# 所以上面的列表里面的 5 個指針存儲的地址是相同的
# 也就是說,它們都指向了同一個列表

# 這種方式創(chuàng)建的話,里面的指針都指向了不同的列表
data = [[], [], [], [], []]
data[0].append(1)
print(data)  # [[1], [], [], [], []]


# 再比如字典,在后續(xù)系列中會說
d = dict.fromkeys([1, 2, 3, 4], [])
print(d)  # {1: [], 2: [], 3: [], 4: []}
d[1].append(123)
print(d)  # {1: [123], 2: [123], 3: [123], 4: [123]}
# 它們都指向了同一個列表

類似的陷阱還有很多,因此在工作中要注意,否則一不小心就會出現(xiàn)大問題。

總之記住三句話:雖然 Python 一切皆對象,但我們拿到的其實是指向?qū)ο蟮闹羔槪蛔兞吭趥鬟f的時候本質(zhì)上是將對象的指針拷貝一份,所以 Python 是變量的賦值傳遞、對象的引用傳遞;在操作變量(指針)的時候,會自動操作變量(指針)指向的內(nèi)存。

小結(jié)

到此關(guān)于列表的內(nèi)容就介紹完了,作為 Python 中的萬能容器,我們可以自由地添加、修改和刪除元素。但在使用的時候要了解它的底層結(jié)構(gòu)以及元素是如何存儲的,應(yīng)該在什么場景下使用列表,它的每個方法的時間復(fù)雜度是多少。

責(zé)任編輯:武曉燕 來源: 古明地覺的編程教室
相關(guān)推薦

2024-08-26 11:13:26

字典entry自定義

2024-09-10 12:15:24

2024-07-29 12:27:55

列表對象元素

2022-04-24 15:17:56

鴻蒙操作系統(tǒng)

2010-11-12 13:34:02

動態(tài)sql語句

2010-01-15 15:26:46

VB.NET自定義類型

2017-02-17 09:37:12

Android自定義控件方法總結(jié)

2009-12-23 14:49:46

WPF面板

2010-02-24 14:59:52

WCF自定義過濾器

2009-09-07 22:00:15

LINQ自定義

2022-05-18 07:44:13

自定義菜單前端

2022-05-07 10:22:32

JavaScript自定義前端

2009-08-04 13:31:35

C#自定義事件

2009-12-24 15:22:10

WPF繼承自定義窗口

2024-03-04 11:13:29

Django數(shù)據(jù)庫Python

2022-05-27 07:51:07

自定義無序列表CSS

2015-02-12 15:33:43

微信SDK

2010-09-09 11:55:36

SQL函數(shù)標(biāo)簽

2022-04-01 15:59:22

SQLPostgreSQL審計

2015-07-29 10:31:16

Java緩存算法
點贊
收藏

51CTO技術(shù)棧公眾號

欧美日韩一区中文字幕| 姝姝窝人体www聚色窝| 成人在线影视| 亚洲国产国产| 国产精品资源站在线| 亚洲欧美一区二区三区久久| 日本一道在线观看| 一级一级黄色片| 偷窥自拍亚洲色图精选| 亚洲国产毛片aaaaa无费看| 国产在线视频2019最新视频| www久久久久久久| videos性欧美另类高清| 岛国av在线一区| 精品中文字幕在线观看| 污污网站在线观看视频| a√资源在线| 蜜臀av一区二区在线免费观看| 亚洲欧美一区二区三区情侣bbw| 激情视频免费网站| 可以在线观看的av网站| 亚洲少妇自拍| 国产视频精品久久久| 久操网在线观看| 黄片毛片在线看| 精品成人在线| 亚洲精品第一页| 99视频在线免费播放| 无码国产色欲xxxx视频| 国产精品亚洲欧美| 久久香蕉国产线看观看av| 亚洲黄色片免费| av在线免费网站| 国产欧美精品一区| 国产精品色悠悠| 激情高潮到大叫狂喷水| 亚洲一区导航| 亚洲一区二区高清| 国产日韩久久| 日本一区二区三区精品| 欧美女优在线视频| 在线免费av一区| 亚洲免费不卡| 国产福利视频导航| 在线观看日韩av电影| www.日韩系列| 最新国产精品自拍| 国产乱码午夜在线视频| 国产婷婷一区二区| 国产精品成人国产乱一区| 337人体粉嫩噜噜噜| 欧美综合自拍| 欧美日韩久久一区| 97超碰国产精品| 久香视频在线观看| 99久久精品国产麻豆演员表| 国产精品国产亚洲伊人久久| www欧美在线| 亚洲欧美日韩国产综合精品二区| 中文字幕日韩欧美在线| 极品人妻一区二区| 成人美女大片| 欧美性猛xxx| 中文有码久久| 日韩大胆视频| 狠狠色伊人亚洲综合成人| 国产69精品久久久| 一本在线免费视频| 哺乳挤奶一区二区三区免费看| 色综合色综合色综合色综合色综合 | 国产亚洲小视频| 国产成人短视频在线观看| 欧美精品免费视频| 天堂…中文在线最新版在线| bbw在线视频| 国产精品乱人伦中文| 风间由美一区二区三区| 成人黄色免费网| 看国产成人h片视频| 91高潮精品免费porn| 登山的目的在线| 亚洲欧洲免费| 中文字幕日韩av电影| 国产又粗又猛又爽又黄的视频小说| 色男人天堂综合再现| 日韩av在线网站| 亚洲午夜精品在线观看| 精品视频在线你懂得| 91精品国产日韩91久久久久久| 男人天堂999| 黄色污污视频在线观看| 成人免费一区二区三区视频| 欧美一区二区三区在线播放 | 国产成人一二三区| 国产小视频在线观看| 波波电影院一区二区三区| 亚洲va欧美va国产综合剧情| 在线观看国产区| 国产精品一区二区黑丝| 九9re精品视频在线观看re6| 亚洲乱码国产乱码精品精软件| 美女脱光内衣内裤视频久久网站 | 日韩av三级在线观看| 久久精品99国产精| 亚洲国产精品久久久天堂 | 5858s免费视频成人| 88av.com| 婷婷激情一区| 在线视频一区二区三区| 欧美污在线观看| 国产午夜精品一区在线观看| 7777精品伊人久久久大香线蕉的| 亚洲 欧美 日韩在线| abab456成人免费网址| 日韩欧美在线网址| 亚洲欧美日韩一二三区| 日韩久久99| 欧美日本在线看| 亚洲中文字幕无码av| av不卡一区二区| 色七七影院综合| 久久精品日韩无码| 亚洲一区国产| 国产精品日韩一区二区三区 | 五月天婷婷综合网| 亚洲久久一区| 欧美最顶级丰满的aⅴ艳星| 黄色在线观看国产| 国产精品538一区二区在线| 91久久国产自产拍夜夜嗨| www.亚洲黄色| 99久久国产综合精品麻豆| 91精品国产吴梦梦| 久久久久毛片| 欧美丰满高潮xxxx喷水动漫| 亚洲精品国产一区黑色丝袜| 青青草国产免费一区二区下载 | 日韩国产成人精品| 国产成人精品一区二区在线| 性色av一区二区三区四区| 99精品久久免费看蜜臀剧情介绍| 成人午夜视频免费观看| 亚洲精品成a人ⅴ香蕉片| 一本色道久久88综合日韩精品| 国产农村妇女精品一区| 久久久久久久高潮| 成人国产精品色哟哟| 黑人精品一区二区三区| 亚洲人成网站色在线观看| 成人性免费视频| 国产精品久久久网站| 亚洲最新在线视频| 一级黄色av片| 国产成都精品91一区二区三| 女人一区二区三区| 黄色成人在线观看| 精品久久久中文| 欧美一级特黄aaa| 欧美gay男男猛男无套| 欧美精品xxx| 久久久久久久久久一级| 国产毛片一区二区| 欧美 国产 精品| 精品国产一区二区三区性色av| 日韩av在线电影网| 欧美一区二区激情视频| 精品一区二区国语对白| 亚洲第一综合网站| 456亚洲精品成人影院| 欧美成人精品高清在线播放| 亚洲第一综合网| 亚洲欧洲综合| 欧美成人第一区| 免费一级欧美在线观看视频| 俺去亚洲欧洲欧美日韩| 草草视频在线播放| 精品国产1区2区| 亚洲精品国产熟女久久久| 久久精品二区亚洲w码| 乱子伦一区二区| 国产精品45p| 国产大片精品免费永久看nba| 在线免费观看黄色网址| 日韩欧美二区三区| 在线观看亚洲大片短视频| 青草av.久久免费一区| 精品一区二区三区国产| 日本美女久久| 欧美激情奇米色| 国产三级小视频| 中国色在线观看另类| aaa毛片在线观看| 加勒比视频一区| 国产精品久久一区主播| 天堂av最新在线| 欧美一区二区三区公司| 日本一区二区网站| 国产精品1024| 国产精品免费成人| 中文精品久久| 91精品在线看| yellow91字幕网在线| 精品亚洲一区二区三区四区五区| 亚洲午夜精品久久久| 国产亚洲精品久| 韩国三级在线播放| 手机精品视频在线观看| 欧美日韩在线高清| 亚洲女同志freevdieo| 精品无码久久久久久国产| 天天天天天天天干| 午夜视频一区在线观看| 精品1卡二卡三卡四卡老狼| 亚洲小说欧美另类婷婷| 午夜精品一区二区在线观看的 | 国产中文字幕91| 麻豆蜜桃在线观看| 亚洲男人天堂2019| 不卡视频在线播放| 欧美精品vⅰdeose4hd| 久久夜靖品2区| 亚洲美女免费视频| 中国男女全黄大片| 精品二区视频| ijzzijzzij亚洲大全| 欧美日韩水蜜桃| 成人激情视频网| 欧美18—19sex性hd| 久久久久国色av免费观看性色 | 国产精品手机在线播放| 国产91精品入口17c| 96sao精品免费视频观看| 国产成人午夜视频网址| 久草在线资源福利站| 欧美激情精品久久久久久大尺度| 精产国品自在线www| 中文字幕在线视频日韩| 国产小视频在线| 欧美色电影在线| 天天干天天色综合| 欧美日韩中文字幕| 亚洲黄色小说图片| 午夜精品久久久久久不卡8050| 久久成人国产精品入口| 亚洲一区二区三区爽爽爽爽爽| 美女福利视频在线观看| 成人av网站在线观看| 精品人妻在线视频| 成人自拍视频在线| 国产国语老龄妇女a片| 粉嫩在线一区二区三区视频| 一级黄色免费视频| jvid福利写真一区二区三区| 日韩av片网站| 秋霞国产午夜精品免费视频| 国产一线二线三线在线观看| 午夜精品婷婷| 欧美连裤袜在线视频| 伊人久久大香线蕉av不卡| 欧美午夜精品理论片a级大开眼界 欧美午夜精品久久久久免费视 | 888av在线| 日韩在线观看免费全| 成人video亚洲精品| 欧美日本中文字幕| 草草视频在线| 日本三级久久久| 国语自产精品视频在线看抢先版结局| 国产日韩精品综合网站| 欧美视频二区欧美影视| 国产精品中出一区二区三区| 亚洲都市激情| 亚洲无玛一区| 欧美大奶一区二区| 欧美精品与人动性物交免费看| 成人在线国产| 国产精品igao激情视频| 国产精品vip| 一区高清视频| 欧美日韩蜜桃| 三级在线免费观看| 亚洲黄色影片| 成人亚洲精品777777大片| 国产精品自拍网站| 国产亚洲无码精品| 国产精品视频第一区| 国产一级片视频| 亚洲免费观看高清在线观看| 国产亚洲欧美精品久久久www| 日韩欧美中文字幕在线观看| 一本久道久久综合无码中文| 欧美成人艳星乳罩| 精品久久av| 欧美成人激情视频| 麻豆tv免费在线观看| 亚洲最新视频在线| 午夜av在线播放| 国产经典一区二区| 中文字幕日韩在线| 91久久大香伊蕉在人线| 亚洲a级精品| 国产内射老熟女aaaa| 日日嗨av一区二区三区四区| 中文字幕av一区二区三区人妻少妇| 91网站最新网址| 在线色欧美三级视频| 性生活在线视频| 91亚洲永久精品| 免费在线观看a级片| 日本一区二区久久| 久久婷婷一区二区| 欧美日韩三级一区| 亚洲三级中文字幕| 久久成人免费视频| 亚洲精品粉嫩美女一区| 久久99精品久久久久子伦| 美女av一区| 最新欧美日韩亚洲| 日韩精品一级二级| 黄色国产在线观看| 亚洲自拍偷拍av| 久久精品亚洲无码| 欧美日韩成人在线一区| 欧美精品a∨在线观看不卡| 久久久久国产精品免费网站| av在线精品| 亚洲精品人成| 久久欧美肥婆一二区| 中文字幕在线视频播放| 日韩一区日韩二区| 一级黄色片视频| 91精品福利在线一区二区三区 | 久久久久中文字幕| 国产成年精品| www.午夜色| 久久国产精品免费| 久久久精品人妻一区二区三区| 国产精品国产三级国产aⅴ原创| 欧美人妻一区二区| 欧美日韩国产精品成人| 极品美乳网红视频免费在线观看| 91精品国产乱码久久久久久蜜臀| 99香蕉久久| 日韩伦理在线免费观看| 成人免费视频视频| 久久免费公开视频| 精品毛片乱码1区2区3区| 中文在线观看免费| 97免费视频在线| 色综合视频一区二区三区44| 亚洲激情电影在线| 青青草国产精品97视觉盛宴 | 欧美三级伦理在线| 91视频免费版污| 国产精品视频在线看| 中文字幕一区二区在线视频| 欧美r级电影在线观看| 秋霞在线视频| 国产精品亚洲激情| 久久精品论坛| 18禁男女爽爽爽午夜网站免费 | 欧美日韩免费一区| 国产又粗又猛又爽又黄91| 亚洲精品成人av| 成人国产二区| 一区二区三区av在线| 国模少妇一区二区三区| 青娱乐国产在线视频| 精品成人佐山爱一区二区| 91福利在线视频| 成人xxxxx| 伊人成年综合电影网| 内射中出日韩无国产剧情| 在线观看91精品国产入口| 黄色在线免费看| 国产偷国产偷亚洲高清97cao| 久久综合亚州| 亚洲综合网在线| 91成人看片片| 黄色在线视频网站| 狠狠爱一区二区三区| 日韩av网站免费在线| 欧美一区免费观看| 欧美精品一区二区三区在线播放| 三上悠亚一区二区| 天天做天天爱天天高潮| 老司机精品福利视频| 国产高潮流白浆| 亚洲精品美女在线| 热久久久久久| 亚洲熟妇无码另类久久久| 国产清纯在线一区二区www| 99国产在线播放| 日本一本a高清免费不卡| 亚洲午夜精品一区 二区 三区| av无码一区二区三区| 欧美日韩精品一区二区三区四区 | 国产欧美日韩亚洲一区二区三区| 五月激情四射婷婷| 亚洲精品aⅴ中文字幕乱码| 91国产一区|