☰
Python列表操作:从创建、append到insert中间插入的实战详解
2026/10/13 13:51:43 网站建设 项目流程

上周有位刚学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'],或者在必须显式表达“新建一个列表类型”的时候。

创建完后,按题目要求,把六个名字加进去。最自然的方式是按下标的顺序append:

names = [] 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_girl",index默认返回第一个匹配的下标。这在实际业务里可能并不是你想要的。如果你想精确控制插到哪一个特定对象前面,不要只靠值去定位,最好结合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是按值删,遇到重复值只会删第一个且不支持反向查找。

第二个动作是查找。查找一个元素是否在列表中,最直观的是用in:

"black_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,把每个方法当成积木,在练习里来回拼装,比看十遍教程都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询