来,咱们聊点Python里最“顶”的东西——列表(list)。但凡你打开任何一本Python入门书、任何一套网课视频,前几章一定离不开它。为什么?因为列表是Python序列类型里的“课代表”,你把列表搞透了,后面学元组、学字符串处理、甚至学数据分析的DataFrame索引切片,都会顺畅一大截。
这篇文章,我不会跟你念官方文档,也不会抄书。我会从“序列到底是个啥”讲起,一路拆到列表的创建、索引、切片、遍历、排序、推导式,再带你看几个实际场景里真正能用上的硬核操作,最后把新手最容易踩的坑挨个儿翻出来。全程用大白话加上可复现的代码,你照着敲一遍,列表这块就稳了。
这篇内容适合谁?刚开始学Python的纯新手,学了两三个月还搞不清切片方向的老朋友,以及想隔离“只读拷贝”和“原地修改”的实战派。不管你现在在哪一步,读完这篇,你再看列表会顺眼很多。
1. 序列到底是什么:为什么Python天天把它挂嘴边
很多人一上来就背定义:“序列(Sequence)是Python的一种内置数据类型,包括字符串、列表、元组……”背完就忘。我换个方式给你讲清楚。
所谓序列,本质上就是一串排好队的数据。关键是“排好队”三个字——每个元素都有一个固定的位置编号,你喊一声“三号出列”,它就知道要取谁。这个位置编号,专业说法叫“索引”(Index)。Python里的序列类型,常见就这么几个:字符串(str)、列表(list)、元组(tuple),还有不太常提的range对象。它们的共同点就是支持索引访问、支持切片、可以被for循环直接遍历。
我经常打个比方:序列就像一根晾衣绳,上面晾着一排衣服,每件衣服之间间距是一样的,你站在绳子一头数过去,第几件就是第几件,不会乱。列表是最灵活的那根晾衣绳,因为晾上去的衣服(元素)随时可以取下来换一件,长度也可以随时加长缩短。
1.1 列表在序列家族里的特殊地位
同为序列,为什么列表用得最多?很简单,两个字:可变(Mutable)。
字符串是不可变的——你没法直接修改字符串里的某个字符,只能先生成一个新串。列表不一样,它允许你原地增删改元素。这就像拿着同一根晾衣绳,今天晾T恤,明天换件衬衫,后天再加一条裤子,绳子还是那根绳子,但是状态一直在变。
可变这个特性直接决定了列表的应用场景:
- 需要频繁收集数据并逐步填充 → 列表是首选容器。
- 需要对元素进行增删改查 → 用列表最顺手。
- 数据规模不大、需要保持先后顺序 → 列表天然有序。
顺带说一句,很多新手一开始分不清“列表”和“数组”。Python内置的“list”其实更接近动态数组,它内部是一段连续的内存空间加上自动扩容机制,跟C语言的数组不是一个东西,也跟NumPy里的ndarray不一样。后面我会专门讲一讲这个区别。
1.2 从“序列协议”看通用操作
既然序列有公共协议,那有一批操作,列表、元组、字符串统统支持。你把这些通用操作练熟,很多代码根本不用记API,逻辑自然就出来了。
| 操作 | 示例 | 效果 |
|---|---|---|
| 索引取值 | s[2] | 取第3个元素 |
| 负数索引 | s[-1] | 取最后一个元素 |
| 切片 | s[1:4] | 取第2到第4个元素 |
| 拼接 | s1 + s2 | 拼接两个序列 |
| 重复 | s * 3 | 重复3次 |
| 成员判断 | x in s | x是否在序列中 |
| 长度 | len(s) | 元素个数 |
| 最大值/最小值 | max(s)/min(s) | 取最大最小值 |
| 遍历 | for x in s: | 逐个取出元素 |
这一张表,某种意义上就是“序列的最小知识集”。列表完全覆盖这些操作,而且还能做得更多,接下来我们深入到列表内部去看。
2. 列表的创建和内存模型:别只看表象
2.1 连新手都会的几种创建方式
最基础的,就是直接拿一对方括号括起来:
my_list = [1, 2, 3, "hello", 3.14, [4, 5]]注意看上面这个列表,元素类型可以不一样——整数、字符串、浮点数,甚至嵌套一个子列表,都行。这是Python列表非常自由的一个特点。很多从C语言或者Java转过来的同学一开始会不习惯,但恰恰是这种自由,让Python列表成了“胶水语言”的万能容器。
除了直接写,还有几个高频创建方式:
# 用list()构造,可迭代对象都能转 list_a = list("python") # ['p', 'y', 't', 'h', 'o', 'n'] # 创建连续整数序列 list_b = list(range(10)) # [0, 1, 2, ..., 9] # 列表推导式 list_c = [x ** 2 for x in range(5)] # [0, 1, 4, 9, 16] # 用乘号生成重复元素 list_d = [0] * 5 # [0, 0, 0, 0, 0]这里面,[0] * 5这种写法看着挺方便,但它有个隐藏的坑,新手很容易中招,我后面“避坑”章节专门说。
2.2 列表的底层模型:为什么=复制会翻车
这个问题我必须专门拉出来讲,因为十个新手有九个在这里翻车:你用list_b = list_a复制列表,然后改了list_b,结果list_a也跟着变了。
原因很简单:列表变量存的是对象引用,不是数据本身。你可以理解成变量名是一张写着地址的纸条,list_a和list_b这两张纸条,写的是同一个地址,内存里住的是同一个列表对象。你通过list_b改了房间里的东西,list_a那张纸条指向的还是同一个房间,看到的东西自然也跟着变了。
正确的“真复制”有几种做法:
# 方法一:切片复制 list_b1 = list_a[:] # 方法二:list()构造 list_b2 = list(list_a) # 方法三:copy()方法 list_b3 = list_a.copy() # 方法四:copy模块的深拷贝(列表里还有可变对象时用这个) import copy list_b4 = copy.deepcopy(list_a)前三种是浅拷贝,如果你的列表里嵌套了子列表,修改子列表的内容时,原列表还是会变。想彻底隔离,就得用deepcopy。这个细节,等你以后写爬虫、数据结构算法题的时候,遇到的频率会非常高。
2.3 与数组、元组的核心差异
很多人会问:“列表都这么强了,为什么还要元组?为什么数据科学里又用NumPy数组?”
我给你们一个特别接地气的类比:
- 列表:像你的工作台,东西可以随便放、随便拿、随便换。
- 元组:像一张写好的承诺书,内容不能改,但可以安全地传来传去。
- 数组(NumPy):像工厂生产线,所有零件规格一致,处理起来速度飞快,但不灵活。
元组的不可变性带来两个优势:一是性能略好,二是可以作为字典的键(列表不行,因为列表不可哈希)。NumPy数组则强在“同类型+连续内存+向量化计算”,处理十万个数值时比Python列表快几十倍甚至上百倍。
# 元组不可变,所以下面这句会报错 # t = (1, 2, 3) # t[0] = 100 # TypeError: 'tuple' object does not support item assignment # NumPy数组要求同类型,但不强制你手动保证 import numpy as np arr = np.array([1, 2, 3, 4])所以别把“列表”当万能药,不同容器有不同使命。你把它们的边界搞清楚,选择起来才不会纠结。
3. 索引与切片:想取哪儿就取哪儿
3.1 索引的本质和负数索引
列表的索引从0开始,这应该是所有编程语言学习的第一课。Python比大多数语言更贴心的一点是,它支持负数索引:
lst = ["a", "b", "c", "d", "e"] print(lst[0]) # a print(lst[4]) # e print(lst[-1]) # e print(lst[-2]) # d负数索引的理解方式很简单:-1代表倒数第一个,-2代表倒数第二个。你别死记公式,想象你从队尾往前数,-1是最后一个人,-2是倒数第二个。很多人不习惯负数索引,实际上它特别实用——取列表最后一个元素,lst[-1]永远比lst[len(lst) - 1]优雅得多。
3.2 切片:[start:stop:step]的完整哲学
切片是Python序列的灵魂操作。语法是:
lst[start:stop:step]左闭右开,意思是包含start,不包含stop。这个“不包含stop”的设定,很多人刚学时觉得反人类,但它是故意的。想想看:
lst[:3]表示取前三个元素,你只要写一个3,就知道是0、1、2,配合len()取前N个特别自然。- 如果要实现一个长度正好为3的窗口,直接写
lst[i:i+3]就好,不会多出一个来。
切片有几个常被忽略的实用细节:
lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 取前5个 print(lst[:5]) # [0, 1, 2, 3, 4] # 取后3个 print(lst[-3:]) # [7, 8, 9] # 反向切片(经典面试题) print(lst[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] # 每隔一个取一个 print(lst[::2]) # [0, 2, 4, 6, 8] # 从索引1到索引8,步长为3 print(lst[1:8:3]) # [1, 4, 7]看到lst[::-1]了吗?这就是Python里反转列表最经典的写法。不用你写循环,一行搞定。很多算法题里,字符串反转、列表逆序,全用它。
需要注意的一个点:当step为正数时,切片从左往右取;当step为负数时,从右往左取。比如:
print(lst[8:2:-2]) # [8, 6, 4]这里start是8,stop是2,步长为-2,从右往左数。你要是把方向搞反了,得到的就是空列表:
print(lst[2:8:-1]) # [] —— 因为从2往8走,步长却是-1,方向冲突3.3 切片的“拷贝”属性
记住一个关键点:切片返回的是新列表(浅拷贝)。这就是为什么我们可以用lst[:]来复制列表。同时,切片不会改原列表。
lst = [1, 2, 3, 4, 5] sub = lst[1:4] # sub是全新列表 sub[0] = 99 print(lst) # [1, 2, 3, 4, 5] —— 原列表没变 print(sub) # [99, 3, 4]这个特性在函数传参时格外有用,你传一个切片进去,就不怕函数内部乱改原数据。
4. 列表高频操作:增删改查和遍历的“最佳姿势”
4.1 增删改的标准动作
列表的增删改有几种方法,你不需要全背,但脑子里得有个“什么场景用什么”的映射表。
增加元素:
lst = [1, 2, 3] # 尾部追加一个 lst.append(4) # [1, 2, 3, 4] # 指定位置插入 lst.insert(1, "a") # [1, "a", 2, 3, 4] # 把另一个列表扩展进来 lst.extend([5, 6]) # [1, "a", 2, 3, 4, 5, 6] # 合并成新列表(不修改原列表) new_lst = lst + [7, 8]这里我要特别说一下append和extend的区别,这是新手最容易懵的地方:
append(x):把x作为一个整体加到列表末尾。如果你append一个列表,那列表里会多一个“列表元素”。extend(iterable):把迭代器里的每个元素逐个加入。
lst = [1, 2, 3] lst.append([4, 5]) print(lst) # [1, 2, 3, [4, 5]] —— 注意是嵌套列表 lst2 = [1, 2, 3] lst2.extend([4, 5]) print(lst2) # [1, 2, 3, 4, 5] —— 元素被“摊平”了这两个用混了,数据形态就会完全跑偏,排查起来还特别费劲。
删除元素:
lst = [1, 2, 3, 4, 5] # 按值删除,只删第一个匹配项 lst.remove(3) # [1, 2, 4, 5] # 按下标删除并返回被删的值 val = lst.pop() # 删除并返回最后一个元素 val2 = lst.pop(0) # 删除并返回第一个元素 # del 按索引或切片删除 del lst[0] # 删除第一个 del lst[1:] # 删除索引1到末尾pop()和remove()的区别:pop()是按位置删,而且会返回删除的值;remove()是按值删,找不到会抛ValueError。写代码时要明确自己到底想删哪个。
修改元素:
修改是最简单的,直接下标赋值:
lst = [1, 2, 3] lst[0] = 100 print(lst) # [100, 2, 3] # 还可以对切片整体赋值 lst[1:] = [200, 300, 400] print(lst) # [100, 200, 300, 400]切片赋值有个“以少换多、以多换少”的能力,我没展开讲,但你可以自己试一下lst[1:3] = [9, 9, 9, 9],看看列表长度会怎么变——这就是列表可变特性的魅力。
4.2 查找、排序和反转
查找元素:
lst = [10, 20, 30, 40, 50] # 判断成员 print(20 in lst) # True print(99 in lst) # False # 查找下标,找不到抛ValueError print(lst.index(30)) # 2 # 统计出现次数 lst2 = [1, 2, 2, 3, 2] print(lst2.count(2)) # 3排序和反转:
nums = [3, 1, 4, 1, 5, 9, 2, 6] # 原地排序,升序 nums.sort() print(nums) # [1, 1, 2, 3, 4, 5, 6, 9] # 原地排序,降序 nums.sort(reverse=True) print(nums) # [9, 6, 5, 4, 3, 2, 1, 1] # 生成新列表,不修改原列表 sorted_nums = sorted(nums) # 原地反转 nums.reverse() # 生成新列表反转 reversed_nums = list(reversed(nums))sort()和sorted()的区别要记牢:sort()是列表的方法,原地排序、不返回新列表、原列表顺序变了;sorted()是Python内置函数,会返回一个排好序的新列表,原列表不变。很多初学者在这个地方写出莫名其妙的bug,就是因为用了lst = lst.sort(),结果lst变成None。
4.3 遍历列表的几种姿势
遍历太常用了,但遍历的姿势也分好几种,不同场景选不同的:
lst = ["a", "b", "c"] # 方式一:直接遍历元素 for item in lst: print(item) # 方式二:需要下标时用enumerate for i, item in enumerate(lst): print(i, item) # 方式三:只用下标 for i in range(len(lst)): print(i, lst[i]) # 方式四:同时遍历两个列表 names = ["张三", "李四"] scores = [88, 95] for name, score in zip(names, scores): print(name, score)重点推荐enumerate和zip,这俩是Python里非常“地道”的内置工具。新手最常犯的错误是纠结“我需要下标”时用range(len(lst)),这没有错,但enumerate更简洁,也更能体现Pythonic风格。
5. 列表推导式:一行顶三行的优雅写法
5.1 基础推导式
列表推导式的格式是:
[表达式 for 变量 in 可迭代对象 if 条件]先看一个最简单的:
# 生成0到9的平方 squares = [x ** 2 for x in range(10)] print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]翻译成普通写法是:
squares = [] for x in range(10): squares.append(x ** 2)三行变一行,而且语义非常清晰:“把每个x的平方放进去”。这还只是最基础的,加上条件筛选之后,它的威力会更明显。
5.2 带条件筛选的推导式
# 取1到20之间的偶数 evens = [x for x in range(1, 21) if x % 2 == 0] print(evens) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] # 把字符串列表里的单词长度都算出来,但只保留长度大于3的 words = ["apple", "hi", "banana", "ok", "watermelon"] lengths = [len(w) for w in words if len(w) > 3] print(lengths) # [5, 6, 10]实际工作中,这种“读列表→过滤→变形”的需求非常多。比如用爬虫刮了一堆文章标题,你想只保留标题里含“Python”的,并全部转成小写:
titles = ["Python入门指南", "Java实战", "python爬虫技巧", "C++笔记"] filtered = [t.lower() for t in titles if "python" in t.lower()] print(filtered) # ['python入门指南', 'python爬虫技巧']你看,一行代码,过滤和转换同时做完,可读性还特别强。新手如果看多了for循环嵌套,一下子可能会觉得推导式不够直观,但我建议你强迫自己多用——养成习惯后,写代码的效率会高很多。
5.3 嵌套推导式:慎用但要知道
嵌套推导式长下面这个样子:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] # 把二维列表展平成一维 flat = [num for row in matrix for num in row] print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]看这个表达式的顺序,其实和正常的嵌套循环是一样的:
flat = [] for row in matrix: for num in row: flat.append(num)嵌套推导式的可读性相对差一些,我不建议一上来就在代码里大量使用。个人经验:超过两层的推导式,不如老老实实写循环,代码是给人读的,不只是给机器跑的。
6. 一个经典实战:李白打酒与Python列表思维
讲了一堆语法,可能有人觉得“道理我都懂,但什么时候用啊”。我拿热搜词里那个很有意思的“李白打酒python”当例子,带你体验一下列表思维在实际解题中的应用。
题目背景是这样的:李白壶中原来有酒若干,他每次遇到酒店,壶中酒就翻一倍;每次遇到花,他就喝掉一斗(也就是1升)。已知他出门时有2斗酒,总共遇到过3次店、5次花,最后一次遇到的是花,并且正好喝光壶中的酒。问:李白的遇店和遇花的顺序有哪些可能?
这个问题放在算法竞赛里其实有很多解法,比如递归枚举、动态规划、二进制枚举等等。Python里有个经典写法,就是利用“二进制序列”来枚举所有可能的顺序,把“店”记为1,“花”记为0,然后在0和1的序列里筛掉不满足条件的结果。
我简化一下流程给你看核心逻辑:
# 枚举所有长度为8的序列,正好4个1(但原始题是3店5花,这里用通用思路演示) # 用itertools生成所有可能排列 import itertools count = 0 for seq in itertools.product([0, 1], repeat=8): if sum(seq) != 3: # 店的数量必须是3 continue wine = 2 ok = True for flag in seq: if flag == 1: wine *= 2 # 遇店加一倍 else: wine -= 1 # 遇花喝一斗 if wine < 0: ok = False break if ok and seq[-1] == 0 and wine == 0: count += 1 # 把当前序列转换成可读的文字描述 route = ''.join('店' if f == 1 else '花' for f in seq) print(route) print('总共有', count, '种可能')这个例子里,列表和元组就是天然的“序列容器”。seq里每一位记录一个事件,sum(seq)统计店的个数,遍历时按位模拟喝酒的过程。如果你用C语言,你大概率会开一个数组,手动维护下标;但用Python,一个元组加一个循环,逻辑清清楚楚。
这其实就是列表思维的核心:把数据抽象成一串有序元素,然后用序列操作去处理它。
7. 列表在真实项目里的三大高频场景
7.1 从字符串或文件里读取数据
写爬虫也好,做数据分析也好,最常见的操作是“读文本→拆分成列表→处理”。
# 字符串拆分成列表 s = "apple,banana,orange" fruits = s.split(",") print(fruits) # ['apple', 'banana', 'orange'] # 读文件按行拆成列表 with open("data.txt", "r", encoding="utf-8") as f: lines = [line.strip() for line in f if line.strip()]读文件时,line.strip()可以去掉结尾的换行符。很多新手不处理换行符,结果后面数据清洗时到处是\n,排查半天才找到源头。
7.2 列表与数据清洗
比如你从网上爬下来的数据里可能有空白、重复、类型不一致等问题:
raw = [" 张三 ", "李四", "", "张三", " 王五 "] # 去掉空白、跳过空字符串 cleaned = [name.strip() for name in raw if name.strip()] # 去重但保持顺序(列表+集合的经典组合) seen = set() dedup = [] for name in cleaned: if name not in seen: seen.add(name) dedup.append(name) print(dedup) # ['张三', '李四', '王五']这个“列表+集合去重”的组合,在数据处理中出场率极高。你以后看别人的代码,这种模式会反复出现。
7.3 列表在算法题里的应用
算法题里列表是绝对的主角。各种遍历、回溯、动态规划,最后的结果存储几乎都靠列表。举一个最常见的例子,“从列表中移除指定元素”的两种写法:
nums = [1, 2, 3, 2, 4, 2, 5] # 方法一:列表推导式生成新列表(推荐) nums = [x for x in nums if x != 2] # 方法二:while循环原地删除(注意:for循环里remove容易跳元素,不推荐) # 这一点下文避坑会详细讲实际刷题时,还有一类“超大数组”的问题,这时候Python列表的内存和性能劣势会暴露出来。所以许多算法题解会强调用collections.deque做队列、用heapq做堆、用bisect维护有序列表。列表虽然基础,但“何时换成别的数据结构”,也是进阶的分水岭。
8. 列表的坑与避坑实录:这些坑我都踩过
我把自己和身边人踩过的列表相关的坑,集中整理一下,这些基本都是真实高频雷区。
8.1 陷阱一:[[]] * n生成的不是n个空列表
这是经典中的经典。
lst = [[]] * 3 print(lst) # [[], [], []] lst[0].append(1) print(lst) # [[1], [1], [1]] —— 三个“空列表”其实是同一个!原因跟之前讲的引用模型一模一样:[[]] * 3是对同一个空列表对象复制了三次引用。正确做法是:
lst = [[] for _ in range(3)] lst[0].append(1) print(lst) # [[1], [], []]8.2 陷阱二:for循环遍历时删除元素
lst = [1, 2, 3, 4, 5, 6] for x in lst: if x % 2 == 0: lst.remove(x) print(lst) # [1, 3, 5, 6] —— 6居然没删掉?为什么6没删掉?因为for循环在内部通过下标迭代,删除元素后,列表长度变化,下标会错位,导致跳过某些元素。不要在遍历列表时直接删除元素——这是铁律。正确做法:
# 方式一:列表推导式生成新列表 lst = [x for x in lst if x % 2 != 0] # 方式二:遍历拷贝 for x in lst[:]: if x % 2 == 0: lst.remove(x)8.3 陷阱三:list.sort()和sorted()混用
lst = [3, 1, 2] result = lst.sort() print(result) # None —— sort是原地操作,返回值是None print(lst) # [1, 2, 3]想拿到排序结果,就用sorted(lst);想原地改,就调用lst.sort()。千万不要写lst = lst.sort(),这一下就把列表变成None了。
8.4 陷阱四:修改列表时使用“默认参数”
def add_item(item, target_list=[]): target_list.append(item) return target_list print(add_item(1)) # [1] print(add_item(2)) # [1, 2] —— 第二次调用,默认列表居然保留着上次的数据!这是Python默认参数陷阱,官方也不建议用可变对象作默认参数。正确做法:
def add_item(item, target_list=None): if target_list is None: target_list = [] target_list.append(item) return target_list这个坑非常隐蔽,没经历过的人真的想不到。我现在看到“默认参数是空列表”的代码,都条件反射地警觉。
8.5 陷阱五:浅拷贝导致的嵌套修改问题
original = [[1, 2], [3, 4]] copy = original.copy() copy[0].append(99) print(original) # [[1, 2, 99], [3, 4]] —— 原列表也被改了原因还是浅拷贝:copy()只复制了外层列表,内层子列表仍然共享同一份引用。要彻底独立,用copy.deepcopy()。
9. 常见问题速查表
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 修改一个“复制”的列表,原列表也变了 | 用的是=赋值,不是真复制 | 用lst.copy()或lst[:] |
lst.sort()输出None | sort()是原地操作,返回None | 用sorted(lst)获取新列表 |
| 遍历时删除元素,有些元素没删掉 | 下标错位导致跳过 | 遍历拷贝lst[:]或用推导式 |
[[]] * 3生成的子列表互相干扰 | 引用的是同一个空列表 | 用[[] for _ in range(3)] |
lst.append([1,2])后列表怪怪的 | append会把列表当作一个元素 | 用extend |
lst.index(x)找不到元素直接报错 | 不存在时会抛ValueError | 先判断x in lst |
默认参数写成list=[]导致数据串了 | 默认参数在函数定义时创建一次 | 改写成list=None |
| 想要同时拿下标和元素 | 写起来麻烦 | 用enumerate(lst) |
| 两个列表要一一对应处理 | 容易用下标循环 | 用zip(list1, list2) |
这张表你可以直接存起来,遇到问题回来翻一翻,基本覆盖了列表相关的日常故障。
10. 写在最后:列表是Python世界的“数字积木”
在实际写代码时,我的体会是:Python列表的核心并不在于“记住哪些方法”,而在于建立序列思维——把一堆数据当成一条线,知道怎么访问、怎么切割、怎么变换、怎么避免引用共享的坑。列表学得扎实了,你再看元组会觉得只是“不能改的列表”,看字符串处理会觉得是“字符序列的切片运用”,甚至再看数据分析里的pandas,你会发现索引和切片的思想全都一脉相承。
最后再分享一个小技巧:当你不太确定一个列表操作会不会修改原列表时,最快的方式不是查文档,而是直接在交互式环境(REPL)或Jupyter里跑一下,输出两个值比对。多跑几次,你对“原地修改”和“返回新对象”的直觉就有了,比死记API管用得多。
列表这东西,初学觉得简单,深入起来也够喝一壶。希望这篇“踩坑+实战”风格的文章,能帮你少走几段弯路。如果你在练习中碰到什么奇怪的列表问题,欢迎留言一起讨论,我在看到之后会尽可能回复。