如果只能让我从 Python 里挑出一个最离不开的数据类型,我会毫不犹豫地选列表(list)。不管是刚入门时写的第一个 scores = [90, 85, 88],还是后来做爬虫、数据处理、接口测试时频繁操作的 result_list,列表几乎出现在 Python 的每一个角落。
列表是什么?它是 Python 内置的一种容器数据类型,用来按顺序保存一组数据。和数字、字符串这种“单个值”不同,列表可以把很多值装在一起,还能随时添加、删除、修改元素。你可以把列表想象成一个随时可以调整隔间的书架:不要求每本书一样厚、一样高,也不要求书架一开始就定好层数,想加书就加,想拿掉就拿掉。
这篇博文适合谁看?刚学 Python 不久、对列表停留在“会 append、会取下标”的朋友,可以从头补齐知识点;写了两三年 Python 但没认真研究过切片、拷贝、推导式的朋友,也能从里面找到不少平时没注意的细节;哪怕是面试前想快速把列表相关的坑过一遍,这篇文章也能帮上忙。
下面我按创建、索引切片、内置方法、内存机制、推导式、嵌套列表、常见问题排查和综合案例这条线往下讲,尽量把列表一次聊透。
1. 为什么我觉得列表是 Python 里存在感最强的数据类型
老实说,列表能成为 Python 里的“头号容器”,和 Python 的设计哲学有很大关系。程序员处理现实问题时,最常面临的问题就是“一堆数据怎么组织”。Excel 表格里的一列数字、服务器返回的一组 JSON 数组、日志里的一串时间点,本质上都是“有序的多个值”。列表就是为这种场景设计的。
和字符串、元组不一样,列表是可变对象,创建之后还能改。这一点太重要了。你在循环里累加数据,在爬虫里往结果列表追加新页面解析出来的条目,在量化回测里记录每天的净值,靠的都是“能改、能追加、能删”这个能力。如果一个容器创建之后就不能动,很多动态场景根本没法写。
1.1 列表和 C 语言数组的本质区别
很多看过 C 语言或 Java 的人,会下意识地把列表理解成“高级数组”。这个类比方向对,但有本质区别。
C 数组是连续内存,里面存的是元素本身,类型必须统一,长度固定。Python 列表则是一个“引用数组”:底层确实是一段连续内存,但内存里存的不是对象本体,而是指向对象的 8 字节指针。所以 Python 列表才能做到同一个列表里既装整数、又装字符串、又装自定义对象。
我再用一个生活化类比帮没有 C 基础的朋友理解:C 数组像一个公寓楼,每间房只能住固定类型的人,建好后不能加楼层;Python 列表像一个储物柜,每个柜格里放的是“取件码”而不是物品本身,所以你可以往同一个位置放各种类型的东西,还可以随时加柜格。因为存的是引用,Python 列表天然灵活,但代价也明显:内存开销更大,大列表远比想象中占内存。一个常见说法是,整数对象本身可能需要 28 字节,列表里保存它的引用还要再占 8 字节,如果存一个十万个整数的列表,光整数对象的开销就相当可观。
1.2 哪些场景优先选列表,哪些场景别用列表
并不是所有“一组数据”都必须用列表。我自己的选型经验是这样的:需要按顺序保存、按索引访问、随时增删尾部,列表是首选;需要频繁在头部插入或删除,别用列表,用 collections.deque,因为列表头部操作是 O(n);只需要判断某个元素在不在集合里、不需要顺序,用 set,因为 set 的查找是 O(1),列表是 O(n);需要按键取值,直接用 dict。
这里插一个实际感受。我见过很多新手用列表模拟键值对:两个列表,一个存 key 一个存 value,通过 index() 去查询。数据量小的时候无所谓,一旦上千条,效率立刻拉胯,而且代码可读性很差。正确的做法就是换成 dict。列表是个好工具,但不是万能容器。理解它适合什么、不适合什么,比背方法列表更重要。
2. 列表创建、索引与切片实操
列表的创建方式很多,但最推荐的还是字面量。我见过不少初学者用 list() 创建空列表,不能说错,但可读性比 [] 差一些。列表创建这件事看起来简单,却有几个容易忽略的细节,下面我拆开说。
2.1 创建列表的五种常见方式
第一,字面量直接创建:books = ["三体", "活着", "围城"]。这是日常写代码最常用的一种,也最直白。用字面量创建的列表,元素之间用逗号隔开,阅读时一眼就能数出来列表里有几个元素、类型是什么。对于代码的可维护性来说,这种直白比什么技巧都重要。
第二,list() 构造器:list("abc") 会得到 ['a', 'b', 'c'];list(range(5)) 会得到 [0, 1, 2, 3, 4]。这里有个容易混淆的点:list("abc") 是把字符串拆成一个个字符,不是把整个字符串作为一个元素。如果你想按逗号切分字符串得到列表,应该用 "a,b,c".split(",")。list() 更多用在需要把迭代器或 range 对象转换成列表的场景。
第三,列表推导式:[x * x for x in range(10)]。它不仅能创建列表,还能在创建过程中做过滤和变形。这个我会在后面的章节专门展开,是 Python 里辨识度最高的语法之一。
第四,乘法重复:zeros = [0] * 5 得到 [0, 0, 0, 0, 0]。这个写法对一维列表很好用,但对嵌套列表要格外小心,因为乘号会把同一个内层对象复制引用,后面我会专门用一节讲这个坑。
第五,从字符串清洗出的列表:line.strip().split(",") 是处理 CSV 数据最常用的套路,先把每行读进来,再按分隔符切分成列表。日常处理日志、配置文件时,这个组合出现频率非常高。
初学者我的建议是优先用字面量,一眼能看出列表里有几个元素。list() 和推导式在特定场景更优雅,但没必要为了“炫技”什么都用推导式。
2.2 索引规则:正着数、倒着数、注意越界
Python 列表索引从 0 开始。这个设计让熟悉程序的用户很舒服,但刚接触的朋友容易把“第 2 个元素”当成 nums[2],实际上应该是 nums[1]。
索引还支持负数。倒数第一个是 nums[-1],倒数第二个是 nums[-2]。这是我平时取“列表最后一项”最常用的写法,比 nums[len(nums) - 1] 简洁太多。负索引的本质是 Python 在访问前自动做了 len(nums) + index 的换算,所以它并没有开辟另一套存储,只是语法糖。
越界访问会直接抛 IndexError。这里有个和切片不太一样的重要区别:索引越界一定报错,切片越界不报错。比如 nums[100] 在只有 5 个元素的列表上执行,程序直接崩溃;而 nums[3:100] 会正常返回一个能取多少取多少的子列表。这个差异是新手容易踩的坑,但恰恰也能被我们利用,后面切片部分会继续提到。
2.3 切片:很多人没用透的硬核技能
切片语法是 nums[start:stop:step],三个字段都可选。需要注意 stop 是不包含的,这是“左闭右开”区间。为什么 Python 选择左闭右开?因为区间长度可以直接算出来,len(nums[2:5]) 等于 5 - 2,切分连续片段时相邻两组不会重叠,也不会漏元素。
几个高频写法我直接列出来:
- nums[:]:完整复制列表,等价于浅拷贝,很多人不知道它能当 copy 用。
- nums[::-1]:反转列表,面试题里出现率极高。
- nums[::2]:取偶数位置元素,处理抽样数据时很好用。
- nums[-3:]:取最后 3 个元素。
- nums[1:4]:取索引 1、2、3 三个元素。
切片返回的是新列表,但这不意味着内部元素也被复制了。它复制的是外层引用,里面如果是嵌套列表,两者仍然共享内层对象。这一点我建议现在先记住,第 4 章讲拷贝时会和内存机制连起来解释。
下面再讲一个多数人没用过的能力:切片赋值。nums[1:3] = [8, 9] 会把原来的第 1、2 个位置替换成 8、9;要删除中间一段可以写 nums[1:4] = [];要在某个位置插入新元素可以写 nums[1:1] = [new]。这种写法能原地修改原列表,代码非常简洁,但可读性比较低,老项目里不太常见。我自己用得不算多,但看到别人的代码里出现时,至少不会懵。
3. 列表内置方法逐个过:怎么用不踩坑
Python 列表的内置方法不算多,翻来覆去就那十几个,但每个都有自己的脾气,用错会出现很隐蔽的逻辑问题。
3.1 append 和 extend 的区别,三分钟讲透
append(obj) 是把 obj 当作一个整体追加到列表末尾。extend(iterable) 是把可迭代对象里的每个元素逐个追加。两者都是原地修改,返回 None。
我见过最多的问题就是把两者搞混。如果你写 nums.append([4, 5]),得到的是 [1, 2, 3, [4, 5]],而不是 [1, 2, 3, 4, 5]。如果写 nums.extend(4),则会直接报 TypeError,因为数字不是可迭代对象。一句话总结:append 是加一个箱子,extend 是拆开箱子把里面的东西一个个拿出来加。
insert(index, obj) 可以在指定位置插入,比如 nums.insert(0, x) 插到最前面。注意 insert 性能是 O(n),因为它要挪动后面的元素。如果你需要频繁在头部插入,用 deque,这一点前面已经强调过,这里就不展开了。
3.2 删除的三种姿势:remove、pop、del
remove(value) 按值删除第一个匹配项,找不到会抛 ValueError。它只删第一个,如果列表里有多个相同值,需要循环处理或改用列表推导式过滤,后面我会给一个完整案例。
pop(index) 按索引删除并返回被删元素,不传 index 时默认弹出最后一个。pop 是少数几个“删除后还告诉你删了什么”的方法,在实现栈结构时特别好用。栈的入栈是 append,出栈是 pop,后进先出,非常自然。
del 是语句不是方法,del nums[i] 按索引删除,del nums[0:3] 按切片批量删除,它不返回任何值。
我的选择习惯是:需要返回值用 pop,不需要返回值且想按索引精确删除用 del,只记得值怎么写用 remove。但记住 remove 按“值”匹配,del 和 pop 按“索引”匹配,这是最容易搞混的地方。官方文档里 remove 还特意提醒:如果想删除所有匹配项,不要在循环里直接调用 remove,否则会跳过元素。
3.3 index、count 和 in:查找元素要分清场景
index(value) 返回匹配到的第一个索引,找不到抛 ValueError。count(value) 返回出现次数。如果只想判断某个元素在不在,更自然的写法是 value in nums,返回 True 或 False,不会抛异常。
这些方法内部都是线性扫描,时间复杂度 O(n)。如果列表很短无所谓,但场景是“频繁判断一个超长列表中是否存在某个值”时,建议把列表转成 set 再用 in,因为 set 查找是哈希计算,平均 O(1)。我在真实项目里遇到过用列表做存在性判断的写法,数据量从百级别升到十万级别时,肉眼可见地变慢。查找性能是列表的一个明显短板,选型时要想清楚。
3.4 sort、sorted 与 key 参数
list.sort() 是原地排序,返回 None,直接改原列表。sorted(list) 返回一个新列表,原列表不变。区别就一句话:sort 更省内存,sorted 更安全。
很多人刚接触时写过 nums_sorted = nums.sort(),结果得到 None,然后排查半天。这就是不熟悉“原地修改返回 None”的后果。Python 里很多方法设计成原地修改后就不会 return 自己,官方认为这样可以避免“同时原地修改又返回副本”的歧义。
真正能发挥排序威力的是 key 参数。key 传入一个函数,Python 会对每个元素调用该函数,用返回值作为比较依据。常见例子:
words = ["pear", "apple", "orange", "banana"] # 按单词长度排序 words.sort(key=len) students = [ {"name": "张三", "score": 92}, {"name": "李四", "score": 88}, {"name": "王五", "score": 95}, ] # 按分数降序 students.sort(key=lambda s: s["score"], reverse=True)当列表里存放的是字典对象时,这种写法几乎是标准答案。lambda 也可以用 operator.itemgetter 替代,可读性更强,但多数场景下 lambda 完全够用。
3.5 copy、clear 与解包技巧
nums.copy() 返回浅拷贝,等价于 nums[:]。nums.clear() 清空所有元素,原列表变成 []。这两个方法都偏底层,面试题里出现率很高。
还有一个我一直觉得很好用的特性是多变量解包:a, b, c = [1, 2, 3],把列表前三个元素赋给三个变量。配合 *rest 可以写成 first, *rest = nums,把第一个元素拿出来,剩下的全给 rest 列表。处理用户输入、解包不定长数据时非常省事。比如读取配置时,第一行是标题行,后面全是正文,一行解包就能分离。
4. 内存机制与拷贝:可变对象、浅拷贝深拷贝的底层逻辑
很多教程把列表的“可变”当成一个概念讲过去,但没解释可变带来的连锁反应。这一章我想把背后的逻辑讲透,以后再遇到诡异的问题,基本可以自己推理出来。
4.1 列表是可变对象,赋值是引用传递
先看一段代码:
a = [1, 2, 3] b = a b.append(4) print(a) # [1, 2, 3, 4]b 只是 a 的“别名”,两者指向同一个列表对象。你在 b 里改,a 里一定同步变化。这个现象在 Python 中到处可见:函数参数传递、把列表放入另一个容器、变量赋值,全部是引用语义。
为什么 Python 要这么设计?因为如果每次赋值都复制一份完整列表,内存和性能完全扛不住。想象一下把一个 10 万元素的列表传给函数,如果参数传递默认复制一份,每次调用都要多一份 O(n) 的复制开销,Python 早就慢得没法实用。理解了这一点,你就不会再说“Python 怎么这么奇怪”,而是每次都先想:这个操作到底会不会把原来的数据改坏。
4.2 浅拷贝和深拷贝:一个常见的实战陷阱
想要独立副本,最直接的做法是 lst.copy() 或 lst[:],但必须知道这只是浅拷贝。
所谓浅拷贝,就是新列表的外壳是新的,但元素仍然指向原来的对象。如果列表里装的都是不可变对象,比如整数、字符串、元组,浅拷贝基本等于深拷贝,因为不可变对象不存在被修改的风险。可如果列表里嵌套了列表、字典、自定义对象,浅拷贝就挡不住了。
a = [[1, 2], [3, 4]] b = a.copy() b[0].append(9) print(a) # [[1, 2, 9], [3, 4]]因为 a[0] 和 b[0] 指向同一个内层列表,b 通过 append 修改内层列表时,a 也受影响。正确解法是 copy.deepcopy(a),它会递归复制所有层级,得到完全独立的副本。但 deepcopy 开销也大,如果列表嵌套很深,深拷贝会明显变慢。实际工程里,能不用就不用,更多时候你只需要浅拷贝,或者干脆想清楚数据归谁管,别乱复制。
4.3 列表扩容机制与尾部性能
CPython 的列表底层是动态数组,它不会每次 append 都精确扩容到刚好能装下新元素,而是会多分配一些空间,减少频繁搬移数据。具体扩容倍数不同版本不一样,总体思路类似 C++ vector 的 amortized。所以尾部 append 和 pop 基本是摊还 O(1),非常快。
但在头部插入或删除就完全不同了,它需要把后面所有元素整体移动一位,时间复杂度 O(n)。列表越长,这个操作越慢。我写任务队列、日志流处理这类需要头尾频繁操作的场景时,几乎必用 collections.deque,因为 deque 在头尾的插入删除都接近 O(1)。这一章看起来是底层知识,但对实际代码性能影响非常直接。
5. 列表推导式:一行代码替代 for 循环的高效写法
列表推导式是 Python 辨识度最高的语法之一。它能用一行实现“遍历 + 转换 + 过滤”的组合操作,我几乎每天都会用。
5.1 从 for 循环到列表推导式
假设要生成 0 到 9 的平方数列表,新手写法是:
squares = [] for i in range(10): squares.append(i * i)用推导式:
squares = [i * i for i in range(10)]两行变一行,不仅代码少,而且通常更快。因为推导式在底层少了大量循环中的变量查找和方法调用开销,比如 append 每次迭代都要做属性查找,而推导式是在构建列表本身,性能更好。数据量越大,这个优势越明显。
从可读性看,推导式把“我要生成什么”直接写在最前面,读代码的人第一眼就知道结果是一个列表,每个元素是什么,比先建空列表再循环追加清晰得多。这也是为什么 Python 社区强调“Pythonic”时,推导式经常被拿出来作为代表。
5.2 带过滤条件的推导式
evens = [x for x in range(20) if x % 2 == 0]if 放在 for 后面,表示只有当条件成立时才把 x 放进结果。这是最基础的过滤写法。也可以同时遍历多个列表,比如:
pairs = [(x, y) for x in [1, 2] for y in ["a", "b"]] # 结果是 [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]这里有一个容易写错的地方:推导式里的 for 顺序和普通嵌套循环顺序一致,先写外层循环,再写内层循环。很多人以为从左到右执行,理顺这个逻辑就错不了。
5.3 嵌套列表的扁平化
嵌套列表变一维,一行写法:
matrix = [[1, 2], [3, 4], [5, 6]] flat = [num for row in matrix for num in row] # [1, 2, 3, 4, 5, 6]这个读法是:先写外层 for row in matrix,再写内层 for num in row,最后写最左边的 num 作为生成表达式。我建议新手按“从 for 开始读,一层层往外层看”的方式理解,熟练之后就是一眼看出结构。扁平化在把二维数据喂给某个只接受一维列表的算法时非常常用。
5.4 什么时候不要硬用推导式
我虽然很推荐推导式,但也要提醒别过度用。当判断条件很复杂、有多个分支,或者需要在循环里做副操作,比如打印日志、累计多个结果时,老老实实写普通循环。推导式不擅长带副作用的逻辑,也很难调试,你就没法在推导式中间打断点。
一个很实用的建议:如果推导式一行超过 80 个字符,或者嵌套超过两层,就拆成普通循环加辅助函数。代码是写给人看的,其次才让机器执行。我自己就曾经为了“一行写完”写过一个三层嵌套、带两个 if 的推导式,结果第二天看代码看了十分钟才看懂,最后还是拆回循环。
6. 嵌套列表与多维数据处理:转置、扁平化与常见坑
数据处理里经常出现“列表套列表”,比如二维表格、矩阵、图像像素数组。我挑几个最常遇到的点和最容易犯的错讲。
6.1 创建二维列表,最大的坑是乘号
创建二维列表时最容易踩的坑是乘号复制引用。很多新手第一反应是 [[0] * 3] * 3 这种写法,看起来很省事,但修改数据时会带来惊吓。我先把它放上来演示一下:
matrix = [[0] * 3] * 3 matrix[0][0] = 1 print(matrix) # 输出 [[1, 0, 0], [1, 0, 0], [1, 0, 0]]明明只想改第一行第一列,结果三行都变了。原因是 [[0] * 3] * 3:先创建一个长度 3 的列表 [0, 0, 0],然后用 *3 复制了 3 个引用,这三行实际上指向同一个内层列表。
正确做法是用列表推导式,每次循环都新创建一个内层列表:
matrix = [[0] * 3 for _ in range(3)]这样得到的三个内层列表互相独立。这个坑在笔试和实际代码里都出现过,我建议你直接把它写进笔记里。
6.2 访问、遍历与修改嵌套列表
访问二维列表的每个格子可以用 matrix[row][col]。遍历时如果同时需要行列索引,用 enumerate 比较顺手:
for i, row in enumerate(matrix): for j, value in enumerate(row): print(i, j, value)如果要给整行重新赋值,直接 matrix[i] = new_row;要给某个格子赋值,matrix[i][j] = new_value。修改时记住一点:通过索引去修改原列表,不要在迭代中试图 append 到 row。因为 row 只是从列表里取出来的引用,append 会影响这个列表对象,但如果你换成一个新列表赋值给 row,原列表不会变。“取出来 vs 改原值”这件事是嵌套列表里最容易绕晕的地方。
6.3 转置与扁平化
矩阵转置一行写法:list(zip(*matrix))。比如:
matrix = [[1, 2, 3], [4, 5, 6]] transposed = list(zip(*matrix)) # [(1, 4), (2, 5), (3, 6)]*zip 把 matrix 的每一行作为独立参数传给 zip,zip 再按列依次组合。注意返回的每一列是元组,如果一定需要列表,再对每个元组转成 list。
扁平化写法前面已经给过:[num for row in matrix for num in row]。两个操作在数据清洗里出场率很高,比如把矩阵压平后接入某个算法,或者把宽表转成长表时做转置。行列转换时还要注意一个问题:如果行的长度不一致,zip 会按最短的行截断,不会报错,这在处理不规整数据时容易造成静默丢数据。
7. 常见问题排查与避坑实录
写 Python 几年,真正让人头大的不是不知道方法,而是逻辑上看起来没问题、程序却给出诡异结果。我把列表上最典型的问题整理成一组速查,每个都能对上真实踩坑经历。
7.1 在 for 循环中删除列表元素,元素会被跳过
这是让我排查到怀疑人生的经典案例:
lst = [1, 2, 2, 3, 4, 2] for x in lst: if x == 2: lst.remove(x) print(lst) # 结果是 [1, 3, 4, 2]本意删掉所有 2,结果还剩一个。原因是 for 循环内部通过索引取元素,每次迭代索引加一,而 remove 会让列表变短,导致某个元素没被遍历到。
三个靠谱解决方案:
- 创建新列表:lst = [x for x in lst if x != 2],简单直接,最推荐。
- 倒序遍历索引:for i in range(len(lst) - 1, -1, -1),满足条件时 del lst[i]。
- 用 while 循环手动控制索引。
真实业务里优先“生成新列表”,它不会破坏正在迭代的列表,也不容易出边界错误。
7.2 空列表判断与布尔值陷阱
判断列表是否为空,新手最常见的是 len(lst) == 0,更地道的写法是 if not lst。空列表在布尔上下文中是 False,非空列表是 True。这个特性不只适用于列表,字符串、元组、字典、set 都一样。
注意 if not lst 和 if lst is None 是两回事。前者判断“内容为空”,后者判断“变量是不是 None”。虽然 None 在布尔上下文里也是 False,如果你只想去掉空列表而不想去掉 None,仍需单独判断类型。这个问题在接口返回值处理时特别容易出现。
7.3 去重保持顺序的三行写法
如果不在乎顺序,直接 set(lst)。如果需要保持原有顺序,推荐用集合辅助:
result = [] seen = set() for x in lst: if x not in seen: seen.add(x) result.append(x)我始终觉得这是最清晰也最不容易出错的写法。网上还有一行版 [x for x in lst if not (x in seen or seen.add(x))],虽然短,但它利用 or 短路来触发 set.add 的副作用,对初学者很不友好,我不建议在团队代码里写这种太晦涩的东西。代码的维护成本比写出来的成本高得多。
7.4 排序的返回值陷阱与 key 用法
sort 返回 None,这个点我前面提过,但还要再拎出来强调。我在 code review 里见到过太多次 result = data.sort() 这种写法,结果 result 是 None,后续所有操作全部报错。如果你希望返回新列表且不改原数据,用 sorted(data);如果希望原地改且不计较返回值,用 data.sort(),两者别混。
关于 key,记住一个原则:key 不是比较运算符,而是“如何提取比较依据的函数”。它可以是 len、str.lower、lambda、operator.itemgetter。比如按字符串长度排序 words.sort(key=len),按字典中的年龄排序 users.sort(key=lambda u: u["age"])。key 只做一次提取计算,不会每次比较都重算,性能也比较友好。
7.5 报错速查表
| 报错 | 原因 | 解决方式 |
|---|---|---|
| IndexError: list index out of range | 索引越界,或 pop 从空列表弹出 | 检查索引和长度,考虑用切片避免越界 |
| ValueError: x is not in list | remove 或 index 找不到目标值 | 先用 in 判断,或 try/except 捕获 |
| ValueError: not enough values to unpack | 解包时变量数不等于列表长度 | 检查长度,用 *rest 接收多余值 |
| TypeError: 'xxx' object is not iterable | 对不可迭代对象用了 extend、sorted 等 | 确认传入的是可迭代对象,如列表、元组、字符串 |
| AttributeError: 'list' object has no attribute 'xxx' | 方法名拼错,把元组当列表用 | 检查方法拼写,或确认类型 |
这五类是我被问过最多的异常,排查时先对号入座,能省不少时间。如果遇到没列出来的,也建议先把类型和当前长度打出来看一下,往往真相就在这两行日志里。
8. 综合实战:用列表写一个学生成绩统计小工具
前面说的内容偏零散,最后我把它们串起来,做一个真实场景下会用到的小工具:统计一个班级若干学生各科成绩的总分、平均分、学科平均分,并按总分排序。
8.1 需求拆解与数据准备
先看数据,这里用一个二维列表保存学生的考试成绩,每行是一个学生,三列分别是语文、数学、英语:
scores = [ [78, 85, 92], [88, 79, 90], [69, 94, 83], [95, 87, 76], ]需求拆成四部分:第一,计算每个学生的总分和平均分;第二,计算每门学科的平均分;第三,按总分从高到低输出排名;第四,把逻辑封装成函数,方便以后换数据复用。这里每个需求都可以用前面讲过的列表技能解决。
8.2 核心代码实现
先看第一个需求:计算学生总分和平均分。用列表推导式处理每一行,sum 和 len 都是列表常用的内置函数,组合起来非常自然:
total = [sum(row) for row in scores] avg = [sum(row) / len(row) for row in scores]如果某一行长度不固定,用 len(row) 作为除数会比写死 3 更稳健。
第二块需求是学科平均分。学过线性代数的朋友都知道,对列做聚合最直接的办法是先转置,让原来的列变成行,然后再对每一行求平均。具体用 zip 转置后写成:
columns = list(zip(*scores)) subject_avg = [sum(col) / len(col) for col in columns]第三个需求是按总分从高到低输出排名。sorted 的 key 可以直接接收内置函数 sum,reverse=True 控制降序。写成代码就是:
for rank, row in enumerate(sorted(scores, key=sum, reverse=True), start=1): print(f"第{rank}名:{row},总分{sum(row)}")这里有两个亮点写法值得记:sorted 的 key=sum 直接拿内置函数作为排序依据,不需要额外写 lambda;enumerate 配合 start=1,让排名从 1 开始,省掉手动加一。
8.3 封装函数与后续扩展
把逻辑封成一个函数,方便对不同数据复用:
def analyze(scores): total = [sum(row) for row in scores] avg = [sum(row) / len(row) for row in scores] columns = list(zip(*scores)) subject_avg = [sum(col) / len(col) for col in columns] ranked = sorted(scores, key=sum, reverse=True) return total, avg, subject_avg, ranked如果数据来自 CSV 文件,可以先读每一行、用 split(",") 转成列表、再把字符串分数转成 int,同样套这几个函数。如果要显示每个学生的姓名,更好的结构是改用字典列表,每个元素是 {"name": "张三", "scores": [78, 85, 92]},这时候排序代码改成 key=lambda s: sum(s["scores"]) 即可。再往后数据量大了,可以直接用 pandas 的 Series 和 DataFrame,但底层“容器 + 顺序 + 变换”的概念,还是今天这些列表基本功。
最后说点个人体会。我在实际项目里发现,列表相关的 bug 几乎都出在两个地方:一是没有搞清楚“复制”和“引用”的区别;二是没有想清楚“我到底要一个新列表还是改原列表”。如果你写列表代码前能先回答这两个问题,至少能避开一半的坑。
另一个小技巧是:调试列表逻辑时,优先把原列表复制一份再操作。我自己经常在交互环境里先跑 lst[:] 观察中间状态,看到结果没问题再写进正式代码。别嫌麻烦,这比上线后发现问题再修要快得多。
列表作为 Python 四大容器之首,值得你花时间彻底吃透。这篇就先聊到这里,下次有机会再一起看看元组、字典和集合。