☰
Python列表操作完全指南:从基础用法到高频踩坑实录
2026/10/7 17:31:23 网站建设 项目流程

只要写过几年代码,你就会发现不管用什么语言,最终都绕不开一个东西——容器。而Python里最常用、最灵活、也最容易上手的就是列表(list)。它既能装数字、字符串,也能装列表本身,还能随时增删改查、切片反转、排序去重。我见过很多初学者把列表当成一个普通的“数组”来用,结果遇到浅拷贝、循环删除、嵌套展开等问题时一头雾水。这篇文章我想把列表操作的完整知识体系拆开讲一遍,从创建初始化到增删改查,从切片到排序,从推导式到嵌套展开,再到高频踩坑实录,帮你把列表这块彻底吃透。

这篇文章适合这么几类人:刚学Python不久、想在基础阶段把列表用扎实的新手;写过一段时间但总在“切片赋值”“深拷贝”“去重保序”这些细节上翻车的老手;还有准备面试、正在复习Python基本功的朋友。参考的版本以Python 3.8+为主,3.10以后的新特性我也会顺带提一下。列表看起来简单,但真正用好的标准是:你不仅知道每个方法怎么调用,还知道它在底层做了什么、什么时候该用什么、什么时候不能用什么。下面直接进入正文。

1. 列表的整体认知:为什么它是Python的顶梁柱

1.1 列表的本质:一个“可以随意改的架子”

先说本质。列表在Python里是一种动态数组,底层是连续内存上的一组指针(PyObject*)。这意味着它有两个特点:第一,可以装任意类型,因为存的不是值本身而是引用;第二,可以在尾部快速追加元素,因为底层预留了额外空间。这两点决定了列表的使用方式和C语言数组完全不同。

你可以拿一个超市货架来类比。C语言数组相当于固定长度的货架,摆多少格就是多少格,想多加一格得重新装修;Python列表则像一个可以自由伸缩的货架,往里放东西时自动扩容,往外拿东西时自动缩紧,而且每层格子大小不一样也没关系,一层放苹果一层放电饭煲都可以。

不过要注意,这个“自动伸缩”不是无代价的。列表在尾部追加是O(1)摊还复杂度,但在头部或中间插入就是O(n)了,因为插入点之后的元素都要整体往后挪。理解这个以后,你就知道为什么频繁在头部插入数据时要考虑collections.deque,而不是无脑用list.insert(0, x)。

1.2 创建列表的几种姿势,别再只会a = []

列表的初始化方式比你想象的多,每种都有自己的适用场景。

# 最基础的字面量 lst1 = [1, 2, 3, "hello", [4, 5]] # 通过range生成数字序列 lst2 = list(range(10)) # [0, 1, 2, ..., 9] # 字符串转列表 lst3 = list("python") # ['p', 'y', 't', 'h', 'o', 'n'] # 重复元素 lst4 = [0] * 5 # [0, 0, 0, 0, 0] # 列表推导式(后面会细讲) lst5 = [x * x for x in range(5)] # [0, 1, 4, 9, 16]

这里有个新手指南里经常被忽略的坑:[0] * 5生成的五个元素都指向同一个对象。如果列表里是不可变对象(比如整数、字符串),没问题;但如果写的是[[]] * 3,得到的是三个指向同一个空列表的引用,往其中一个添加元素,其它两个也会跟着变。这在后面讲深拷贝时还要专门强调。

1.3 索引:正向从0开始,负向从-1开始,但别越界

Python列表的索引规则看起来简单,实际上暗藏不少细节。正向索引从0开始,负向索引从-1开始(-1是最后一个元素)。你需要记住的是:list[len(list)]一定会报IndexError,所以遍历时要么用for x in lst,要么用range(len(lst)),要么用enumerate。

我特别想提醒你的是,索引不光可以“取”,还可以“改”:

lst = [1, 2, 3, 4, 5] lst[0] = 100 # 替换第一个元素 lst[-1] = 500 # 替换最后一个元素 print(lst) # [100, 2, 3, 4, 500]

