分享

《源码探秘 CPython》29. 列表支持的操作(下)

 古明地觉O_o 2022-12-08 发布于北京

两个列表相加


两个列表之间可以进行加法运算,结果是一个新的列表,那么源码里面是如何实现的呢?

static PyObject *list_concat(PyListObject *a, PyObject *bb){    //相加之后的列表长度    Py_ssize_t size;     //循环变量    Py_ssize_t i;     //两个二级指针,指向相应的ob_item    PyObject **src, **dest;    //相加之后创建的新列表    PyListObject *np;    //类型检测,注意这里的bb是一个PyObject *    //所以它可以不是列表,但它的类型对象必须继承list    //因此这里检测函数是PyList_Check,相当于isinstance(obj, list)    //如果是PyList_CheckExactly,相当于type(obj) is list    if (!PyList_Check(bb)) {        PyErr_Format(PyExc_TypeError,                  "can only concatenate list (not \"%.200s\") to list",                  bb->ob_type->tp_name);        return NULL;    }#define b ((PyListObject *)bb)    //注意上面的宏,这里会将 bb 转成 PyListObject *        //判断长度是否溢出    if (Py_SIZE(a) > PY_SSIZE_T_MAX - Py_SIZE(b))        return PyErr_NoMemory();    //计算新列表的长度    size = Py_SIZE(a) + Py_SIZE(b);    //申请空间,其中指针数组的长度为size    np = (PyListObject *) list_new_prealloc(size);    //申请失败返回NULL    if (np == NULL) {        return NULL;    }    //先将 + 左边的列表里面的元素拷贝过去    //获取a -> ob_item和np -> ob_item    src = a->ob_item;    dest = np->ob_item;    //将元素依次拷贝过去,并增加引用计数    for (i = 0; i < Py_SIZE(a); i++) {        PyObject *v = src[i];        Py_INCREF(v);        dest[i] = v;    }        //再将 + 右边的列表里面的元素拷贝过去    //获取b->ob_item和np->ob_item + Py_SIZE(a)    //要从Py_SIZE(a)的位置开始设置, 否则就把之前的元素覆盖掉了    src = b->ob_item;    dest = np->ob_item + Py_SIZE(a);    //将元素依次拷贝过去, 增加引用计数    for (i = 0; i < Py_SIZE(b); i++) {        PyObject *v = src[i];        Py_INCREF(v);        dest[i] = v;    }    //设置ob_size    Py_SIZE(np) = size;    //返回np    return (PyObject *)np;#undef b}

以上就是列表的加法运算,逻辑就是创建一个新列表,然后将相加的两个列表里面的元素依次拷贝过去。

判断某个元素是否在列表中


对于一个序列来说,可以使用in操作符,等价于调用__contains__魔法方法。

static intlist_contains(PyListObject *a, PyObject *el){    PyObject *item;    Py_ssize_t i;    int cmp;    //挨个循环,比较是否相等,如果存在相等元素,cmp会等于1    //因此cmp == 0 && i < Py_SIZE(a)不满足,直接返回    //不相等则为0,会一直比完列表中所有的元素    for (i = 0, cmp = 0 ; cmp == 0 && i < Py_SIZE(a); ++i) {        item = PyList_GET_ITEM(a, i);        Py_INCREF(item);        cmp = PyObject_RichCompareBool(el, item, Py_EQ);        Py_DECREF(item);    }    return cmp;}

真的非常简单,没有什么好说的。

列表的深浅拷贝


列表的深浅拷贝也是初学者容易犯的错误之一,我们看一个Python的例子。

lst = [[]]
# 默认是浅拷贝, 这个过程会创建一个新列表# 但我们说列表l里面都是指针,因此只会将里面的指针拷贝一份# 但是指针指向的内存并没有拷贝lst_cp = lst.copy()
# 两个对象的地址是一样的print(id(lst[0]), id(lst_cp[0])) # 2207105155392 2207105155392
# 操作lst[0], 会改变lst_cp[0]lst[0].append(123)print(lst, lst_cp) # [[123]] [[123]]
# 操作lst_cp[0], 会改变lst[0]lst_cp[0].append(456)print(lst, lst_cp) # [[123, 456]] [[123, 456]]

我们通过索引或者切片也是一样的道理:

lst = [[], 1, 2, 3]val = lst[0]lst_cp = lst[0: 1]
print(lst[0] is val is lst_cp[0]) # True# 此外,lst[:]等价于lst.copy()

之所以会有这样现象,是因为Python的变量、容器里面的元素都是一个泛型指针PyObject *,在传递的时候会传递指针, 但是在操作的时候会操作指针指向的内存。

所以lst.copy()就是创建了一个新列表,然后把元素拷贝了过去,只不过元素都是指针。因为只是拷贝指针,没有拷贝指针指向的对象(内存),所以它们指向的是同一个对象。

但如果我们就想在拷贝指针的同时也拷贝指针指向的对象呢?答案是使用一个叫copy的模块。

import copy
lst = [[]]# 此时拷贝的时候,就会把指针指向的对象也给拷贝一份lst_cp1 = copy.deepcopy(lst)lst_cp2 = lst[:]
lst_cp2[0].append(123)print(lst) # [[123]]print(lst_cp1) # [[]]
# lst[:]这种方式也是浅拷贝, 所以修改lst_cp2[0], 也会影响lst[0]# 但是没有影响lst_cp1[0], 证明它们是相互独立的, 因为指向的是不同的对象

浅拷贝示意图如下:

里面的两个指针数组存储的元素是一样的,都是同一个对象的地址。

深拷贝示意图如下:

里面的两个指针数组存储的元素是不一样的,因为是不同对象的地址。

注意:copy.deepcopy虽然在拷贝指针的同时会将指针指向的对象也拷贝一份,但这仅仅是针对于可变对象,对于不可变对象是不会拷贝的。

import copy
lst = [[], "古明地觉"]lst_cp = copy.deepcopy(lst)
print(lst[0] is lst_cp[0]) # Falseprint(lst[1] is lst_cp[1]) # True

为什么会这样,其实原因很简单。因为不可变对象是不支持本地修改的,你若想修改只能创建新的对象并指向它。但是这对其它的变量则没有影响,其它变量该指向谁就还指向谁。

因为b = a只是将a存储的对象的指针拷贝一份给b,然后a和b都指向了同一个对象,至于a和b本身则是没有任何关系的。如果此时a指向了新的对象,是完全不会影响b的,b还是指向原来的对象。


因此,如果一个指针指向的对象不支持本地修改,那么深拷贝不会拷贝对象本身,因为指向的是不可变对象,所以不会有修改一个影响另一个的情况出现。

关于列表还有一些陷阱:

lst = [[]] * 5lst[0].append(1)print(lst)  # [[1], [1], [1], [1], [1]]# 列表乘上一个n,等于把列表里面的元素重复n次# 但列表里面存储的是指针,也就是将指针重复n次# 所以上面的列表里面的5个指针存储的地址是相同的# 也就是说,它们都指向了同一个列表

# 这种方式创建的话,里面的指针都指向了不同的列表lst = [[], [], [], [], []]lst[0].append(1)print(lst) # [[1], [], [], [], []]

# 再比如字典,在后续系列中会说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]}# 它们都指向了同一个列表

类似的陷阱还有很多,因此在工作中要注意,否则一不小心就会出现大问题。

总之记住三句话:虽然Python中一切皆对象,但我们拿到的其实是指向对象的指针变量在传递的时候本质上是将对象的指针拷贝一份,所以Python是变量的赋值传递、对象的引用传递而在操作变量(指针)的时候,会操作变量(指针)指向的内存

小结


到此,关于列表的操作就介绍完了,我们用了三篇文章来介绍,因为列表用的非常频繁。在使用时容易掉入陷阱中,所以多花了一些笔墨去介绍它。

下一篇我们来介绍列表的创建和销毁,以及列表的缓存池是如何实现的。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章