Python列表操作:从创建、append到insert中间插入的实战详解

发布时间:2026/10/13 13:51:50
Python列表操作:从创建、append到insert中间插入的实战详解 上周有位刚学Python的学员给我发来一道练习题标题里的需求写了一半就断了创建一个空列表命名为names往里面添加old_driver、rain、jack、shanshan、peiqi、black_girl这些元素然后在names列表里black_girl前面插入一……后面没了。我一看就猜到他要说的是“插入一个元素”这几乎是每本Python入门教材里都会出现的列表操作题。题目本身不难但如果你只是把参考答案抄一遍、运行一遍、看到结果一样就关掉那这道题就白做了。这短短半句话背后牵扯出的是Python列表从创建、追加到定点插入的一整套基本功也是之后写算法、处理数据时几乎每天都会碰到的操作。这篇文章就顺着这个练习往下挖把列表操作的每个细节讲透顺便把我平时教学里常见的坑一并排掉。1. 题目背后的事实先会读写列表才能谈算法先把题目本身拆开看。这个练习一共考了三个动作创建空列表、往列表末尾添加元素、在指定元素前面插入新元素。用到的数据结构是Python里最基础、也最常用的list列表。列表的底层逻辑可以理解成一排带编号的格子编号从0开始。第一个格子叫索引0第二个叫索引1依次往后排。每个格子里存的是指向真实数据的“标签”而不是数据本身。这个设计带来的结果是列表可以有不同长度的数据也可以随时增删改查灵活性很高。但也正因为这种连续编号的存储方式在中间插入一个元素时后面的全部格子里的小标签都得往后挪一位这就是后面要讲的性能问题了。为什么练习里非要你先创建一个空列表再用append往里面添元素而不是直接写成一行names [old_driver, rain, jack, shanshan, peiqi, black_girl]因为真实开发里数据往往不是一开始就齐全的。可能是从文件里一行行读进来的可能是接口返回后边遍历边收集的也可能是在循环里根据条件决定加不加某一个值。这种“先建一个空容器再往里面动态装东西”的写法才是列表最常见的用法。直接初始化一个写死的列表更多用于常量配置或者测验而不是日常数据流水线的样子。这道题真正的价值是让你建立两个直觉往列表末尾加一个元素用append它永远是O(1)的快速操作往列表中间插入一个元素用insert它需要让后续元素全部右移。把这个直觉在脑子里扎根以后再遇到“在某个位置前插一条数据”的需求就不会去问“是不是直接把索引写死就行”这种问题了。写代码的第一步永远是搞清楚数据从哪里来、到哪里去而不是急着敲键盘。顺带一提标题里“black_girl前面插入一”这种半截需求在真实项目里特别常见。需求方经常自己也没想清楚要插什么、插几个、插什么样的数据。这时候第一件事不是写代码而是问清楚插入的元素是什么是单个数还是整个列表如果原来就有多个叫black_girl的元素插到第几个前面这些都想明白了代码自然就出来了。2. 空列表的诞生与第一批元素入库append、extend、直接复制的差别创建空列表在Python里有两种等价写法names [] # 或者 names list()两者都能得到空列表但我推荐你直接写方括号。它更简洁执行速度也更快更重要的是所有Python程序员都认这个写法。list()这个写法更适合用在需要把其他类型转成列表的场景比如list(abc)会把字符串拆成[a, b, c]或者在必须显式表达“新建一个列表类型”的时候。创建完后按题目要求把六个名字加进去。最自然的方式是按下标的顺序appendnames [] names.append(old_driver) names.append(rain) names.append(jack) names.append(shanshan) names.append(peiqi) names.append(black_girl) print(names) # 输出: [old_driver, rain, jack, shanshan, peiqi, black_girl]append每次只接收一个参数作用是把传入的对象当作整体追加到列表末尾。这里每个人名字是字符串追加完后列表顺序就是你添加的顺序这一点务必记住列表不是集合它是有顺序的而且允许重复。如果你想一次性加多个元素有两个常见选择extend和号。names [] names.extend([old_driver, rain, jack, shanshan, peiqi, black_girl])extend接收一个可迭代对象列表、元组、字符串都行然后把它里面的每个元素依次取出来逐个追加到原列表里。注意extend是原地修改names不会返回新列表。还有另一个写法用加号names [] names names [old_driver, rain, jack, shanshan, peiqi, black_girl]加号会产生一个全新的列表再把新列表赋值回names。如果names原本很大这种写法会额外复制一份内存性能不如extend。但在某些场景里你不想动原列表、想生成一个新列表时加号是合适的。我把这三个常用方式理成了一张对比表方便你区分方式原地修改原列表一次可加的数量返回结果适用场景append(x)是只能1个None修改原列表循环中逐条添加extend(iterable)是可迭代对象里的多个None合并一个批量数据序列list list否两个列表合并新列表不想改动原列表、需要新对象时新手最容易犯的错是写出names.append(alex, jim)这种代码以为可以一次传两个参数。运行后会立刻报错TypeError: append() takes exactly one argument (2 given)正确做法要么拆成两行append要么用extend加一个列表要么定义一个新列表再用加号合并。报错信息本身已经把原因说得很清楚了这也是我常跟初学者讲的一句话报错不是坏事读报错信息是编程里最重要的能力之一。3. 在 black_girl 前面插入index 先找到座位insert 再插队题目要求把新元素插到black_girl前面。假设这里插入的元素是alex练习时你完全可以换成别的名字。我的实现思路分两步先拿到black_girl当前的位置再在那个位置调用insert。names [] names.append(old_driver) names.append(rain) names.append(jack) names.append(shanshan) names.append(peiqi) names.append(black_girl) target names.index(black_girl) names.insert(target, alex) print(names) # 输出: [old_driver, rain, jack, shanshan, peiqi, alex, black_girl]这里最关键的是index方法。names.index(black_girl)会返回black_girl在列表中第一次出现的位置。在这道题里黑色girl位于索引5也就是第6个位置。拿到target之后names.insert(target, alex)的含义是把alex插到索引target的位置上原来索引target及之后的所有元素整体向右移动一位。所以最终alex出现在black_girl之前列表顺序符合预期。为什么不直接把insert的位置写成5因为列表里的数据是会变的。今天六个元素时black_girl在索引5明天你在前面加了一个人它就变索引6了。如果用死数字代码马上出错或者插错位置。通过index动态查找才是通用写法。这也是我反复强调的能用“找位置”就不要“猜位置”。insert的索引规则值得展开讲讲很多人在这上面栽跟头。如果index等于0新元素插到整个列表的最前面。如果index等于列表长度len(names)效果等同于append新元素会出现在末尾。如果index大于len(names)Python不会报错而是直接把元素放到列表末尾行为和index过大时一致。如果index是负数比如-1表示从右边数的第1个位置插到这个位置之前。注意names.insert(-1, alex)不是把alex放到列表末尾而是放到倒数第一个元素前面。想让alex成为最后一个元素应该写names.insert(len(names), alex)或者干脆用append。还有一个和索引相关的点list允许重复元素。如果列表里有多个black_girlindex默认返回第一个匹配的下标。这在实际业务里可能并不是你想要的。如果你想精确控制插到哪一个特定对象前面不要只靠值去定位最好结合id、序号或其他唯一标识来定位。初学者暂时不需要写那么复杂但心里要有这根弦。这个例子里原列表末尾正好是black_girl所以你也可以写成names.insert(-1, alex)得到同样的结果。但一旦black_girl不在末尾-1就会插到你意想不到的位置。养成用index找索引、再insert的习惯是最稳的。4. 插队需要付出的代价insert 的 O(n) 魔法与追尾场景把insert想象成在排队买票的队伍里插队。你站在队尾加进去后面的人都不受影响队伍只是多了一个人。但如果你非要插到队伍中间那被插队位置以后的所有人每个人都要往后退一步。排队队伍越长这一步造成的整体移动就越明显。Python列表的底层结构也是这个道理。列表的数据在内存里是一块连续区域每个元素槽位都有固定的大小存放的是对对象的引用。当insert发生在中间位置从插入点起后面的所有元素都要在内存里“向右平移”一格为新元素腾地方。这就解释了为什么insert不是免费的它的时间开销和插入位置后面的元素个数成正比。用复杂度表示就是O(n)n是列表长度。相比之下append往末尾追加时只要末尾还有空闲容量就只做一次写入时间复杂度是O(1)。即便列表预先分配的空间用完了也只是一次整体扩容平摊下来仍然接近O(1)。这个性能差距不是理论空谈我在一个模拟场景里实测过import time lst list(range(100000)) t1 time.perf_counter() lst.append(100000) t2 time.perf_counter() print(末尾追加耗时:, t2 - t1) t1 time.perf_counter() lst.insert(0, -1) t2 time.perf_counter() print(头部插入耗时:, t2 - t1)在我这台机器上尾部追加是微秒级头部插入则明显高一两个数量级。当列表长度为十万时单次可能感受不明显但如果在一个循环里反复往头部插入比如一万次那就是O(n^2)级别的灾难整个程序会卡到让你怀疑人生。遇到这种情况我一般会看插入的场景来选方案只需要在头部频繁插入换成collections.deque它有appendleft方法两端操作都是O(1)需要批量插入大量数据与其在循环里反复insert不如一次性构建一个新列表最后再整体替换必须从后往前凑数据时先从尾部append最后用reverse或[::-1]倒置如果一定要在中间插入且数据量大可以考虑改用链表类数据结构但Python内置不提供通常需要引入第三方库日常业务里很少真用到。对这道练习题来说六个元素的列表insert一下毫无压力。但知道为什么insert“贵”能让你将来面对几十万条数据时心里有数不会写出“往头部插十万次”这种代码。另外insert是原地修改列表并返回None的。所以不要写这种names names.insert(2, alex)执行完之后names会变成None因为insert的返回值是None你再用names做什么都会报错。这个坑我见过太多次了几乎每期教学都有人踩。记住凡是原地修改的方法比如append、extend、insert、sort、reverse它们都是返回None不是在返回新列表。需要保留原列表时记得先复制一份再用这些方法。5. 这个练习做完之后建议你顺手掌握的四个动作删除、查找、切片、复制插入只是列表操作的一小块。练习题做完了我建议你别急着关页面沿着这个names列表继续折腾四个扩展动作。这些动作和插入一样常用组合起来能解决绝大部分实际需求。第一个动作是删除。列表里有几个方法可以删除元素names.remove(jack)删除第一个值为jack的元素。如果找不到抛ValueError。names.pop(2)删除并返回索引2处的元素。如果不传参数默认删除最后一个元素。del names[0]删除索引0处的元素不返回任何值。names.clear()清空整个列表让names变成[]。举个例子如果你想把black_girl从“尾部”挪到“头部”可以用pop加insert组合girl names.pop() # 删除最后一个元素返回black_girl names.insert(0, girl) # 把它插到最前面这比先remove再insert更精准因为remove是按值删遇到重复值只会删第一个且不支持反向查找。第二个动作是查找。查找一个元素是否在列表中最直观的是用inblack_girl in names # True rain in names # True tom in names # False而index不仅能判断存在还能返回位置。建议配合try/except使用否则找不到元素时会抛异常try: pos names.index(rain) print(rain在列表中的位置是, pos) except ValueError: print(rain不在列表里)这是日常开发里很标准的写法先尝试再做异常兜底。第三个动作是切片。切片可以从列表里拿出一段子列表语法是names[开始:结束:步长]其中结束位置是开区间不包含在内。# 继续使用包含alex后的names # [old_driver, rain, jack, shanshan, peiqi, alex, black_girl] sub names[0:3] # 前三个元素 print(sub) # [old_driver, rain, jack] tail names[-3:] # 最后三个元素 print(tail) # [peiqi, alex, black_girl] reverse_names names[::-1] # 倒序 print(reverse_names) # [black_girl, alex, peiqi, shanshan, jack, rain, old_driver]切片返回的是新列表不会影响原列表。这个特性在调试时非常好用你可以随时随地切一段出来看而不必担心误改数据。第四个动作是复制。你可能会觉得拷贝列表很简单直接用等号赋值new_names names但不管你信不信等号并没有复制列表。new_names和names指向的是同一个内存对象你改new_names会直接改到names。这是Python里经典的引用问题。要真正复制一份内容相同的独立列表用这些方法new_names_1 names.copy() new_names_2 names[:] new_names_3 list(names)这三种方式在单层列表上是等价的。但names列表里只有字符串属于最浅的层次。如果列表里嵌套了列表、字典这类可变对象上面的复制只是浅拷贝内层对象仍然是共享的。这时候要用copy模块的deepcopy。import copy matrix [[1, 2], [3, 4]] matrix_copy copy.deepcopy(matrix)我建议初学者先把浅拷贝和等号赋值的区别刻进脑子里这是后面写程序时最容易犯的隐藏错误。很多人查询了半天最后发现是赋值后不小心把原始数据污染了那种滋味很不好受。6. 我在教这个练习时看到学员最常踩的几个坑讲完正题说说实际教学过程中反复出现的几个坑。你如果正在学提前看一眼能省不少时间。第一个坑和标题本身有关题目没写完就开写。这题写的是“black_girl前面插入一”少了一个宾语。有的学员会猜“插入一个字符串”有的猜“插入a”还有人卡住不敢动。真实项目里需求永远比这个更模糊正确的做法是先确认再动手。我上课时会让学员先自己把题目补完整然后解释补出来的依据。比如我建议插alex是因为它常见、和原列表风格一致。这样练习的其实不只是Python还有需求拆解。第二个坑是把append当成万能添加方法。前面提过append只收一个参数。想一次加三个字符串要么三个append要么一个extend。这里重复一遍是因为踩的学员太多# 错误写法 names.append(old_driver, rain, jack) # 正确写法1 names.append(old_driver) names.append(rain) names.append(jack) # 正确写法2 names.extend([old_driver, rain, jack])第三个坑是insert参数顺序写反。insert的语法是insert(索引, 元素)不是insert(元素, 索引)。写成names.insert(alex, 2)会报TypeError。这类错误靠记忆不如靠多看报错信息Python报错里会清楚告诉你传了多少个参数、期望多少个参数。眼疾手快地看报错比死记API更有效。第四个坑是insert之后以为返回了新列表。前面专门讲过insert原地修改并返回None。比如这么写result names.insert(2, alex) print(result) # None一旦你把names赋值给另一个变量之后想保留中间状态就很容易踩到。正确做法是先复制再插入或者先插入完再用原变量。第五个坑是在循环里删除元素导致跳过。这个问题不是题目要求的但太常见了。比如想删除所有包含r的名字# 错误示范 for name in names: if r in name: names.remove(name)由于remove会让后面的元素往前挪for循环内置的索引还在往后走就会跳过一个元素。处理这种需求有更安全的办法最推荐的是列表推导式names [name for name in names if r not in name]列表推导式直接生成一个新列表避免了在循环里改列表长度的麻烦。这是我在这个阶段一定会让学员掌握的写法它比for循环加remove优雅得多也更不容易出错。第六个坑也是最后一个是对重复元素定位不准。如果names列表里有两个rain你执行names.index(rain)只会拿到第一个。想要找第二个就得用enumerate遍历[ (i, name) for i, name in enumerate(names) if name rain ]学列表操作时多想想重复值的情况能让代码健壮度上一个台阶。说实话这个练习题目留给人的改装空间很大。你可以试试自己给原列表加上几个重复名字再实现在第二个black_girl前面插入数据你还可以试试不调用insert用切片加拼接的方式实现同样效果比较两者的写法差异。这些拓展玩得越多你的索引直觉就越准。写列表代码最忌讳的就是死记API把每个方法当成积木在练习里来回拼装比看十遍教程都管用。