这种通过索引直接赋值的能力,是列表作为“可变序列”的核心体现。理解“可变”和“不可变”的区别,是理解列表一切操作的关键。字符串是不可变序列,你不能str[0] = 'a';列表是可变序列,所以一切修改操作才能成立。

2. 增删改查:基本功里藏着的性能逻辑

2.1 追加用append,合并用extend,插入用insert

列表的“增”有四个主要方法:append、extend、insert、以及+运算符。很多初学者分不清append和extend,我来用一句话说清楚:append把参数作为一个整体放进列表,extend把参数里的每个元素拆开依次放进列表。

a = [1, 2, 3] a.append([4, 5]) # [1, 2, 3, [4, 5]] b = [1, 2, 3] b.extend([4, 5]) # [1, 2, 3, 4, 5]

如果你想知道哪个性能更好,我直接说结论:extend比多次append快得多。原因在于extend在底层是批量拷贝元素,一次性完成扩容和赋值,而多次append要重复检查容量、触发扩容。数据量小的时候差异不明显,但如果你在一个循环里往列表里塞几万条数据,用extend配合生成器,效率和代码可读性都会好很多。

insert是另一个极端——它可以在任意位置插入元素,但插入位置越靠前,后续元素就必须整体后移,代价越大。所以如果你需要频繁在开头插入元素,务必改用collections.deque的appendleft。

2.2 删除元素:remove、pop、del,以及那个经典的循环删除坑

“删”的方法也多,选错了一样出事故。

lst = [1, 2, 3, 4, 5] # remove按值删除(只删第一个匹配项) lst.remove(3) # pop按索引删除,并返回被删的值 val = lst.pop(0) # del按索引或范围删除,不返回任何值 del lst[1]

这里的经典坑是“在循环中删除元素”。比如你想把列表里所有偶数删掉:

lst = [1, 2, 3, 4, 5, 6] for x in lst: if x % 2 == 0: lst.remove(x) print(lst) # 你以为会是[1,3,5],实际得到[1, 3, 5]

等等,这个例子好像对了?其实这个例子只是碰巧对了。换一个集合就会出问题:

lst = [1, 2, 3, 4, 5, 6, 8, 10] for x in lst: if x % 2 == 0: lst.remove(x) print(lst) # 你以为会是[1,3,5],实际得到[1, 3, 5, 10]?不对,再仔细想想

问题的根源是:你边遍历边删除,列表的索引在每次删除后都改变了,但循环内部的迭代计数器并不会自动回退,导致被删除元素的后一个元素被跳过。最稳妥的方案是用列表推导式创建新列表,或者倒序遍历删除:

# 推荐方式1:列表推导式(后面专门讲) lst = [x for x in lst if x % 2 != 0] # 推荐方式2:倒着遍历 for i in range(len(lst) - 1, -1, -1): if lst[i] % 2 == 0: del lst[i]

我个人强烈建议:凡是要“按条件筛选”,一律用列表推导式;凡是务必要原地改,用倒序删除或先标记再删除。

2.3 查的效率和in运算符的真相

判断一个元素在不在列表里,用in运算符是最自然的方式。但你要知道,in是对列表做线性扫描,时间复杂度是O(n)。如果列表很大,频繁做if x in lst查询,性能会非常感人。这时候你就应该考虑把列表转成集合set(lst),集合的in查询是O(1)的。

但是有代价:转集合会去重、会丢失顺序(严格说,Python 3.7以后dict保序,set仍然无序)、并且要求元素可哈希。如果你既要保序又要去重又要快速查询,可以用dict.fromkeys(lst),这一点在后面“去重”部分专门演示。

3. 切片:列表操作里最优雅、也最容易被误解的部分

3.1 完整的切片语法,不只是list[i:j]

切片可以说是Python序列类型最具特色的操作。完整语法是list[start:stop:step],三个参数都可以省略。规则总结成一句话:左闭右开,包前不包后。

lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 基本切片 lst[2:5] # [2, 3, 4] lst[:4] # [0, 1, 2, 3] lst[4:] # [4, 5, 6, 7, 8, 9] lst[:] # 整个列表的浅拷贝 # 步长切片 lst[::2] # [0, 2, 4, 6, 8] lst[::-1] # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] lst[1:8:3] # [1, 4, 7] # 负数索引 lst[-3:] # [7, 8, 9] lst[:-1] # 去掉最后一个元素

