Python列表:初学者应该懂得操作和内部实现(文末附赠教程分享)
简单的操作
掌握了列表的创建以及访问,我们接下来去了解下更重要的一点,“列表的增删改”。
在绝大多数情况下,你创建的列表是动态的。这就意味着你的列表创建之后,会随着程序的运行增删元素。比如,你创建了家庭的年龄列表,随着时间的推移,对应成员的年龄元素会递增,再或者家里娶了媳妇那么这个列表是不是就应该增加一个呢?
0x00、添加元素
在实际的代码中,我们有两种添加元素的情况:
一种是在列表尾部添加,另一种是在列表中插入元素。
在列表尾部添加元素,我们可以用到 append() 这个方法:
>>> friends=['Mark','Alison','Kobe'] >>> print(friends) ['Mark', 'Alison', 'Kobe'] >>> friends.append('Jordan') >>> print(friends) ['Mark', 'Alison', 'Kobe', 'Jordan'] >>>
方法 append()将元素 'Jordan' 添加到了列表尾部,而不影响列表中的其他所有元素。
在实际开发中,我们可以先创建一个空列表,再结合使用一系列的 append() 语句添加元素:
>>> friends=[] >>> friends.append('Mark') >>> friends.append('Alison') >>> friends.append('Kobe') >>> print(friends) ['Mark', 'Alison', 'Kobe'] >>>
这种创建列表的方式在代码中极其常见,因为经常要程序运行后,你才知道用户要在程序中存储哪些数据。所以为控制用户,你可以先创建一个空列表,然后将用户提供的数据 append 到空列表中。
在列表中插入元素,在 Python 中有个 insert 方法来实现这种情况。你可以指定新元素的索引和值:
>>> friend=['Mark','Alison'] >>> friend.insert(1,'Kobe') >>> print(friend) ['Mark', 'Kobe', 'Alison'] >>>
0x01、修改列表元素
其实修改列表的元素和访问列表元素的语法类似。修改元素只需要指定列表名和元素的索引即可:
>>> friends=['Mark','Alison'] >>> friends[0]='xiyouMc' >>> print(friends) ['xiyouMc', 'Alison'] >>>
这样,我们就修改了索引为 0 的值。当然,你可以修改任何元素。
0x02、从列表中删除元素
要删除列表中的元素,大概有两种方式:
一种是使用 del 语句,另一种是 pop 方法。
若要有 del 语句删除列表元素,首先你需要知道准备删除元素的索引值:
>>> friends=['Mark','Alison','Kobe'] >>> del friends[2] >>> print(friends) ['Mark', 'Alison'] >>>
像这样,我们使用 del 语句就删除了索引为 2 的元素 'Kobe'。
pop ,这个方法支持两种删除方式。在数据结构上来讲 Python 的列表,那么就可以将其理解为栈,通过 pop 也就是弹出,我们可以将栈顶或者尾部元素弹出列表中:
>>> friends=['Mark','Alison','Kobe'] >>> friends.pop() 'Kobe' >>> print(friends) ['Mark', 'Alison'] >>>
可以看到,我们在 pop 的时候会拿到尾部的元素。所以你可以猜到 pop 函数其实就是从列表中拿出尾部的元素来使用,当然它会将尾部的元素也删除掉。
pop 的另一种方式有点类似于 del 语句,它也是首先需要指定要删除的元素索引:
>>> friends=['Mark','Alison','Kobe'] >>> friends.pop(0) 'Mark' >>> print(friends) ['Alison', 'Kobe'] >>>
这段代码我们就将列表中索引为 0 的元素 pop 了出来,并将其从列表中删除掉。
综上两种删除方式,在实际代码中,如果你无法确定使用哪种方式来删除,可以参考这个标准:如果你要从列表中删除一个元素,并且不再以任何方式使用它,那么就用 del 语句;如果你要在删除元素后还能继续使用它,那么就使用 pop 方法。
至此列表的概念和使用都已经讲完了,接下来让我们更加深入的去理解列表的内部实现。
这里多说句我个人学习新技术、新语言的态度,学习一门新的语言多数情况下我们都会去学习它怎么使用。但若你想使用的更好,那么一定要深入到它的内部实现原理,掌握了它的原理才能更好使用这门语言。
列表的内部实现
说到 Python 列表的内部实现,其实也就是 CPython 对列表的实现。既然要说 CPython,那么肯定就是 C 相关的。
0x00、数据结构
在 CPython 中,其实列表对象是一个 C 语言中的数据结构。没有 C 基础的读者,我可以简单做个介绍,在 C 程序中数据的传递都是有一定结构的,有了这个结构那么我们就可以在代码中更好的处理数据。所以,这也就叫做数据结构。
那么 Python 列表的数据结构是怎么样的?
typedef struct { PyObject_VAR_HEAD PyObject **ob_item; Py_ssize_t allocated; } PyListObject;
其中可以看到 ob_item,这是指向列表元素的指针数组,allocated 是指申请的内存的槽的个数。
简单的说下就是列表对象在 C 程序中的数据结构:有一个指针数组用来保存列表元素的指针,和一个可以在列表中放多少元素的标记。
0x01、列表初始化
上面有提过,列表的初始化可以直接指定参数,也可以初始化一个空列表,a = []
那么 CPython 是怎么实现这个初始化动作的,来看看:
arguments: size of the list = 0 returns: list object = [] PyListNew: nbytes = size * size of global Python object = 0 allocate new list object allocate list of pointers (ob_item) of size nbytes = 0 clear ob_item set list's allocated var to 0 = 0 slots return list object
可以看到,先分配一个对象的内存块,同时将这个内存块的大小置零。再给这个对象分配一个内存槽的大小。
这里说下,上面说到在列表的数据结构中有个内存的槽的个数。这个个数并不是当前列表就有这么多的元素。列表元素的个数和 len(列表)是一样,就是真正的元素的个数。但分配的槽的大小,会比元素个数大一点,目的就是为了防止在每次添加元素的时候都去调用分配内存的函数。那么接下来看看 Append 函数。
0x02、Append 函数
再次回顾下 Append 函数,它是一个可以在列表中添加元素的函数。
>>> friends=[] >>> friends.append('Mark') >>> friends.append('Alison') >>> print(friends) ['Mark', 'Alison'] >>>
那么这个 Append 函数在 CPython 中又是怎么实现的呢?
其实在 CPython 底层有一个函数 app1 ,就是提供给列表做 Append 操作的。比如我们给一个列表添加一个整数:l.append(1)
arguments: list object, new element returns: 0 if OK, -1 if not app1: n = size of list call list_resize() to resize the list to size n+1 = 0 + 1 = 1 list[n] = list[0] = new element return 0
它会先拿到当前列表的大小,同时调用 list_resize() 重新设置一个新的 size。那么 list_resize() 函数是什么?
arguments: list object, new size returns: 0 if OK, -1 if not list_resize: new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) = 3 new_allocated += newsize = 3 + 1 = 4 resize ob_item (list of pointers) to size new_allocated return 0
可以看到它会多申请一些内存,这样也就避免了多次调用该函数。总的槽大小也就是4,同时第一个位置的数据就是 1。来看看下面这张我手绘的图:
可以看到,L[0] 就是我们新添加的元素,同时多分配出来的内存用虚线表示,它代表已经分配但是未使用的内存。
接下来,我们继续 append(2),调用 list_resize 函数,参数为 n + 1 = 2,但因为已经申请了四个空间,所以不需要再次申请内存。同样的 append(3) 和 append(4) 是一样的。
0x03、Insert 函数
基于上面的例子,我们在这个列表的索引为 1 的位置插入一个新的元素。这时候 CPython 会调用 ins1() 这个函数:
arguments: list object, where, new element returns: 0 if OK, -1 if not ins1: resize list to size n+1 = 5 -> 4 more slots will be allocated starting at the last element up to the offset where, right shift each element set new element at offset where return 0
虚线的方框依旧是申请未使用的槽空间。现在可以看到已经有了 8 个槽空间,但是列表的大小却是 5。
0x04、Pop 操作
在 Python 的列表中,Pop 函数就是取出列表的最后一个元素,在 CPython 对应的函数是 listpop() 函数。
在这个函数还是会调用到 list_resize 函数,如果取出列表元素后的列表大小小于分配的槽空间数的一半,那么将会缩减列表的大小。
arguments: list object returns: element popped listpop: if list empty: return null resize list with size 5 - 1 = 4. 4 is not less than 8/2 so no shrinkage set list object size to 4 return last element
可以看到调用 pop 函数之后,虽然索引为 4 的位置已经被取出了,但是在列表中 4 的索引仍然指向原来的整数对象,只不过列表的大小变成了 4。
接下来我们再调用 pop 函数:
由于再次 pop 之后,元素的个数小于了槽空间的一半,所以CPython 将这个列表的槽空间缩减到了 6 。同时你依然可以看到原索引还是指向原数据内存。这也就是说用 pop 的话,你的程序里面就可以持续使用这个数据。
最后,想学习Python的小伙伴们!
请关注+私信回复:“学习”就可以拿到一份我为大家准备的Python学习资料!
pytyhon学习资料
python学习资料