ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python列表全解析:从序列索引到切片实战与避坑指南

2026/9/9 9:42:05 拓冰建站 浏览量
Python列表全解析:从序列索引到切片实战与避坑指南 来咱们聊点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 sx是否在序列中长度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:i3]就好不会多出一个来。切片有几个常被忽略的实用细节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是8stop是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(reverseTrue) 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], repeat8): 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, encodingutf-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_listNone): 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()输出Nonesort()是原地操作返回None用sorted(lst)获取新列表遍历时删除元素有些元素没删掉下标错位导致跳过遍历拷贝lst[:]或用推导式[[]] * 3生成的子列表互相干扰引用的是同一个空列表用[[] for _ in range(3)]lst.append([1,2])后列表怪怪的append会把列表当作一个元素用extendlst.index(x)找不到元素直接报错不存在时会抛ValueError先判断x in lst默认参数写成list[]导致数据串了默认参数在函数定义时创建一次改写成listNone想要同时拿下标和元素写起来麻烦用enumerate(lst)两个列表要一一对应处理容易用下标循环用zip(list1, list2)这张表你可以直接存起来遇到问题回来翻一翻基本覆盖了列表相关的日常故障。10. 写在最后列表是Python世界的“数字积木”在实际写代码时我的体会是Python列表的核心并不在于“记住哪些方法”而在于建立序列思维——把一堆数据当成一条线知道怎么访问、怎么切割、怎么变换、怎么避免引用共享的坑。列表学得扎实了你再看元组会觉得只是“不能改的列表”看字符串处理会觉得是“字符序列的切片运用”甚至再看数据分析里的pandas你会发现索引和切片的思想全都一脉相承。最后再分享一个小技巧当你不太确定一个列表操作会不会修改原列表时最快的方式不是查文档而是直接在交互式环境REPL或Jupyter里跑一下输出两个值比对。多跑几次你对“原地修改”和“返回新对象”的直觉就有了比死记API管用得多。列表这东西初学觉得简单深入起来也够喝一壶。希望这篇“踩坑实战”风格的文章能帮你少走几段弯路。如果你在练习中碰到什么奇怪的列表问题欢迎留言一起讨论我在看到之后会尽可能回复。