切片会创建新列表(浅拷贝),所以切片操作本身不会影响原列表。这一点让很多人在复制时直接用lst2 = lst[:],但只顾着“防住”了,没意识到切片本质上只是浅拷贝——它拷贝的是元素的引用,如果列表里嵌套着列表,内层列表的修改会同时影响两个外层列表。这个问题在3.4里正式讲。

3.2 切片赋值:批量替换数据的隐藏利器

切片的“读”只是它一半的价值,把切片用在赋值语句左侧才是真正的进阶用法。你可以用切片一次性替换、删除、插入多个元素:

lst = [0, 1, 2, 3, 4, 5] # 替换 lst[1:4] = [10, 20] # 把长度为3的切片替换成长度为2的列表 print(lst) # [0, 10, 20, 4, 5] # 删除切片 lst[1:3] = [] # []就是删除 print(lst) # [0, 4, 5] # 插入元素 lst[1:1] = [100, 200] # 在idx=1位置插入 print(lst) # [0, 100, 200, 4, 5]

看到没有?lst[1:1] = [100, 200]实现了“在指定位置插入多个元素”的效果,而且一行代码搞定。如果你想在头部插入一段数据,lst[:0] = [...]就等同deque.extendleft,但性能上不推荐大列表这么做。切片赋值的背后逻辑是:把右侧的列表展开,替换掉切片指定的区域,左右两侧列表长度不需要相等。

3.3 反转列表的三种姿势:reverse()vs[::-1]vsreversed()

反转列表也是一个高频操作。三者有明显区别:

lst = [1, 2, 3, 4, 5] # 方式1:原地反转,不返回值 lst.reverse() # lst变成[5, 4, 3, 2, 1] # 方式2:切片反转,返回新列表,原列表不动 reversed_lst = lst[::-1] # 方式3:reversed()返回一个反向迭代器 it = reversed(lst) # 惰性求值,需要list(it)才变成列表

我的建议是:如果你要改原列表,用lst.reverse();如果你要一个新列表,用lst[::-1];如果你只是想在循环里从尾部往头部遍历,用reversed(lst)更省内存。尤其注意reversed(lst)返回的是迭代器而不是列表,这是新手极容易踩的坑。

3.4 浅拷贝与深拷贝:列表拷贝的底层逻辑

这一步不搞清楚,后面做嵌套列表处理时会吃大亏。先说结论:lst.copy()、lst[:]、list(lst)都只是浅拷贝,它们会创建一个新列表,但列表里的每一个元素仍然是原对象的引用。

怎么理解?以嵌套列表为例:

a = [[1, 2], [3, 4]] b = a.copy() b[0].append(999) print(a) # [[1, 2, 999], [3, 4]]

你明明拷的是a,结果改b的时候a也变了。因为b和a里的两个子列表是同一个对象。如果你希望完全独立的副本,必须用深度拷贝:

import copy a = [[1, 2], [3, 4]] c = copy.deepcopy(a) c[0].append(999) print(a) # [[1, 2], [3, 4]],不受影响

深度拷贝也不是万能灵药:它比浅拷贝慢得多,而且如果列表里有自定义对象、文件句柄、线程锁之类的对象,深度拷贝会直接失败。所以标准做法是:能浅拷贝就浅拷贝,只有明确要“彻底独立”的时候才用deepcopy。

4. 排序与比较:不只是sort()那么简单

4.1sort()与sorted()的本质区别

排序是个经典高频需求,Python提供了sort()方法和内置函数sorted()。它们的区别可以用一句话概括:list.sort()是原地排序,直接修改原列表,返回None;sorted(list)是排序后返回新列表,原列表不变。

lst = [5, 2, 9, 1] lst.sort() # lst变成[1, 2, 5, 9],该方法返回None lst2 = [5, 2, 9, 1] new_lst = sorted(lst2) # lst2还是[5, 2, 9, 1],new_lst是[1, 2, 5, 9]

