引言
这是第六章”列表、元组与字典”的第 9 篇。前 8 篇我们掌握了四种容器各自的语法与常用方法:列表的方法与切片(第 1 篇)、元组的不可变性与解包(第 2 篇)、字典的增删查改与遍历(第 3 篇)、集合的去重与运算(第 4 篇),还理解了引用与深浅拷贝(第 5 篇)、嵌套容器的遍历写法(第 6 篇)、collections 模块的 Counter 与 defaultdict(第 7 篇),并在第 8 篇用词频统计完成了一次小型实战。但”会方法”和”会解决问题”之间还有一段路:真实数据处理中,排序、筛选、聚合是三件出现频率最高的事。本篇就以上一篇积累的知识为原料,完成一个综合项目——学生成绩记录分析,产出三类结果:排序后的名次、筛选后的名单、聚合后的统计报表。
读者应先掌握:第五章的 f-string 格式化与 sorted 的基本用法、第 3 篇的字典遍历、第 6 篇的嵌套容器遍历、第 7 篇的 Counter 与 defaultdict。下一篇是第六章综合测验,本篇的排序与筛选是测验的高频考点,请务必亲手运行每一个代码块。
概念与原理
排序:比较、键函数与稳定性
Python 提供的排序入口有两个:内置函数 sorted(iterable, *, key=None, reverse=False) 与列表方法 list.sort()。两者都在原元素的”键”上做比较:key 是一个接收单个元素、返回排序键的函数,每个元素只调用一次。复杂度方面,两者都是 O(n log n),底层是成熟的 Timsort 算法(自 Python 2.3 起就是 Timsort 实现)。更关键的性质是稳定性(stable):比较结果相等的元素,排序后保持它们在原序列中的相对顺序。稳定性的价值在于它可以”叠加”出多级排序——先按次要键排一遍,再用稳定排序按主键排一遍,主键相同的元素之间就自然保留上次排序(次要键)的顺序。
筛选:保留满足条件的元素
**筛选(filtering)**指从容器中挑出满足某条件的元素,结果是原容器的一个子集。筛选与排序是正交的两个操作:排序改变”顺序”,筛选改变”数量”。实现筛选有两条路:列表推导式的 if 子句([x for x in data if 条件],返回新列表)与内置函数 filter(返回迭代器,需配合 list() 使用)。教学与工程中推荐推导式,可读性更好,且不会遇到”迭代器只能消费一次”的陷阱。
聚合:分组、累积与汇总
聚合(aggregation)指把一组记录归并成少数几个汇总值:总和、平均值、最大值、最小值、计数。它的核心思想是分组(group by):选定一个字段作为分组标签,把标签相同的记录放进同一组,再对每组做同样的归并计算。字典天然是”标签 → 数据”的映射容器,所以字典(或 defaultdict)是手工聚合的标准工具:键是组标签,值是累积器(累加的数、累积的列表)。上一章的”求最大最小值”是聚合的特例——分组大小恰好为 1。第 7 篇的 Counter 本质上是”值为整数计数”的特化字典,适合统计各组数量,而一般的统计(平均、最值)用 defaultdict(list) 先收集再计算更顺手。
操作与实现
1. 准备数据:列表套字典
综合项目的惯例是一开始就把数据结构选对。这里用列表套字典的嵌套容器:外层列表保持”记录”的有序集合,内层字典描述一条学生的完整属性。10 条记录分属一班、二班,每门课 55~96 分,包含两种典型情况:个别科目不及格的学生(筛选时会被淘汰),以及总分相近的学生(便于观察排序稳定性)。
students = [
{'name': '张伟', 'sid': '2026001', 'cls': '一班', 'chinese': 88, 'math': 92, 'english': 85},
{'name': '李娜', 'sid': '2026002', 'cls': '一班', 'chinese': 76, 'math': 81, 'english': 90},
{'name': '王强', 'sid': '2026003', 'cls': '一班', 'chinese': 92, 'math': 78, 'english': 80},
{'name': '刘洋', 'sid': '2026004', 'cls': '一班', 'chinese': 65, 'math': 59, 'english': 70},
{'name': '陈静', 'sid': '2026005', 'cls': '一班', 'chinese': 81, 'math': 84, 'english': 62},
{'name': '赵磊', 'sid': '2026006', 'cls': '二班', 'chinese': 70, 'math': 95, 'english': 88},
{'name': '孙丽', 'sid': '2026007', 'cls': '二班', 'chinese': 85, 'math': 72, 'english': 91},
{'name': '周明', 'sid': '2026008', 'cls': '二班', 'chinese': 58, 'math': 66, 'english': 73},
{'name': '吴婷', 'sid': '2026009', 'cls': '二班', 'chinese': 90, 'math': 88, 'english': 96},
{'name': '郑浩', 'sid': '2026010', 'cls': '二班', 'chinese': 73, 'math': 61, 'english': 55},
]
for s in students:
s['total'] = s['chinese'] + s['math'] + s['english']
print(f'共 {len(students)} 条记录,字段:{list(students[0])}')
print('第一条记录:', students[0])
运行结果:
共 10 条记录,字段:['name', 'sid', 'cls', 'chinese', 'math', 'english', 'total']
第一条记录: {'name': '张伟', 'sid': '2026001', 'cls': '一班', 'chinese': 88, 'math': 92, 'english': 85, 'total': 265}
注意 for s in students 遍历的是元素本身(字典),在循环体内直接修改 s['total'] 是在给原字典补字段——列表套字典的结构让”统一加工每条记录”变得非常自然。这份数据集在本篇多次使用:为保证后面每个代码块都能直接复制运行,需要数据的块都会自带完整定义,内容与本块一致,可以快速略读。
2. 排序:先理解稳定性
排序前先做一个 3 元素的最小验证,把稳定性的语义钉死:(1, 'a') 与 (1, 'b') 的键同为 1,排序后 'a' 依然在 'b' 前面。
pairs = [(1, 'a'), (1, 'b'), (0, 'a')]
print('整体排序: ', sorted(pairs))
print('只看第一键排序:', sorted(pairs, key=lambda p: p[0]))
运行结果:
整体排序: [(0, 'a'), (1, 'a'), (1, 'b')]
只看第一键排序: [(0, 'a'), (1, 'a'), (1, 'b')]
第二行只按第一键排序,(1, 'a') 仍排在 (1, 'b') 之前——不是运气,是稳定性保证。后面”多级排序”会直接用到这个性质。
3. 按总分排序:key 与 reverse
成绩分析的第一步是按总分降序排出名次。key=lambda s: s['total'] 取出每个学生的总分作为排序键,reverse=True 整体反转比较方向得到降序。
students = [
{'name': '张伟', 'sid': '2026001', 'cls': '一班', 'chinese': 88, 'math': 92, 'english': 85},
{'name': '李娜', 'sid': '2026002', 'cls': '一班', 'chinese': 76, 'math': 81, 'english': 90},
{'name': '王强', 'sid': '2026003', 'cls': '一班', 'chinese': 92, 'math': 78, 'english': 80},
{'name': '刘洋', 'sid': '2026004', 'cls': '一班', 'chinese': 65, 'math': 59, 'english': 70},
{'name': '陈静', 'sid': '2026005', 'cls': '一班', 'chinese': 81, 'math': 84, 'english': 62},
{'name': '赵磊', 'sid': '2026006', 'cls': '二班', 'chinese': 70, 'math': 95, 'english': 88},
{'name': '孙丽', 'sid': '2026007', 'cls': '二班', 'chinese': 85, 'math': 72, 'english': 91},
{'name': '周明', 'sid': '2026008', 'cls': '二班', 'chinese': 58, 'math': 66, 'english': 73},
{'name': '吴婷', 'sid': '2026009', 'cls': '二班', 'chinese': 90, 'math': 88, 'english': 96},
{'name': '郑浩', 'sid': '2026010', 'cls': '二班', 'chinese': 73, 'math': 61, 'english': 55},
]
for s in students:
s['total'] = s['chinese'] + s['math'] + s['english']
by_total = sorted(students, key=lambda s: s['total'], reverse=True)
for rank, s in enumerate(by_total, 1):
print(f'{rank:>2} {s["name"]} {s["total"]}')
运行结果:
1 吴婷 274
2 张伟 265
3 赵磊 253
4 王强 250
5 孙丽 248
6 李娜 247
7 陈静 227
8 周明 197
9 刘洋 194
10 郑浩 189
enumerate(by_total, 1) 把排序结果与自然序号绑定,生成 1 到 10 的排名。sorted 返回新列表,students 本身未被改动——这一点后面还要反复强调。
4. 按学号、姓名排序:字符串键的两种语义
按学号排序时,学号是定长、零填充的数字字符串('2026001'),它的字典序恰好等于数值序,直接当字符串排即可。按姓名排序则暴露一个语言层面的细节:
students = [
{'name': '张伟', 'sid': '2026001', 'cls': '一班', 'chinese': 88, 'math': 92, 'english': 85},
{'name': '李娜', 'sid': '2026002', 'cls': '一班', 'chinese': 76, 'math': 81, 'english': 90},
{'name': '王强', 'sid': '2026003', 'cls': '一班', 'chinese': 92, 'math': 78, 'english': 80},
{'name': '刘洋', 'sid': '2026004', 'cls': '一班', 'chinese': 65, 'math': 59, 'english': 70},
{'name': '陈静', 'sid': '2026005', 'cls': '一班', 'chinese': 81, 'math': 84, 'english': 62},
{'name': '赵磊', 'sid': '2026006', 'cls': '二班', 'chinese': 70, 'math': 95, 'english': 88},
{'name': '孙丽', 'sid': '2026007', 'cls': '二班', 'chinese': 85, 'math': 72, 'english': 91},
{'name': '周明', 'sid': '2026008', 'cls': '二班', 'chinese': 58, 'math': 66, 'english': 73},
{'name': '吴婷', 'sid': '2026009', 'cls': '二班', 'chinese': 90, 'math': 88, 'english': 96},
{'name': '郑浩', 'sid': '2026010', 'cls': '二班', 'chinese': 73, 'math': 61, 'english': 55},
]
for s in students:
s['total'] = s['chinese'] + s['math'] + s['english']
by_sid = sorted(students, key=lambda s: s['sid'])
print('按学号(字符串)排序前三名:', [s['sid'] for s in by_sid][:3])
by_name = sorted(students, key=lambda s: s['name'])
print('按姓名(Unicode 码点)排序:')
for s in by_name:
print(' ', s['name'], s['sid'])
运行结果:
按学号(字符串)排序前三名: ['2026001', '2026002', '2026003']
按姓名(Unicode 码点)排序:
刘洋 2026004
吴婷 2026009
周明 2026008
孙丽 2026007
张伟 2026001
李娜 2026002
王强 2026003
赵磊 2026006
郑浩 2026010
陈静 2026005
'刘' 的码点 U+5218 小于 '郑' 的 U+90D1,所以顺序既不是拼音序也不是笔画序,而是码点序。这不是 bug,是”字符串比较 = 逐字符比较码点”的必然结果。真实系统若要按拼音排序,需要第三方库(如 pypinyin),超出了本章范围——知道这个边界本身,就是大学水平的体现。
5. 多级排序:班级内按总分降序
需求”先按班级、班级内再按总分降序”是典型的多级排序。sorted 的 key 可以返回元组,元组按第一项、第二项依次比较;第二项写 -s['total'] 实现对总分这一级的单独反转。注意 reverse=True 会把整个元组比较反转(班级、总分全都反转),所以这里只用负号做单级反转。
students = [
{'name': '张伟', 'sid': '2026001', 'cls': '一班', 'chinese': 88, 'math': 92, 'english': 85},
{'name': '李娜', 'sid': '2026002', 'cls': '一班', 'chinese': 76, 'math': 81, 'english': 90},
{'name': '王强', 'sid': '2026003', 'cls': '一班', 'chinese': 92, 'math': 78, 'english': 80},
{'name': '刘洋', 'sid': '2026004', 'cls': '一班', 'chinese': 65, 'math': 59, 'english': 70},
{'name': '陈静', 'sid': '2026005', 'cls': '一班', 'chinese': 81, 'math': 84, 'english': 62},
{'name': '赵磊', 'sid': '2026006', 'cls': '二班', 'chinese': 70, 'math': 95, 'english': 88},
{'name': '孙丽', 'sid': '2026007', 'cls': '二班', 'chinese': 85, 'math': 72, 'english': 91},
{'name': '周明', 'sid': '2026008', 'cls': '二班', 'chinese': 58, 'math': 66, 'english': 73},
{'name': '吴婷', 'sid': '2026009', 'cls': '二班', 'chinese': 90, 'math': 88, 'english': 96},
{'name': '郑浩', 'sid': '2026010', 'cls': '二班', 'chinese': 73, 'math': 61, 'english': 55},
]
for s in students:
s['total'] = s['chinese'] + s['math'] + s['english']
grouped = sorted(students, key=lambda s: (s['cls'], -s['total']))
for s in grouped:
print(f'{s["cls"]} {s["name"]} {s["total"]}')
运行结果:
一班 张伟 265
一班 王强 250
一班 李娜 247
一班 陈静 227
一班 刘洋 194
二班 吴婷 274
二班 赵磊 253
二班 孙丽 248
二班 周明 197
二班 郑浩 189
改用稳定性思想也能得到同一结果:先 sorted(students, key=lambda s: s['total'], reverse=True),再按 s['cls'] 稳定排一遍——第二次排序时班级相同的学生保持第一次排序的总分降序。元组键写法一行搞定,是推荐方案;稳定性方案的价值在于它揭示原理,也让”不能用负数的字符串键”(如想按姓名降序)有了出路:单独写 reverse=True 的那一次排序即可。
6. 筛选:及格线与单科前三
筛选用列表推导式的 if 子句。把及格线”三门全部 ≥ 60”翻译成 min(...) >= 60,一次过滤掉刘洋(数学 59)、周明(语文 58)、郑浩(英语 55)三人,剩下 7 人。单科前三则是”排序 + 切片”的组合:[:3] 取降序结果的前三个。
students = [
{'name': '张伟', 'sid': '2026001', 'cls': '一班', 'chinese': 88, 'math': 92, 'english': 85},
{'name': '李娜', 'sid': '2026002', 'cls': '一班', 'chinese': 76, 'math': 81, 'english': 90},
{'name': '王强', 'sid': '2026003', 'cls': '一班', 'chinese': 92, 'math': 78, 'english': 80},
{'name': '刘洋', 'sid': '2026004', 'cls': '一班', 'chinese': 65, 'math': 59, 'english': 70},
{'name': '陈静', 'sid': '2026005', 'cls': '一班', 'chinese': 81, 'math': 84, 'english': 62},
{'name': '赵磊', 'sid': '2026006', 'cls': '二班', 'chinese': 70, 'math': 95, 'english': 88},
{'name': '孙丽', 'sid': '2026007', 'cls': '二班', 'chinese': 85, 'math': 72, 'english': 91},
{'name': '周明', 'sid': '2026008', 'cls': '二班', 'chinese': 58, 'math': 66, 'english': 73},
{'name': '吴婷', 'sid': '2026009', 'cls': '二班', 'chinese': 90, 'math': 88, 'english': 96},
{'name': '郑浩', 'sid': '2026010', 'cls': '二班', 'chinese': 73, 'math': 61, 'english': 55},
]
for s in students:
s['total'] = s['chinese'] + s['math'] + s['english']
passed = [s for s in students if min(s['chinese'], s['math'], s['english']) >= 60]
print(f'三门全部及格 {len(passed)} 人:')
for s in passed:
print(' ', s['name'], s['total'])
top_math = sorted(students, key=lambda s: s['math'], reverse=True)[:3]
print('数学单科前三:')
for s in top_math:
print(' ', s['name'], s['math'])
运行结果:
三门全部及格 7 人:
张伟 265
李娜 247
王强 250
陈静 227
赵磊 253
孙丽 248
吴婷 274
数学单科前三:
赵磊 95
张伟 92
吴婷 88
min(a, b, c) >= 60 等价于三个条件同时成立,把”三门全部及格”翻译成一行代码——这是筛选条件的最简表达,比连写三个 and 更不易出错。
7. 聚合:按班级统计与人数计数
聚合的通用模式是”defaultdict + 循环累积”。统计平均分、最高最低需要先收集该组全部总分,所以累积器用列表:cls_totals[s['cls']].append(s['total'])。计数类聚合则交给 Counter,它累加整数并支持 most_common。
from collections import Counter, defaultdict
students = [
{'name': '张伟', 'sid': '2026001', 'cls': '一班', 'chinese': 88, 'math': 92, 'english': 85},
{'name': '李娜', 'sid': '2026002', 'cls': '一班', 'chinese': 76, 'math': 81, 'english': 90},
{'name': '王强', 'sid': '2026003', 'cls': '一班', 'chinese': 92, 'math': 78, 'english': 80},
{'name': '刘洋', 'sid': '2026004', 'cls': '一班', 'chinese': 65, 'math': 59, 'english': 70},
{'name': '陈静', 'sid': '2026005', 'cls': '一班', 'chinese': 81, 'math': 84, 'english': 62},
{'name': '赵磊', 'sid': '2026006', 'cls': '二班', 'chinese': 70, 'math': 95, 'english': 88},
{'name': '孙丽', 'sid': '2026007', 'cls': '二班', 'chinese': 85, 'math': 72, 'english': 91},
{'name': '周明', 'sid': '2026008', 'cls': '二班', 'chinese': 58, 'math': 66, 'english': 73},
{'name': '吴婷', 'sid': '2026009', 'cls': '二班', 'chinese': 90, 'math': 88, 'english': 96},
{'name': '郑浩', 'sid': '2026010', 'cls': '二班', 'chinese': 73, 'math': 61, 'english': 55},
]
for s in students:
s['total'] = s['chinese'] + s['math'] + s['english']
s['pass_all'] = min(s['chinese'], s['math'], s['english']) >= 60
cls_totals = defaultdict(list)
for s in students:
cls_totals[s['cls']].append(s['total'])
for cls, totals in sorted(cls_totals.items()):
print(f'{cls} 人数 {len(totals)} 平均 {sum(totals) / len(totals):.1f} '
f'最高 {max(totals)} 最低 {min(totals)}')
pass_counter = Counter(s['cls'] for s in students if s['pass_all'])
print('各班全部及格人数:', dict(pass_counter))
运行结果:
一班 人数 5 平均 236.6 最高 265 最低 194
二班 人数 5 平均 232.2 最高 274 最低 189
各班全部及格人数: {'一班': 4, '二班': 3}
三个知识点在这里汇合:分组标签当字典键(第 3 篇)、defaultdict(list) 收集组成员(第 7 篇)、sum/max/min/len 四个内置归并函数做汇总。平均分保留一位小数用 :.1f(第五章格式化);若平均分恰好是整数(比如 240),也会显示成 240.0——只是显示形式不同,数值不变,不影响排序与比较。
8. 对齐报表:中文显示宽度的问题与解决
报表输出的难点不是 f-string 语法,而是宽度语义。f'{x:<6}' 的 6 按”字符数”填充,但中文字符在终端里占据两个英文字符的显示宽度。直接排版的结果:
print(f"{'姓名':<6}|{'学号':<8}")
print(f"{'张三丰':<6}|{'2026001':<8}")
运行结果:
姓名 |学号
张三丰 |2026001
以 | 为参照可以发现两行错位:'张三丰' 的显示宽度是 6 列,但 :<6 只按字符数补了 3 个空格,实际占 9 列,比表头”姓名”那列的 8 列宽出一列——错位就是这么来的。解决办法是引入一个”按显示宽度填充”的小助手:用标准库 unicodedata.east_asian_width 判断每个字符是宽字符(中文、日文、韩文等,宽 2 列)还是窄字符(ASCII 等,宽 1 列)。'WF' 覆盖宽字符(W,Wide)与全角字符(F,Fullwidth)两类。
import unicodedata
def display_width(text):
return sum(2 if unicodedata.east_asian_width(ch) in 'WF' else 1 for ch in str(text))
def cell(text, width, align='left'):
text = str(text)
padding = ' ' * max(width - display_width(text), 0)
return padding + text if align == 'right' else text + padding
def row(cells):
return ' '.join(cells)
students = [
{'name': '张伟', 'sid': '2026001', 'cls': '一班', 'chinese': 88, 'math': 92, 'english': 85},
{'name': '李娜', 'sid': '2026002', 'cls': '一班', 'chinese': 76, 'math': 81, 'english': 90},
{'name': '王强', 'sid': '2026003', 'cls': '一班', 'chinese': 92, 'math': 78, 'english': 80},
{'name': '刘洋', 'sid': '2026004', 'cls': '一班', 'chinese': 65, 'math': 59, 'english': 70},
{'name': '陈静', 'sid': '2026005', 'cls': '一班', 'chinese': 81, 'math': 84, 'english': 62},
{'name': '赵磊', 'sid': '2026006', 'cls': '二班', 'chinese': 70, 'math': 95, 'english': 88},
{'name': '孙丽', 'sid': '2026007', 'cls': '二班', 'chinese': 85, 'math': 72, 'english': 91},
{'name': '周明', 'sid': '2026008', 'cls': '二班', 'chinese': 58, 'math': 66, 'english': 73},
{'name': '吴婷', 'sid': '2026009', 'cls': '二班', 'chinese': 90, 'math': 88, 'english': 96},
{'name': '郑浩', 'sid': '2026010', 'cls': '二班', 'chinese': 73, 'math': 61, 'english': 55},
]
for s in students:
s['total'] = s['chinese'] + s['math'] + s['english']
print(row([cell('排名', 4, 'right'), cell('姓名', 6), cell('学号', 8), cell('班级', 6), cell('总分', 4, 'right')]))
for rank, s in enumerate(sorted(students, key=lambda s: s['total'], reverse=True), 1):
print(row([cell(rank, 4, 'right'), cell(s['name'], 6), cell(s['sid'], 8),
cell(s['cls'], 6), cell(s['total'], 4, 'right')]))
运行结果:
排名 姓名 学号 班级 总分
1 吴婷 2026009 二班 274
2 张伟 2026001 一班 265
3 赵磊 2026006 二班 253
4 王强 2026003 一班 250
5 孙丽 2026007 二班 248
6 李娜 2026002 一班 247
7 陈静 2026005 一班 227
8 周明 2026008 二班 197
9 刘洋 2026004 一班 194
10 郑浩 2026010 二班 189
至此,排序 → 筛选 → 聚合 → 报表的完整数据管线跑通:display_width 与 cell 只有 7 行,却是”报表可读性”的关键,也是第五章格式化知识在真实工程中的自然延伸。
易错点与陷阱
1. 混淆 sorted() 与 list.sort()。 sorted(students) 返回新列表,students 原封不动;students.sort() 原地排列并返回 None。最常见的翻车代码是 students = students.sort(),把列表覆盖成 None,下一行 for s in students 直接 TypeError。经验法则:只要不打算放弃原序列,一律用 sorted。
2. 多级排序时滥用 reverse=True。 reverse=True 反转的是整个排序键的比较结果。对元组键 (s['cls'], s['total']) 加 reverse=True,班级和总分都会变成降序——“班级升序 + 总分降序”只能靠 -s['total'] 这种负号技巧(仅数字键可用)或两次稳定排序实现。写多级排序前先自问:每一级的升降方向分别是什么?
3. 中文排序结果与直觉不符。 内置排序按 Unicode 码点比较中文,既非拼音也非笔画,“张伟”排到”赵磊”前面并不是排序坏了。需要拼音序时用 pypinyin 之类第三方库,或干脆换用学号、拼音字段作为排序键。这是中文数据处理的常见坑,不是 Python 的缺陷。
4. 聚合结果的分组键顺序不固定。 遍历 defaultdict/普通字典的分组结果时,输出顺序是插入顺序,不保证按班级名的字典序。报表场景务必先 sorted(cls_totals.items()) 再输出,否则同一份数据换台机器可能打印出不同顺序的表格。
小结
本篇把前 8 篇的容器知识拧成一条真实的数据管线:列表套字典组织记录,sorted + key 完成单级与多级排序(稳定性和 -score 负号是两大原理),列表推导式做筛选,defaultdict 与 Counter 做分组聚合,最后用一个 7 行的宽度助手解决了中文报表的对齐问题。下一篇是第六章综合测验,本篇的排序键、reverse、筛选与聚合几乎必考;动手把每个代码块跑一遍,比背诵二十个方法更有效——因为数据处理的判断力来自”看到结构性数据就知道该调用什么”,而这只来自重复练习。
练习与思考题
- 通用单科排名:把”数学单科前三”改成函数
top_by(students, subject, n),按subject指定的科目取前n名,并保持姓名与分数并列输出。验证top_by(students, 'english', 2)应输出吴婷 96、孙丽 91。 - 稳定排序实现多级:只用两次
sorted(先总分降序,再按班级稳定排序)复现”班级内总分降序”的名单,并断言其结果与本篇元组键版本完全一致。 - 平均分报表:用本篇的
cell助手输出各班平均分报表(班级、人数、平均分一列,平均分保留一位小数右对齐),验证一行结果:一班平均 236.6。 - 思考题:
cell(text, width, align)中max(width - display_width(text), 0)的max(..., 0)有什么作用?如果去掉它,当内容显示宽度超过width时会发生什么?(提示:字符串乘负整数。)