




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第Python内建类型list源码学习目录问题:1常用方法小结:题外话:2list的内部结构:PyListObject3尾部操作和头部操作3.1尾部操作3.2头部操作4浅拷贝和深拷贝4.1浅拷贝4.2深拷贝4.3直接赋值4.4小结个人总结:TODO:5动态数组5.1容量调整5.2append()5.3insert()5.4pop()5.5remove()6一些问题
问题:
深入认识Python内建类型这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型。
list是日常开发中最常用的内建类型之一,掌握好它的底层知识,无论是对数据结构基础知识的理解,还是对开发效率的提升,应该都是有一定帮助的
list对象支持哪些操作?时间复杂度、空间复杂度分别是多少?试分析append和insert这两个典型方法的时间复杂度。头部添加元素时性能较差,如何解决?
1常用方法
大家对于list应该是比较熟悉的,我们先列举一些常用的方法:
append:向尾部追加元素
l=[1,2,3]
l.append(4)
[1,2,3,4]
pop:弹出元素(默认从尾部弹出元素,也可以通过index参数从指定位置弹出)
l=[1,2,3,4]
l.pop()
[1,2,3]
l=[1,2,3,4]
l.pop(0)#指定index
[2,3,4]
insert:在指定位置插入元素
l=[1,2,3]
l.insert(0,4)
[4,1,2,3]
index:查找指定元素第一次出现位置的下标
l=[1,2,3,4]
l.index(1)
extend:用一个可迭代对象扩展列表元素逐一追加到尾部
l=[1,2,3]
l.extend({1:2,3:4})
[1,2,3,1,3]
count:计算元素出现的次数
l=[1,2,3,1]
l.count(1)
l.count(2)
reverse:将列表反转(注意这里是直接在原列表上进行操作,可以和切片区分下)
l=[1,2,3]
l.reverse()
[3,2,1]
clear:将列表清空
l=[1,2,3]
l.clear()
小结:
list的操作总体比较简单,但是要注意的是:由于list底层是由数组实现的,对应的各类插入和删除操作就会由于数组的特型而在复杂度上有所差别,例如:通过insert()在头部插入元素时,需要挪动整个列表,此时时间复杂度为O(n),而append()直接在尾部插入元素时,时间复杂度为O(1)。在使用时要注意时空复杂度问题(后续我会结合源码详细介绍)。
题外话:
list的操作有很多,后续我会对典型操作进行源码分析,还有很多操作可能就需要大家自行去学习了解了,但是本质上都是大同小异的,掌握好数组以及Python对此在源码处理上的一些技巧就能熟练掌握了。这里我结合自己的学习、工作、面试经历,总结了一点问题和心得:
list为什么可以使用负数索引从源码角度看是因为判断了索引输入为负数的情况,会做一个相应的处理:索引值+列表长度。例如:索引为-1,列表长度为3,最终的索引就是2,也就是最后一个元素的索引。对比一下Java中的数组,如果使用负数索引,会报错索引越界,在网上查能否有相关方法实现负数索引:有人说用一个类来包装,也有的说用一个字典去映射负数和正确的索引,也有的直接给出了无法做到的答案。相关的做法应该是有很多的,但同时也不要忽视了安全性等相关问题。这里提出这个点也是我自己觉得比较有趣的一个地方吧,索引值+列表长度其实是一个很简单的做法,但总感觉又有那么一点一般人想不出来的巧妙,hh以pop()和insert()为例,这两个方法有一个共同的参数:索引。对于pop(),如果索引值大于等于列表长度,则会报错;而对于insert(),如果索引值大于等于列表长度则统一将元素插入到列表最后。从源码角度看其实就是insert()对于索引参数做了一个判断处理,当它大于列表长度时,会将索引值更改为列表长度,例如:index=5,而list=[1,2,3],源码处理时,会将index置为3。所以如果开发者乐意的话,应该可以对pop()也采用相同的操作,我个人认为这里应该是有其他的考虑的,例如安全性等问题,当然也有可能只是写成了这样而已,hhreverse(),reversed(),切片的区别。个人认为本质上这三者其实没啥关系,大家如果容易弄混可能还是因为不够熟练。重点问题可能在切片以及深拷贝、浅拷贝的问题上,后续我会详细介绍相关内容。append()和extend()的区别。面试题好像会问这个,也是很基础的知识了。如果换个角度问可能会更有趣些:l.append(3)和l.extend(3)的效果是不是一样的?答:不一样,后者会直接报错,hh
不知不觉就写了这么多,相比上次写str相关的内容还是要顺畅多了(捂脸)。相关的小知识应该是还有很多的,大家可以在学习的过程中多找一些问题和资料来融会贯通,当然我个人认为如果能把底层的逻辑搞清楚,其他的应该都不成问题~
2list的内部结构:PyListObject
源码如下:
typedefstruct{
PyObject_VAR_HEAD
/*Vectorofpointerstolistelements.list[0]isob_item[0],etc.*/
PyObject**ob_item;
/*ob_itemcontainsspacefor'allocated'elements.Thenumber
*currentlyinuseisob_size.
*Invariants:
*0=ob_size=allocated
*len(list)==ob_size
*ob_item==NULLimpliesob_size==allocated==0
*list.sort()temporarilysetsallocatedto-1todetectmutations.
*ItemsmustnormallynotbeNULL,exceptduringconstructionwhen
*thelistisnotyetvisibleoutsidethefunctionthatbuildsit.
Py_ssize_tallocated;
}PyListObject;
源码分析:
ob_item:二维指针,指向动态数组的指针,数组保存元素对象指针allocated:动态数组总长度,即列表当前的容量ob_size:当前元素个数,即列表当前的长度
这里出现了一些新的概念:动态数组,容量。后续我会对此进行介绍,这里大家对照示意图应该就能有比较初步的认识了:
3尾部操作和头部操作
认识了list的底层结构之后,这里在强调一下尾部操作和头部操作的一些问题,巩固一下数组相关的数据结构知识,也顺便引出一些动态数组的相关内容。
3.1尾部操作
假设列表对象l内部数组长度为5,当前保存了2个元素,分别是1,2。当我们调用append()方法向尾部追加元素时,由于内部数组还未用满,只需将新元素保存于下一个可用位置,并更新ob_size字段。因此,绝大多数情况下,append()方法的性能都足够好,时间复杂度为O(1)。
若list对象内部数组已满,则再添加元素时就需要进行扩容。append()等方法在操作时都会对内部数组进行检查,如需扩容,则会重新分配一个长度更大的数组并替换旧数组。因此,当使用append()方法遇到扩容时,会将列表元素从旧数组拷贝到新数组,此时时间复杂度为O(n)。
个人想法:这里引出这个问题是因为我个人当时在学习到这个知识点时,意识到了一个问题:怎么扩容。如果没记错的话Java面试题中也会经常闻到动态扩容。这里抛开面试不谈,我的想法主要是在怎么去避免频繁扩容,毕竟扩容一次就需要拷贝所有的元素。这在日常开发中应该也是一个需要大家关注的问题:像这种类似的消耗突增的操作,我们需要去避免,降低它的触发频率,但同时也不能忽视时空上的消耗。
3.2头部操作
与尾部相比,由于在列表头部增删元素需要挪动其他元素,性能差别就很大了(数组的基础知识)
这里既然提到了头部操作,我们来看一下Python中的队列:
通过list实现栈是很好的,但是用来实现队列是很不合理的:(LeetCode上的用栈实现队列除外,hh)
q=[]
q.append(1)
q.append(2)
q.append(3)
#deque
q.pop(0)
这样的队列实现,在出队操作时会移动所有队列元素(除pop掉的元素以外),性能很差
如果需要频繁进行头部操作,可以使用标准库中的deque,这是一种双端队列结构:
fromcollectionsimportdeque
q=deque()
#enqueue
q.append(job)
#deque
q.popleft()
4浅拷贝和深拷贝
浅拷贝和深拷贝应该是容器相关的一个比较重要的知识点了,无论是Python还是Java中都会涉及,面试中应该也比较常见。(其实可以单独写一篇博客来介绍这部分内容,这里就先放在了list中来介绍,后续会考虑重新总结归纳出一篇博客)
4.1浅拷贝
调用list对象的copy方法,可以将列表拷贝一份,生成一个新的列表,但是不会拷贝列表中的元素。结合源码来说就是:会拷贝一下底层数组中指向具体元素对象的指针,但是元素对象不会被拷贝。
下面通过对浅拷贝后的列表进行修改来实际体会一下:
示例1:
l1=[1,[1,2,3]]
[1,[1,2,3]]
#浅拷贝
l2=l1.copy()
[1,[1,2,3]]
#修改新列表中的可变元素
l2[1][0]=6
[1,[6,2,3]]
[1,[6,2,3]]
示例2:
l1=[1,[1,2,3]]
[1,[1,2,3]]
#浅拷贝
l2=l1.copy()
[1,[1,2,3]]
#修改新列表中的不可变元素
l2[0]=2
[2,[1,2,3]]
[1,[1,2,3]]
示例3:
l1=[1,[1,2,3]]
[1,[1,2,3]]
#浅拷贝
l2=l1.copy()
[1,[1,2,3]]
#这里要注意并没有修改可变元素[1,2,3],而是将l2[1]处的指针修改了,指向了一个新对象[3,4]
l2[1]=[3,4]
[2,[3,4]]
[1,[1,2,3]]
list对象的底层数组保存的是元素对象的指针,copy()方法复制底层数组时,拷贝的也是元素对象的指针,而不会拷贝具体的元素对象。因此,新旧列表对象的数组中的指针指向的还是相同的对象。图示如下:
4.2深拷贝
通过copy模块中的deepcopy()函数可以进行深拷贝,deepcopy()函数会递归复制所有容器对象,确保新旧列表不会包含同一个容器对象(这里要注意元组是比较特殊的,稍微会说明)
下面通过对深拷贝后的列表进行修改来实际体会一下:
fromcopyimportdeepcopy
l1=[1,[1,2,3]]
[1,[1,2,3]]
#深拷贝
l2=deepcopy(l1)
[1,[1,2,3]]
l2[1][0]=5
[1,[5,2,3]]
[1,[1,2,3]]
l2[0]=2
[2,[5,2,3]]
[1,[1,2,3]]
图示如下:
4.3直接赋值
除了深拷贝、浅拷贝外,最常见的操作就是直接赋值,即对象的引用(别名),这里就不涉及到复制操作了。
4.4小结
直接赋值:b=a
浅拷贝:b=a.copy()
深拷贝:b=copy.deepcopy(a)
个人总结:
弄清楚list底层数组中存储的是指向对应元素的指针,以及深拷贝时的递归,我觉得就能对相关的知识点有一个比较清晰的认识了。但是对于Python中的元组需要特殊考虑,它是一个不可变的容器,当元组中含有可变数据类型的容器和不含时,深拷贝的情况是有区别的。
本质上元组是不会去创建新对象的,因为它不可变(这里我没有找到百分百的证据,主要是根据经验和查到的资料,大家可以去看一下源码),但是当元组中还有可变数据类型的容器,就又会由于深拷贝递归去拷贝相应的对象而导致重新创建一个新的元组对象。
这里可能比较绕,大家可以自行去打印看一下结果。但是我个人觉得核心就是弄清楚list底层数组中存储的是指向对应元素的指针,以及深拷贝时的递归。
TODO:
后续我会重新写一篇博客来专门将深浅拷贝的源码列出来,来看看为什么对于元组就会特殊处理。
要是我们的列表元素是自定义的数据类型又是如何处理的。深拷贝递归复制时判断的条件到底是如何写的。
5动态数组
这一部分的内容也是这篇博客最主要的重点:分析list底层数组的特性。前面我们已经介绍了list的常用操作、底层结构,以及以list为例简单介绍了深浅拷贝。这一小节会结合源码详细介绍list底层动态数组的特性,我个人觉得这一设计也传达了我们在业务开发上的一个比较常用和关键的思想。
5.1容量调整
前面已经提到,当我们调用append()、pop()、insert()等方法时,列表长度会发生变化。当列表长度超过底层数组容量时,便需要对底层数组进行扩容;当列表长度低于底层数组容量并且达到某个值时,便需要对底层数组进行缩容。
append()等方法是依赖list_resize()函数来调整列表长度的。源码如下:
staticint
list_resize(PyListObject*self,Py_ssize_tnewsize)
PyObject**items;
size_tnew_allocated,num_allocated_bytes;
Py_ssize_tallocated=self-allocated;
/*Bypassrealloc()whenapreviousoverallocationislargeenough
toaccommodatethenewsize.Ifthenewsizefallslowerthanhalf
theallocatedsize,thenproceedwiththerealloc()toshrinkthelist.
if(allocated=newsizenewsize=(allocated1)){
assert(self-ob_item!=NULL||newsize==0);
Py_SIZE(self)=newsize;
return0;
/*Thisover-allocatesproportionaltothelistsize,makingroom
*foradditionalgrowth.Theover-allocationismild,butis
*enoughtogivelinear-timeamortizedbehavioroveralong
*sequenceofappends()inthepresenceofapoorly-performing
*systemrealloc().
*Thegrowthpatternis:0,4,8,16,25,35,46,58,72,88,...
*Note:new_allocatedwon'toverflowbecausethelargestpossiblevalue
*isPY_SSIZE_T_MAX*(9/8)+6whichalwaysfitsinasize_t.
new_allocated=(size_t)newsize+(newsize3)+(newsize93: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;
return0;
源码分析:
重要参数:
items指针:用于保存新数组new_allocated:用于保存新数组容量num_allocated_bytes:用于保存新数组内存大小,单位为字节allocated:用于保存旧数组容量
重要步骤:
第12行:检查新长度与底层数组容量的大小关系。如果新长度不超过现有数组容量,并且大于等于现有容量的一半,则不会更新数组容量,只修改ob_size即可。从这里我们可以得知list对象的扩缩容条件:
扩容条件:新长度大于底层数组容量缩容条件:新长度小于底层数组容量的一半
第27行:新容量=新长度+1/8*新长度+3或6(取决于新长度是否小于9)
第28~31行:如果新容量超过了允许的最大值,则返回错误
第33~34行:如果新长度为0,则新容量也为0,此时底层数组为空
第36~40行:调用PyMem_Realloc()函数重新分配底层数组
第41~44行:依次设置新底层数组、新长度、新容量
注:
在扩容时先增加1/8,再增加3或6,是为了有效限制扩容频率,避免list对象长度较小时会频繁扩容(我个人在工作中没有遇到需要考虑类似问题的需求,都是在写业务代码(捂脸),但是我认为这种思想还是值得学习的)
PyMem_Realloc()函数是Python内部实现的内存管理函数之一,功能和C的库函数realloc()类似。主要步骤如下:
PyAPI_FUNC(void*)PyMem_Realloc(void*ptr,size_tnew_size);
新申请一块大小为new_size的内存区域将数据从旧内存区域ptr拷贝到新内存区域释放旧内存区域ptr返回新内存区域
图示如下:
5.2append()
append()方法在Python内部由C函数list_append()实现,而list_append()进一步调用app1()函数完成元素追加
app1()函数源码如下:
staticint
app1(PyListObject*self,PyObject*v)
Py_ssize_tn=PyList_GET_SIZE(self);
assert(v!=NULL);
if(n==PY_SSIZE_T_MAX){
PyErr_SetString(PyExc_OverflowError,
"cannotaddmoreobjectstolist");
return-1;
if(list_resize(self,n+1)0)
return-1;
Py_INCREF(v);
PyList_SET_ITEM(self,n,v);
return0;
源码分析:
第4行:调用PyList_GET_SIZE()获取列表当前长度,即ob_size
第7~11行:如果列表当前长度达到了最大值PY_SSIZE_T_MAX,则报错
第13~14行:调用list_resize(),必要时进行扩容
第16行:增加元素对象引用计数(这里是列表引用了该元素对象)
第17行:调用PyList_SET_ITEM()将元素对象指针保存在列表的最后一个位置
5.3insert()
insert()方法在Python内部由C函数list_insert_impl()实现,而list_insert_impl()进一步调用ins1()函数完成元素插入
ins1()函数源码如下:
staticint
ins1(PyListObject*self,Py_ssize_twhere,PyObject*v)
Py_ssize_ti,n=Py_SIZE(self);
PyObject**items;
if(v==NULL){
PyErr_BadInternalCall();
return-1;
if(n==PY_SSIZE_T_MAX){
PyErr_SetString(PyExc_OverflowError,
"cannotaddmoreobjectstolist");
return-1;
if(list_resize(self,n+1)0)
return-1;
if(where0){
where+=n;
if(where0)
where=0;
if(wheren)
where=n;
items=self-ob_item;
for(i=n;--i=where;)
items[i+1]=items[i];
Py_INCREF(v);
items[where]=v;
return0;
源码分析:
第19~25行:检查插入位置下标,如果下标为负数,则加上n将其转化为非负数。如果转化后的where值仍然小于0,则代表越界,但是这里会直接将where设置为0(即插入到列表头部),而没有报错。若下标为正数,也会判断是否越界,越界时则插入到列表尾部,同样不会报错。
第26~28行:将插入位置以后的元素逐一往后移动一个位置。(这里的for循环是从后往前迭代的)
5.4pop()
pop()方法将指定下标的元素从列表中弹出,下标默认为-1,即默认弹出最后一个元素
pop()方法在Python内部由list_pop_impl()函数实现。源码如下:
staticPyObject*
list_pop_impl(PyListObject*self,Py_ssize_tindex)
/*[clinicendgeneratedcode:output=6bd69dcb3f17eca8input=b83675976f329e6f]*/
PyObject*v;
intstatus;
if(Py_SIZE(self)==0){
/*Special-casemostcommonfailurecause*/
PyErr_SetString(PyExc_IndexError,"popfromemptylist");
returnNULL;
if(index0)
index+=Py_SIZE(self);
if(index0||index=Py_SIZE(self)){
PyErr_SetString(PyExc_IndexError,"popindexoutofrange");
returnNULL;
v=self-ob_item[index];
if(index==Py_SIZE(self)-1){
status=list_resize(self,Py_SIZE(self)-1);
if(status=0)
returnv;/*andvnowownsthereferencethelisthad*/
else
returnNULL;
Py_INCREF(v);
status=list_ass_slice(self,index,index+1,(PyObject*)NULL);
if(status0){
Py_DECREF(v);
returnNULL;
returnv;
源码分析:
第12~13行:如果给定下标为负数,则加上列表长度,转化为普通下标
第14~16行:检查下标是否合法,如果越界则报错
第19~25行:如果待弹出元素为列表的尾部元素,则调用list_resize()直接处理(比较巧妙,hh)
第26~31行:其他情况下调用list_ass_slice()删除元素,调用前需要通过Py_INCREF增加元素的引用计数,因为在list_ass_slice()函数内部会释放被删除元素
5.5remove()
remove()方法将给定元素从列表中删除,与pop()不同,remove()直接删除给定元素,而不是通过下标进行索引
remove()方法在Python内部由C函数list_remove()实现。源码如下:
staticPyObject*
list_remove(PyListObject*self,PyObject*value)
/*[clinicendgeneratedcode:output=f087e1951a5e30d1input=2dc2ba5bb2fb1f82]*/
Py_ssize_ti;
for
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 师资共享与教育信息化建设及人才培养协议
- 盘扣式脚手架租赁与现场安全管理服务协议
- 电子烟企业产品召回与消费者权益保护服务合同
- 股权激励与员工持股计划实施协议
- 碳中和战略规划与实施指导协议
- 政府基础设施建设项目材料供应合同
- 视频号网红电商合作运营协议
- 犯罪所得财产分割与追缴流程协议
- 影视作品改编权及衍生品生产市场推广合同
- 亲子早教中心儿童美术教育项目合作协议
- 《摄影构图技巧》课件
- 员工手册-沃尔玛
- 行政或后勤岗位招聘笔试题及解答
- 【MOOC】信号与线性系统-华中科技大学 中国大学慕课MOOC答案
- “人工智能+”山区学校校本课程开发(丽水学院)知道智慧树章节答案
- 中医体重管理
- 高血压危象课件
- 民航行业智能化民航运输与服务方案
- 新版加油站全员安全生产责任制
- 工程机械智能化安全系统
- 广东省广州三校2023-2024学年高二下学期期末考试+物理试卷(含答案)
评论
0/150
提交评论