为什么说这是面试题里的高频考点?因为很多人会写出lst = lst.sort()这种代码,然后发现lst变成了None。记住:如果你想保留原列表,用sorted();如果原列表不需要了,用sort()省内存。

4.2 复杂排序:key参数是排序的灵魂

不管是sort()还是sorted(),都支持key和reverse两个参数。key接收一个函数,这个函数的返回值将作为排序依据。

words = ["banana", "apple", "cherry", "date"] words.sort(key=len) # 按字符串长度排序 print(words) # ['date', 'apple', 'banana', 'cherry'] students = [ {"name": "Tom", "score": 82}, {"name": "Jerry", "score": 95}, {"name": "Alice", "score": 78}, ] students.sort(key=lambda s: s["score"], reverse=True) # 按成绩从高到底排序

这里值得注意的技巧:如果要对多重条件排序,最简单的做法是让key返回一个元组。比如先按成绩降序、成绩相同再按名字字母序:

students.sort(key=lambda s: (-s["score"], s["name"]))

Python会从左到右依次比较元组里的每个元素,这个特性在面试和实际工作中都特别实用。再补充一点:如果你需要“稳定的排序”,Python的sort()本身就是稳定的,这意味着两个排序关键字相同的元素,排序后相对位置不会变化。这个特性在“先按一个字段排,再按另一个字段排”的多级排序时可以直接依赖。

4.3 列表比较的大小规则

你可能没注意过,列表之间可以直接用比较运算符比较大小。规则是:从第一个元素开始逐对比较,直到分出胜负;如果一个列表是另一个的前缀,短的更小。

[1, 2, 3] < [1, 2, 4] # True,因为3 < 4 [1, 2] < [1, 2, 3] # True,因为多的更长 ["a", "b"] > ["A", "B"] # True,因为'a'的ASCII码更大

这个规则在很多算法题里会被用到,比如按字典序排列多个列表、比较JSON数组的先后顺序等。理解起来不难,但如果你不知道列表可以比较,就可能写出不必要的循环。

5. 列表推导式:让代码从“能跑”变成“优雅”

5.1 推导式的基本结构与多层嵌套

列表推导式是Python最值得学习的语法糖之一。它的本质是把for循环和append合并成一行表达式。基本结构是[expression for item in iterable if condition]。

# 普通写法 squares = [] for i in range(10): squares.append(i * i) # 推导式写法 squares = [i * i for i in range(10)]

推导式里可以加if条件,也可以加if-else;还可以用两层for实现类似双重循环的效果:

# 过滤偶数 evens = [x for x in range(20) if x % 2 == 0] # 条件赋值 labels = ["even" if x % 2 == 0 else "odd" for x in range(5)] # 两层循环——类似嵌套for pairs = [(x, y) for x in range(3) for y in range(3)]

需要注意的是,推导式的可读性随着复杂度增长会急剧下降。如果表达式超过一屏,或者for层数超过两层,那就老老实实写普通循环。代码是写给人看的,不是用来炫技的。

5.2 推导式的性能优势和费内存的坑

推导式通常比等价的for+append循环快一些,原因是在for循环里每次append都要进行一次名称查找和方法调用,而推导式在底层做了优化。

但这不意味着推导式永远是好选择。比如你要处理一亿个数字、然后只用到结果的前几个,用推导式会一次性把一亿个元素的列表全部放进内存,这显然是个灾难。这种场景应该用生成器表达式:

# 这是一个生成器表达式,不是列表推导式 gen = (x * x for x in range(10**8)) print(next(gen)) # 惰性求值,特别省内存

判断方法很简单:方括号[ ... ]是列表推导式,圆括号( ... )是生成器表达式。后续遍历都一样,但生成器只能迭代一次,不能索引。

5.3 推导式的过滤和变换结合

我实际操作中最喜欢的场景是把“过滤”和“变换”放在一步完成:从一堆数据里过滤出符合条件的数据、同时修改它们的格式。

data = [ {"name": "Alice", "score": 92}, {"name": "Bob", "score": 65}, {"name": "Cathy", "score": 88}, ] # 找出及格的人,只保留名字大写 passed_names = [p["name"].upper() for p in data if p["score"] >= 60] print(passed_names) # ['ALICE', 'BOB', 'CATHY']

