08-解密Python中列表的底层实现 楔子 Python中的列表可以说使用的非常广泛了,在初学列表的时候,老师会告诉你列表就是一个大仓库,什么都可以存放。不过在最开始的几个章节中,我们花了很大的笔墨介绍了Python中的对象,并明白了Python中变量的本质,我们知道列表中存放的元素其实都是泛型指针PyObject *,所以到现在列表已经没有什么好神秘的了。
并且根据我们使用列表的经验,我们可以得出以下两个结论:
每个列表中的元素个数可以不一样:所以这是一个变长对象
可以对列表中的元素进行添加、删除、修改等操作,所以这是一个可变对象
在分析列表对应的底层结构之前,我们先来回顾一下列表的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 >>> lst = [1 , 2 , 3 , 4 ]>>> lst[1 , 2 , 3 , 4 ] >>> lst.append(5 )>>> lst[1 , 2 , 3 , 4 , 5 ] >>> lst.pop()5 >>> lst[1 , 2 , 3 , 4 ] >>> lst.pop(0 )1 >>> lst[2 , 3 , 4 ] >>> lst.insert(0 , 'x' )>>> lst['x' , 2 , 3 , 4 ] >>> lst.extend([7 , 8 , 9 ])>>> lst['x' , 2 , 3 , 4 , 7 , 8 , 9 ] >>> lst.index(3 )2 >>> lst.count(3 )2 >>> lst.reverse()>>> lst[9 , 8 , 7 , 4 , 3 , 2 , 'x' ] >>> lst.remove(4 )[9 , 8 , 7 , 3 , 2 , 'x' ] >>> lst.clear()>>> lst[] >>>
上面的一些操作是列表经常使用的,但是在分析它的实现之前,我们肯定要了解它们的时间复杂度如何。这些东西即使不看源码,也是必须要知道的,尤其想要成为一名优秀的Python工程师。
append:会向尾部追加元素,所以时间复杂度为O(1)
pop:默认从尾部弹出元素,所以时间复杂度为O(1);如果不是尾部,而是从其它的位置弹出元素的话,那么该位置后面所有的元素都要向前移动,此时时间复杂度为O(n)
insert:向指定位置插入元素,该位置后面的所有元素都要向后移动,所以时间复杂度为O(n)
注意:由于列表里面的元素个数是可以自由变化的,所以列表有一个容量的概念,我们后面会说。当添加元素时,列表可能会扩容;同理当删除元素时,列表可能会缩容。
下面我们就来看一下列表对应的底层结构。
列表的内部结构–PyListObject *list* 对象(列表)
在 *Python* 内部,由 *PyListObject* 结构体表示,定义于头文件 *Include/listobject.h* 中:
1 2 3 4 5 typedef struct { PyObject_VAR_HEAD PyObject **ob_item; Py_ssize_t allocated; } PyListObject;
我们看到里面有如下成员:
PyObject_VAR_HEAD: 变长对象的公共头部信息
ob_item:一个二级指针,指向一个PyObject *类型的指针数组,这个指针数组保存的便是对象的指针,而操作底层数组都是通过ob_item来进行操作的。
allocated:容量, 我们知道列表底层是使用了C的数组, 而底层数组的长度就是列表的容量
列表之所以要有容量的概念,是因为列表可以动态添加元素,但是底层的数组在创建完毕之后,其长度却是固定的。所以一旦添加新元素的时候,发现底层数组已经满了,这个时候只能申请一个更长的数组,同时把原来数组中的元素依次拷贝到新的数组里面(这一过程就是列表的扩容)
,然后再将新元素添加进去。但是问题来了,总不可能每添加一个元素,就申请一次数组、将所有元素都拷贝一次吧。所以Python在列表扩容的时候,会将底层数组申请的长一些,可以在添加元素的时候不用每次都申请新的数组。
这便是列表的底层结构示意图,图中的object只是单纯的代指对象,不是Python中的基类object。我们看到底层数组的长度为5,说明此时列表的容量为5,但是里面只有3个PyObject *指针,说明列表的ob_size是3,或者说列表里面此时有3个元素。注意:尽管底层数组的容量目前是5个,但是我们访问的时候,最多只能访问到第三个元素,也就是说索引最大只能是2,这是显而易见的,因为列表里面只有3个元素。
如果这个时候我们往列表中append一个元素,那么会将这个新元素设置在底层数组中索引为ob_size的位置、或者说第四个位置。一旦设置完,ob_size会自动加1,因为ob_size要和列表的长度保持一致。
如果此时再往列表中append一个元素的话,那么还是将新元素设置在索引为ob_size的位置,此时也就是第5个位置。
列表的容量是5,但此时长度也达到了5,这说明当下一次append的时候已经没有办法再容纳新的元素了。因为此时列表的长度、或者说元素个数已经达到了容量,当然最直观的还是这里的底层数组,很明显全都占满了。那这个时候如果想再接收新的元素的话,要怎么办呢?显然只能扩容了。
原来的容量是5个,长度也是5个,当再来一个新元素的时候由于没有位置了,所以要扩容。但是扩容的时候肯定会将容量申请的大一些、即底层数组申请的长一些(具体申请多长,Python内部有一个公式,我们后面会说)
,假设申请的新的底层数组长度是8,那么说明列表的容量就变成了8。然后将原来数组中的PyObject *按照顺序依次拷贝到新的数组里面,再让ob_item指向新的数组。最后将要添加的新元素设置在新的数组中索引为ob_size的位置、即第6个位置,然后将ob_size加1,此时ob_size就变成了6。
以上便是列表底层在扩容的时候所经历的过程。
由于扩容会申请新的数组,然后将旧数组的元素拷贝到新数组中,所以这是一个时间复杂度为O(n)的操作。而append可能会导致列表扩容,因此append最坏情况下也是一个O(n)的操作,只不过扩容不会频繁发生,所以append的平均时间复杂度还是O(1)。
另外我们还可以看到一个现象,那就是Python中的列表在底层是分开存储的,因为PyListObject结构体实例并没有存储相应的指针数组,而是存储了指向这个指针数组的二级指针。显然我们添加、删除、修改元素等操作,都是通过ob_item这个二级指针来间接操作这个指针数组。
所以底层对应的PyListObject实例的大小其实是不变的,因为指针数组没有存在PyListObject里面。但是Python在计算内存大小的时候是会将这个指针数组也算进去的,所以Python中列表的大小是可变的。
而且我们知道,列表在append之后地址是不变的,至于原因上面的几张图已经解释的很清楚了。如果长度没有达到容量,那么append其实就是往底层数组中设置了一个新元素;如果达到容量了,那么会扩容,但是扩容只是申请一个新的指针数组,然后让ob_item重新指向罢了。所以底层数组会变,但是PyListObject结构体实例本身是没有变化的。因此列表无论是append、extend、pop、insert等等,只要是在本地操作,那么它的地址是不会变化的。
下面我们再来看看Python中的列表所占的内存大小是怎么算的:
PyObject_VAR_HEAD: 24字节
ob_item: 二级指针, 8字节
allocated: 8字节
但是不要忘记,在计算列表大小的时候,ob_item指向的指针数组也要算在内。所以:一个列表的大小 = 40 + 8 * 指针数组长度(或者列表容量)
。注意是底层数组长度、或者列表容量,可不是列表长度,因为底层数组一旦申请了,不管你用没用,大小就摆在那里了。就好比你租了间房子,就算不住,房租该交还是得交。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 print ([].__sizeof__()) print ([1 , 2 , "x" * 1000 ].__sizeof__()) lst = list (range (10 )) print (lst.__sizeof__()) lst.append(123 ) print (lst.__sizeof__())
所以列表的大小我们就知道是怎么来的了,而且为什么列表在通过索引定位元素的时候,时间复杂度是O(1)。因为列表中存储的都是对象的指针,不管对象有多大,其指针大小是固定的,都是8字节。通过索引可以瞬间计算出偏移量,从而找到对应元素的指针,而操作指针会自动操作指针所指向的内存。
1 2 print ([1 , 2 , 3 ].__sizeof__()) print ([[1 , 2 , 3 ]].__sizeof__())
相信上面这个结果,你肯定能分析出原因。因为第一个列表中有3个指针,所以是40 + 24 = 64;而第二个列表中有一个指针,所以是40 + 8 = 48。用一张图来展示一下[1, 2, 3]
和[[1, 2, 3]]
的底层结构,看看它们之间的区别:
分析完PyListObject之后,我们来看看它支持的操作,显然我们要通过类型对象PyList_Type来查看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PyTypeObject PyList_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0 ) "list" , sizeof (PyListObject), 0 , (destructor)list_dealloc, 0 , 0 , 0 , 0 , (reprfunc)list_repr, 0 , &list_as_sequence, &list_as_mapping, };
我们看到,列表支持序列型操作和映射型操作,下面我们就来分析一下。
列表支持的操作 我们看看平常使用的列表所支持的操作在底层是如何实现的。
自动扩容 我们先来说说列表的扩容,因为我们知道列表是会自动扩容的,那么什么时候会扩容呢?我们说列表扩容的时候,是在添加元素时发现底层数组已经满了的情况下才会扩容。换句话说,一个列表在添加元素的时候会扩容,那么说明在添加元素之前,其内部的元素个数和容量是相等的。然后我们看看底层是怎么实现的,这些操作都位于Objects/listobject.c中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 static int list_resize (PyListObject *self, Py_ssize_t newsize) { PyObject **items; size_t new_allocated, num_allocated_bytes; 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 = (size_t )newsize + (newsize >> 3 ) + (newsize < 9 ? 3 : 6 ); if (new_allocated > (size_t )PY_SSIZE_T_MAX / sizeof (PyObject *)) { PyErr_NoMemory(); return -1 ; } if (newsize == 0 ) new_allocated = 0 ; num_allocated_bytes = new_allocated * sizeof (PyObject *); items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes); if (items == NULL ) { PyErr_NoMemory(); return -1 ; } self->ob_item = items; Py_SIZE(self) = newsize; self->allocated = new_allocated; return 0 ; }
我们看到还是很简单的,没有什么黑科技,下面我们就来分析一下列表扩容的时候,容量和元素个数之间的规律。其实在list_resize函数中是有注释的,其种一行写着:The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
说明我们往一个空列表中不断append元素的时候,容量会按照上面的规律进行变化,我们来试一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 lst = [] allocated = 0 print ("此时容量是: 0" )for item in range (100 ): lst.append(item) ob_size = len (lst) if ob_size > allocated: allocated = (lst.__sizeof__() - [].__sizeof__()) // 8 print (f"列表扩容啦, 新的容量是: {allocated} " ) """ 此时容量是: 0 列表扩容啦, 新的容量是: 4 列表扩容啦, 新的容量是: 8 列表扩容啦, 新的容量是: 16 列表扩容啦, 新的容量是: 25 列表扩容啦, 新的容量是: 35 列表扩容啦, 新的容量是: 46 列表扩容啦, 新的容量是: 58 列表扩容啦, 新的容量是: 72 列表扩容啦, 新的容量是: 88 列表扩容啦, 新的容量是: 106 Process finished with exit code 0 """
我们看到和官方给的结果是一样的,显然这是毫无疑问的,我们根据底层的公式也能算出来。
1 2 3 4 5 6 7 8 9 10 ob_size = 0 allocated = 0 print (allocated, end=" " )for item in range (100 ): ob_size += 1 if ob_size > allocated: allocated = ob_size + (ob_size >> 3 ) + (3 if ob_size < 9 else 6 ) print (allocated, end=" " )
但还是那句话,扩容是指解释器发现容量不够的情况下才会扩容,如果我们直接通过lst = []这种形式创建列表的话,那么其长度和容量是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 lst = [0 ] * 1000 print (len (lst), (lst.__sizeof__() - [].__sizeof__()) // 8 ) print ("新容量:" , 1001 + (1001 >> 3 ) + (3 if 1001 < 9 else 6 )) lst.append(123 ) print ((lst.__sizeof__() - [].__sizeof__()) // 8 )
介绍完扩容,那么介绍缩容,因为列表元素个数要是减少到和容量不匹配的话,也要进行缩容。
举个生活中的例子,假设你租了10间屋子用于办公,显然你要付10间屋子的房租,不管你有没有住,一旦租了肯定是要付钱的。同理底层数组也是一样,只要你申请了,不管有没有元素,内存已经占用了。但有一天你用不到10间屋子了,假设会用8间或者9间,那么会让剩余的屋子闲下来。但由于退租比较麻烦,并且只闲下来一两间屋子,所以多余的屋子就不退了,还是会付10间屋子的钱,这样当没准哪天又要用的时候就不用重新租了。对于列表也是如此,如果在删除元素(相当于屋子不用了)的时候发现长度没有超过容量但是又达到了容量的一半,所以也不会缩容。但是,如果屋子闲了8间,也就是只需要两间屋子就足够了,那么此时肯定要退租了,闲了8间,可能会退掉6间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 lst = [0 ] * 1000 print (len (lst), (lst.__sizeof__() - [].__sizeof__()) // 8 ) lst[500 :] = [] print (len (lst), (lst.__sizeof__() - [].__sizeof__()) // 8 ) print (499 + (499 >> 3 ) + (3 if 499 < 9 else 6 )) lst.pop() print (len (lst), (lst.__sizeof__() - [].__sizeof__()) // 8 )
一切都和我们想的是一样的,另外在代码中我们还看到一个if语句,就是如果newsize是0,那么容量也是0,我们来测试一下。
1 2 3 4 5 6 7 8 9 10 11 12 lst = [0 ] * 1000 print (len (lst), (lst.__sizeof__() - [].__sizeof__()) // 8 ) lst[:] = [] print (len (lst), (lst.__sizeof__() - [].__sizeof__()) // 8 ) print (0 + (0 >> 3 ) + (3 if 0 < 9 else 6 ))
以上就是列表在改变容量时所采用的策略,我们从头到尾全部分析了一遍。
append追加元素 append方法用于像尾部追加一个元素,我们看看底层实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 static PyObject *list_append (PyListObject *self, PyObject *object) { if (app1(self, object) == 0 ) Py_RETURN_NONE; return NULL ; } static int app1 (PyListObject *self, PyObject *v) { 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 ; } if (list_resize(self, n+1 ) < 0 ) return -1 ; Py_INCREF(v); PyList_SET_ITEM(self, n, v); return 0 ; } #define PyList_GET_ITEM(op, i) (((PyListObject *)(op))->ob_item[i]) #define PyList_SET_ITEM(op, i, v) (((PyListObject *)(op))->ob_item[i] = (v)) #define PyList_GET_SIZE(op) (assert(PyList_Check(op)),Py_SIZE(op))
获取元素 我们在使用列表的时候,可以通过val = lst[1]这种方式获取元素,那么底层是如何实现的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 static PyObject *list_subscript (PyListObject* self, PyObject* item) { if (PyIndex_Check(item)) { Py_ssize_t i; i = PyNumber_AsSsize_t(item, PyExc_IndexError); if (i == -1 && PyErr_Occurred()) return NULL ; if (i < 0 ) i += PyList_GET_SIZE(self); return list_item(self, i); } else if (PySlice_Check(item)) { } else { } } static PyObject *list_item(PyListObject *a, Py_ssize_t i) { if (!valid_index(i, Py_SIZE(a))) { if (indexerr == NULL ) { indexerr = PyUnicode_FromString( "list index out of range" ); if (indexerr == NULL ) return NULL ; } PyErr_SetObject(PyExc_IndexError, indexerr); return NULL ; } Py_INCREF(a->ob_item[i]); return a->ob_item[i]; }
显然获取元素的时候不光可以通过索引,还可以通过切片的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 static PyObject *list_subscript (PyListObject* self, PyObject* item) { if (PyIndex_Check(item)) { } else if (PySlice_Check(item)) { Py_ssize_t start, stop, step, slicelength, cur, i; PyObject* result; PyObject* it; PyObject **src, **dest; if (PySlice_Unpack(item, &start, &stop, &step) < 0 ) { return NULL ; } slicelength = PySlice_AdjustIndices(Py_SIZE(self), &start, &stop, step); if (slicelength <= 0 ) { return PyList_New(0 ); } else if (step == 1 ) { return list_slice(self, start, stop); } else { result = list_new_prealloc(slicelength); if (!result) return NULL ; src = self->ob_item; dest = ((PyListObject *)result)->ob_item; for (cur = start, i = 0 ; i < slicelength; cur += (size_t )step, i++) { it = src[cur]; Py_INCREF(it); dest[i] = it; } Py_SIZE(result) = slicelength; return result; } } else { PyErr_Format(PyExc_TypeError, "list indices must be integers or slices, not %.200s" , item->ob_type->tp_name); return NULL ; } }
我们发现这个和字符串类似啊,因为通过字符串也支持切片的方式获取。
随着源码的分析,我们也渐渐明朗Python的操作在底层是如何实现的了,真的一点不神秘,实现的逻辑非常简单。
设置元素 获取元素知道了,设置元素也不难了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 static int list_ass_subscript (PyListObject* self, PyObject* item, PyObject* value) { if (PyIndex_Check(item)) { Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError); if (i == -1 && PyErr_Occurred()) return -1 ; if (i < 0 ) i += PyList_GET_SIZE(self); return list_ass_item(self, i, value); } else if (PySlice_Check(item)) { } } static int list_ass_item(PyListObject *a, Py_ssize_t i, PyObject *v) { if (!valid_index(i, Py_SIZE(a))) { PyErr_SetString(PyExc_IndexError, "list assignment index out of range" ); return -1 ; } if (v == NULL ) return list_ass_slice(a, i, i+1 , v); Py_INCREF(v); Py_SETREF(a->ob_item[i], v); return 0 ; }
通过索引设置元素,逻辑很容易,关键是通过切片设置元素会比较复杂。而复杂的原因就在于步长,我们通过Python来演示一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 lst = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ] lst[0 : 3 ] = [11 , 22 , 33 ] print (lst) lst[0 : 3 ] = [1 , 2 ] print (lst) lst[0 : 3 ] = "" print (lst) lst[0 : 1 ] = [1 , 2 , 3 , 4 ] print (lst) lst[:: 2 ] = ['a' , 'b' , 'c' , 'd' ] print (lst) try : lst[:: 2 ] = ['a' , 'b' , 'c' ] except Exception as e: print (e)
至于通过切片来设置元素,源码很长,这里就不分析了,总之核心如下:
如果步长为1: 那么会调用list_ass_slice。我们说: list_item是基于索引获取元素、list_slice是基于切片获取元素、list_ass_item是基于索引设置元素、list_ass_slice是基于切片设置元素。而list_ass_slice内部的代码逻辑也很长,但是核心并不难, 我们通过lst[a: b] = [v1, v2, v3, ...]这种方式就会走这里的list_ass_slice。
如果步长不为1,那么就是采用循环的方式逐个设置。
主要是考虑的情况比较多,但是核心逻辑并不复杂,有兴趣可以自己去深入了解一下。
insert插入元素 insert用来在指定的位置插入元素,我们知道它是一个时间复杂度为O(n)的一个操作,因为插入位置后面的所有元素都要向后移动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 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 ; } if (list_resize(self, n+1 ) < 0 ) 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 ; }
所以可以看到,Python插入数据是非常灵活的。不管你在什么位置插入,都是合法的。因为它会自己调整位置,在确定位置之后,会将当前位置以及之后的所有元素向后挪动一个位置,空出来的地方设置为插入的值。
pop弹出元素 pop默认是从尾部弹出元素的,因为如果不指定索引的话,默认是-1。当然我们也可以指定索引,弹出指定索引对应的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 static PyObject *list_pop_impl (PyListObject *self, Py_ssize_t index) { PyObject *v; int status; if (Py_SIZE(self) == 0 ) { PyErr_SetString(PyExc_IndexError, "pop from empty list" ); return NULL ; } if (index < 0 ) index += Py_SIZE(self); if (!valid_index(index, Py_SIZE(self))) { PyErr_SetString(PyExc_IndexError, "pop index out of range" ); return NULL ; } v = self->ob_item[index]; if (index == Py_SIZE(self) - 1 ) { status = list_resize(self, Py_SIZE(self) - 1 ); if (status >= 0 ) return v; else return NULL ; } Py_INCREF(v); status = list_ass_slice(self, index, index+1 , (PyObject *)NULL ); if (status < 0 ) { Py_DECREF(v); return NULL ; } return v; }
所以pop本质上也是调用了list_ass_slice。
index查询元素的索引 index可以接收一个元素,返回该元素首次出现的索引。当然还可以额外指定一个start和end,表示查询的范围
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 static PyObject *list_index_impl (PyListObject *self, PyObject *value, Py_ssize_t start, Py_ssize_t stop) { Py_ssize_t i; if (start < 0 ) { start += Py_SIZE(self); if (start < 0 ) start = 0 ; } if (stop < 0 ) { stop += Py_SIZE(self); if (stop < 0 ) stop = 0 ; } for (i = start; i < stop && i < Py_SIZE(self); i++) { PyObject *obj = self->ob_item[i]; Py_INCREF(obj); int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); Py_DECREF(obj); if (cmp > 0 ) return PyLong_FromSsize_t(i); else if (cmp < 0 ) return NULL ; } PyErr_Format(PyExc_ValueError, "%R is not in list" , value); return NULL ; }
所以lst.index是一个时间复杂度为O(n)的操作,因为它在底层要循环整个列表,如果运气好,可以第一个元素就是,运气不好可能就好循环整个列表了。同理后面要说的if value in lst这种方式也是一样的,因为都要循环整个列表,只不过后者返回的是一个布尔值。
count查询元素出现的次数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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]; if (obj == value) { count++; continue ; } Py_INCREF(obj); int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); Py_DECREF(obj); if (cmp > 0 ) count++; else if (cmp < 0 ) return NULL ; } return PyLong_FromSsize_t(count); }
count毫无疑问,无论在什么情况下,它都是一个时间复杂度为O(n)的操作,因为列表必须要从头遍历到尾。
remove根据元素的值删除元素 除了根据索引删除元素之外,也可以元素指向的对象维护的值删除元素,删除第一个出现元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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); int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); Py_DECREF(obj); 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 ; } PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list" ); return NULL ; }
reverse翻转列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 static PyObject *list_reverse_impl (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; } }
所以到现在,你还认为Python中的列表神秘吗?虽然我们不可能写出一个Python解释器,但是底层的一些思想其实并没有那么难,作为一名程序猿很容易想的到。
两个列表相加 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 static PyObject *list_concat (PyListObject *a, PyObject *bb) { Py_ssize_t size; Py_ssize_t i; PyObject **src, **dest; PyListObject *np; 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) if (Py_SIZE(a) > PY_SSIZE_T_MAX - Py_SIZE(b)) return PyErr_NoMemory(); size = Py_SIZE(a) + Py_SIZE(b); np = (PyListObject *) list_new_prealloc(size); if (np == NULL ) { return NULL ; } 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; } 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; } Py_SIZE(np) = size; return (PyObject *)np; #undef b }
判断元素是否在列表中 对于一个序列来说,可以使用in操作符,等价于调用其__contains__
魔法方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static int list_contains (PyListObject *a, PyObject *el) { PyObject *item; Py_ssize_t i; int cmp; 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的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 lst = [[]] lst_cp = lst.copy() print (id (lst[0 ]), id (lst_cp[0 ])) lst[0 ].append(123 ) print (lst, lst_cp) lst_cp[0 ].append(456 ) print (lst, lst_cp)
我们通过索引或者切片也是一样的道理
1 2 3 4 5 6 lst = [[], 1 , 2 , 3 ] val = lst[0 ] lst_cp = lst[0 : 1 ] print (lst[0 ] is val is lst_cp[0 ])
之所以会有这样现象,是因为我们说过Python中变量、容器里面的元素都是一个泛型指针PyObject *,在传递的时候会传递指针, 但是在操作的时候会操作指针指向的内存。
所以lst.copy()就是创建了一个新列表,然后把元素拷贝了过去,并且这里的元素是指针。因为只是拷贝指针,没有拷贝指针指向的对象(内存)
,所以它们的地址都是一样的,因为指向的是同一个对象。
但如果我们就想在拷贝指针的同时也拷贝指针指向的对象呢?答案是使用一个叫copy的模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 import copylst = [[]] lst_cp1 = copy.deepcopy(lst) lst_cp2 = lst[:] lst_cp2[0 ].append(123 ) print (lst) print (lst_cp1)
浅拷贝示意图如下:
里面的两个底层数组的元素是一样的
深拷贝示意图如下:
里面的两个底层数组的元素是不一样的
注意:copy.deepcopy虽然在拷贝指针的同时会将指针指向的对象也拷贝一份,但这仅仅是针对于可变对象,对于不可变对象是不会拷贝的。
1 2 3 4 5 6 7 import copylst = [[], "古明地觉" ] lst_cp = copy.deepcopy(lst) print (lst[0 ] is lst_cp[0 ]) print (lst[1 ] is lst_cp[1 ])
为什么会这样,其实原因很简单。因为不可变对象是不支持本地修改的,你若想修改只能指向新的对象,但是对其它的变量则没有影响,其它变量该指向谁就还指向谁。因为a = b只是将对象的指针拷贝一份给a,然后a和b都指向了同一个对象,至于a和b本身则是没有任何关系的。如果此时b指向了新的对象,是完全不会影响a的,a还是指向原来的对象。所以如果一个指针指向的对象不支持本地修改,那么深拷贝不会拷贝对象本身,因为指向的是不可变对象,所以不会有修改一个影响另一个的情况出现。
关于列表还有一些陷阱:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 lst = [[]] * 5 lst[0 ].append(1 ) print (lst) lst = [[], [], [], [], []] lst[0 ].append(1 ) print (lst) d = dict .fromkeys([1 , 2 , 3 , 4 ], []) print (d) d[1 ].append(123 ) print (d)
创建PyListObject 我们说创建一个列表,Python底层只提供了唯一一个Python/C API,也就是PyList_New。这个函数接收一个size参数,从而允许我们在创建一个PyListObject对象时指定底层数组的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 PyObject * PyList_New (Py_ssize_t size) { PyListObject *op; #ifdef SHOW_ALLOC_COUNT static int initialized = 0 ; if (!initialized) { Py_AtExit(show_alloc); initialized = 1 ; } #endif if (size < 0 ) { PyErr_BadInternalCall(); return NULL ; } if (numfree) { numfree--; op = free_list[numfree]; _Py_NewReference((PyObject *)op); #ifdef SHOW_ALLOC_COUNT count_reuse++; #endif } else { op = PyObject_GC_New(PyListObject, &PyList_Type); if (op == NULL ) return NULL ; #ifdef SHOW_ALLOC_COUNT count_alloc++; #endif } if (size <= 0 ) op->ob_item = NULL ; else { op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof (PyObject *)); if (op->ob_item == NULL ) { Py_DECREF(op); return PyErr_NoMemory(); } } Py_SIZE(op) = size; op->allocated = size; _PyObject_GC_TRACK(op); return (PyObject *) op; }
我们注意到源码里面有一个缓冲池,是的,创建PyListObject对象时,会先检测缓冲池free_lists里面是否有可用的对象,有的话直接拿来用,否则通过malloc在系统堆上申请。缓冲池中最多维护80个PyListObject对象。
1 2 3 4 5 #ifndef PyList_MAXFREELIST #define PyList_MAXFREELIST 80 #endif static PyListObject *free_list[PyList_MAXFREELIST];
根据之前的经验我们知道,既然能从缓存池中获取,那么在执行析构函数的时候也要把列表放到缓存池里面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static void list_dealloc (PyListObject *op) { Py_ssize_t i; PyObject_GC_UnTrack(op); Py_TRASHCAN_SAFE_BEGIN(op) if (op->ob_item != NULL ) { 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对象时,实际上是分为两步的,先创建PyListObject对象,然后创建底层数组,最后让PyListObject对象中的ob_item成员指向这个底层数组。同理,在销毁一个PyListObject对象时,先销毁ob_item维护的底层数组,然后再释放PyListObject对象自身(如果缓存池已满的情况下)
。
现在可以很清晰地明白了,原本空荡荡的缓存池其实是被已经死去的PyListObject对象填充了,在以后创建新的PyListObject对象时,Python会首先唤醒这些死去的PyListObject对象,给它们一个洗心革面、重新做人的机会。但需要注意,这里缓存的仅仅是PyListObject对象,对于底层数组,其ob_item已经不再指向了。从list_dealloc中我们看到,PyListObject对象在放进缓存池之前,ob_item指向的数组就已经被释放掉了,同时数组中指针指向的对象的引用计数会减1。所以最终数组中这些指针指向的对象也大难临头各自飞了,或生存、或毁灭,总之此时和PyListObject之间已经没有任何联系了。但是为什么要这么做呢?为什么不连底层数组也一起维护呢?可以想一下,如果继续维护的话,数组中指针指向的对象永远不会被释放,那么很可能会产生悬空指针的问题,所以这些指针指向的对象所占的空间必须交还给系统(前提是没有其它指针指向了)
。
但是实际上,是可以将PyListObject对象维护的底层数组进行保留的,即只将数组中指针指向的对象的引用计数减1,然后将数组中的指针都设置为NULL,不再指向之前的对象了,但是并不释放底层数组本身所占用的内存空间。因此这样一来,释放的内存不会交给系统堆,那么再次分配的时候,速度会快很多。但是这样带来一个问题,就是这些内存没人用也会一直占着,并且只能供PyListObject对象的ob_item指向的底层数组使用,因此Python还是为避免消耗过多内存,采取将底层数组的内存交换给了系统堆这样的做法,在时间和空间上选择了空间。
元组的底层结构–PyTupleObject 因为元组比较简单,和列表比较相似,所以就放在一起介绍了。我们知道元组,就相当于不支持元素添加、修改、删除等操作的列表。
元组的实现机制非常简单,可以看做是在列表的基础上删除了增删改
等操作。既然如此,那要元组有什么用呢?毕竟元组的功能只是列表的子集。元组存在的最大一个特点就是,它可以作为字典的key、以及可以作为集合的元素。因为字典和集合存储数据的原理是哈希表,字典和集合我们后续章节会说。对于列表这样的可变对象来说是可以动态改变的,而哈希值是一开始就计算好的,显然如果支持动态修改的话,那么哈希值肯定会变,这是不允许的。所以如果我们希望字典的key是一个序列,显然元组再适合不过了。
从tuple的特点也能看出:tuple的底层是一个变长对象,但同时也是一个不可变对象。
1 2 3 4 typedef struct { PyObject_VAR_HEAD PyObject *ob_item[1 ]; } PyTupleObject;
可以看到,对于不可变对象来说,它底层结构体定义也非常简单。一个引用计数、一个类型、一个指针数组。这里的1可以想象成n,我们在PyLongObject中说过。
并且我们发现不像列表,元组没有allocated,这是因为它是不可变的,不支持resize操作。至于维护的值,同样是指针组成的数组,数组里面的每一个指针都指向了具体的值。
PyTupleObject的创建 正如列表一样,Python创建PyTupleObject也提供了类似的初始化方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 PyObject * PyTuple_New (Py_ssize_t size) { PyTupleObject *op; Py_ssize_t i; if (size < 0 ) { PyErr_BadInternalCall(); return NULL ; } #if PyTuple_MAXSAVESIZE > 0 if (size == 0 && free_list[0 ]) { op = free_list[0 ]; Py_INCREF(op); #ifdef COUNT_ALLOCS tuple_zero_allocs++; #endif return (PyObject *) op; } if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL ) { free_list[size] = (PyTupleObject *) op->ob_item[0 ]; numfree[size]--; #ifdef COUNT_ALLOCS fast_tuple_allocs++; #endif #ifdef Py_TRACE_REFS Py_SIZE(op) = size; Py_TYPE(op) = &PyTuple_Type; #endif _Py_NewReference((PyObject *)op); } else #endif { if ((size_t )size > ((size_t )PY_SSIZE_T_MAX - sizeof (PyTupleObject) - sizeof (PyObject *)) / sizeof (PyObject *)) { return PyErr_NoMemory(); } op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size); if (op == NULL ) return NULL ; } for (i=0 ; i < size; i++) op->ob_item[i] = NULL ; #if PyTuple_MAXSAVESIZE > 0 if (size == 0 ) { free_list[0 ] = op; ++numfree[0 ]; Py_INCREF(op); } #endif #ifdef SHOW_TRACK_COUNT count_tracked++; #endif _PyObject_GC_TRACK(op); return (PyObject *) op; }
和PyListObject初始化类似,同样需要做一些类型检测,内存是否溢出等等。
当然有了列表的经验,元组的一些底层操作我们就不分析了,它是列表的子集。
静态资源缓存 列表和元组两者在通过索引查找元素的时候是一致的,但是元组除了能作为字典的key之外,还有一个特点,就是分配的速度比较快。一方面是因为由于其不可变性,使得在编译的时候就确定了,另一方面就是它还具有静态资源缓存的作用。
对于一些静态变量,比如元组,如果它不被使用并且占用空间不大时,Python 会暂时缓存这部分内存。这样,下次我们再创建同样大小的元组时,Python 就可以不用再向操作系统发出请求,去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。你可以理解为PyTupleObject对象在被析构时,不仅对象本身没有被回收,连底层的指针数组也被缓存起来了。
1 2 3 4 5 6 7 8 from timeit import timeitt1 = timeit(stmt="x1 = [1, 2, 3, 4, 5]" , number=1000000 ) t2 = timeit(stmt="x2 = (1, 2, 3, 4, 5)" , number=1000000 ) print (round (t1, 2 )) print (round (t2, 2 ))
可以看到用时,元组只是列表的五分之一。这便是元组的另一个优势,可以将资源缓存起来。