这种写法在处理日志分析、数据清洗时会非常常用。它的执行顺序是先for遍历,再if过滤,最后把前面的表达式计算出来加入新列表。顺序一旦搞混,结果就会有偏差。

6. 嵌套列表与多维结构:从邻接矩阵到矩阵转置

6.1 用嵌套列表表示矩阵和邻接矩阵

列表里套列表是Python中表示二维数组最自然的方式。比如一个3x3的矩阵:

matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ] # 访问第i行第j列 matrix[1][2] # 6

在算法和数据结构里,嵌套列表常被用来表示图论里的邻接矩阵:adj[i][j]表示节点i到节点j是否有边。构建一个n阶邻接矩阵,最稳妥的写法是:

n = 5 adj = [[0] * n for _ in range(n)] # 不要写:adj = [[0] * n] * n adj[0][1] = 1 print(adj)

这里就回到1.2节提到的陷阱:[[0] * n] * n创建的每一行都是同一个列表对象的引用,改一行等于改全部。用列表推导式[[0] * n for _ in range(n)]才会创建n个独立行。这是构建邻接矩阵、棋盘、二维表格时最常见的坑,没有之一。

6.2 嵌套列表的展开:从递归到列表推导式

把嵌套列表变成一维列表,行业内叫“展开(flatten)”。最简单的情况是只有两层的嵌套列表:

lst = [[1, 2], [3, 4, 5], [6]] flattened = [x for sub in lst for x in sub] print(flattened) # [1, 2, 3, 4, 5, 6]

如果嵌套层数是任意的,比如[1, [2, [3, [4, [5]]]]],就得用递归。手写一个递归展开有时要十来行,但Python有个优雅得让人哭笑不得的办法——用json模块的dumps和loads:

import json def flatten_json(arr): return json.loads(json.dumps(arr).replace("[", "").replace("]", "").split(","))

不过这种做法只适用于纯数字或纯字符串,遇到复杂结构会直接翻车。我的建议是:数据是标准两层嵌套就用列表推导式,层数不确定就用递归,别偷懒走json歪路。

6.3 矩阵转置与zip的配合

矩阵转置是嵌套列表操作的经典场景。最Pythonic的写法是用zip配合*解包:

matrix = [ [1, 2, 3], [4, 5, 6], ] transposed = list(zip(*matrix)) print(transposed) # [(1, 4), (2, 5), (3, 6)]

注意这里得到的是元组列表而不是列表列表。如果你要的必须是列表:

transposed = [list(row) for row in zip(*matrix)]

zip(*matrix)的工作原理是:*matrix把matrix的每一行作为独立参数传给zip,然后zip把每个可迭代对象的第i个元素组合成元组。这种“解包+拉链”的组合非常常用,处理多个列表的并行遍历时也靠它。

7. 列表与其它数据结构的联动

7.1 列表与字符串的互转

把一个字符串拆成字符列表,用list(s);把一个由字符串组成的列表拼回字符串,用"".join(lst)。这是最基础的操作,但join的使用频率高得惊人。

s = "python" chars = list(s) # ['p', 'y', 't', 'h', 'o', 'n'] back = "".join(chars) # 'python' words = ["hello", "world"] line = ", ".join(words) # 'hello, world'

需要注意:join只接受可迭代对象里的元素是字符串。如果列表里有整数,直接"".join([1, 2, 3])就会报TypeError,得先把数字转成字符串——比如用推导式"".join(str(x) for x in lst)。

字符串拆分一般用split()方法,它的返回值天然就是列表:

s = "apple,banana,orange" parts = s.split(",") # ['apple', 'banana', 'orange']

7.2 列表与集合的去重逻辑

列表去重是面试里非常常见的小题。如果你不在乎顺序,最简单的写法是:

lst = [3, 1, 2, 1, 3, 4] unique = list(set(lst)) # 结果顺序不保证

如果你要保留原始顺序,标准做法是用dict.fromkeys():

lst = [3, 1, 2, 1, 3, 4] unique = list(dict.fromkeys(lst)) print(unique) # [3, 1, 2, 4]

dict.fromkeys(lst)创建了一个以lst元素为键、值为None的字典,字典键天然去重且顺序保留(Python 3.7+的字典保序),转回列表就完成了保序去重。这个技巧实用、简洁、而且不需要引入collections。如果列表元素数量非常大,你还可以用collections.OrderedDict做同样的事情,但从Python 3.7开始原生dict就够用了。

7.3 列表与队列、栈的角色切换

有人总喜欢给列表贴上“数组”的标签,实际上它也可以当栈和队列用。用append+pop()就是标准的栈(LIFO);用append+pop(0)是队列(FIFO),但前面说过pop(0)是O(n),性能差。真要队列,用collections.deque:

from collections import deque q = deque([1, 2, 3]) q.append(4) # 尾部进 q.popleft() # 头部出,O(1)

记住了:列表适合当栈,不太适合当队列。这个区分在写BFS(广度优先搜索)的时候尤其重要,我见过正是因为用了pop(0)让整个算法在大数据量下卡到怀疑人生的案例。

8. 常见问题与排查技巧实录

8.1 高频报错速查表

错误信息出现原因解决方案
IndexError: list index out of range索引超出列表长度检查索引上界,或改用lst[-1]、enumerate遍历
ValueError: list.remove(x): x not in list删除的值不存在先if x in lst判断,或用lst.pop(idx)按索引删
TypeError: 'NoneType' object is not iterable对list.sort()返回值做遍历sort()返回None,要用sorted(lst)
TypeError: 'int' object is not subscriptable列表里的元素是整数却用了[i][j]先检查元素类型,确保嵌套结构一致
NameError: name 'x' is not defined推导式变量作用域理解错误推导式内变量不会泄漏到外部,检查拼写

这张表里的错误我几乎都帮人调过,其中sort()返回None这个坑出现频率最高。它的本质是把“方法原地修改后不返回新值”这个设计理解反了。

8.2 可变对象作为默认参数和“共享引用”问题

列表是可变对象,所以当它被函数作为默认参数时会出现一个经典陷阱:

def add_item(item, lst=[]): lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2] —— 你猜到了吗,是同一个列表!

默认参数在函数定义时只创建一次,后续调用如果没有传入新列表,用的都是同一个列表对象,所以数据被累积了。正确做法是默认参数设为None,在函数体里重新赋值:

def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst

这个坑在工作两年以上的人手里也可能犯,它的根源是“可变对象的引用共享”。同理,不要轻易把一个列表直接赋给另一个变量期望它们互不影响,要用copy()或deepcopy()明确复制。

8.3 大列表性能排查的几点经验

最后分享几个我实测下来的经验。第一,判断元素是否在列表里,如果列表过大,务必转集合,但转集合本身也有一次O(n)遍历,所以只适合“转一次、查多次”的场景。第二,如果你要批量拼接多个列表,用itertools.chain可以省内存:

from itertools import chain lst = list(chain([1, 2], [3, 4], [5]))

第三,如果列表里元素同构且大量存在,array模块或numpy的ndarray是更好的选择,存储密度高、计算快。列表是通用容器,但通用意味着它对特定数据类型没有优化。第四,频繁插入头部或者频繁删除头部,换deque;频繁按键查找,换dict;频繁按值查找且元素可哈希,换set。列表不是唯一选择,选用正确的容器往往比优化列表操作更有效。

用一句话总结我个人这几年在项目里实操的体会:列表操作学到后面,拼的不是API背得多熟,而是什么时候该用列表、什么时候该换别的数据结构,以及修改数据时脑子里能浮现出内存里那些指针的指向关系。把“引用”“拷贝”“复杂度”这三件事时刻放在心上,Python列表就再也不是一个能让你翻车的地方。

如果你正在学Python,建议把这篇文章里的代码自己敲一遍,尤其是切片赋值、循环删除、嵌套列表初始化这几个片段,每一个都能在现场面试里救你一命。后面如果大家有兴趣,我还可以接着写字典、集合、元组的操作专题,再把它们的性能和适用场景做一个完整对比。

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

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

立即咨询