系列导读 :本文是《从零到精通 Python》系列第 04 篇。前三篇打好了变量、运算符和流程控制的基础,本篇深入 Python 中最常用的两大序列类型:列表(list) 和 元组(tuple)。从底层内存模型到高阶技巧,配合完整的学生成绩管理系统实战,帮你真正掌握序列操作的精髓。
目录
- 序列类型概览
- 列表:创建、索引与切片
- 列表常用方法详解
- 列表推导式与嵌套推导
- 元组:不可变性与应用场景
- 序列解包与星号表达式
- [列表 vs 元组:性能与选型对比](#列表 vs 元组:性能与选型对比)
- [实操 Demo:学生成绩管理系统](#实操 Demo:学生成绩管理系统)
- 总结与拓展
1. 序列类型概览
Python 中的**序列(Sequence)**是一类支持下标访问、切片操作和迭代的有序数据结构。内置序列类型包括:
| 类型 | 可变性 | 可重复 | 典型用途 |
|---|---|---|---|
list |
可变 | 是 | 动态数据集合,增删改查 |
tuple |
不可变 | 是 | 固定数据、函数多返回值、dict key |
str |
不可变 | 是 | 文本处理 |
range |
不可变 | 否 | 整数区间、循环迭代 |
bytes |
不可变 | 是 | 二进制数据 |
所有序列共享以下通用操作:
python
seq = [10, 20, 30, 40, 50]
# 索引访问
print(seq[0]) # 10 (正向)
print(seq[-1]) # 50 (反向)
# 切片
print(seq[1:4]) # [20, 30, 40]
print(seq[::2]) # [10, 30, 50]
# 长度 / 成员检查 / 拼接 / 重复
print(len(seq)) # 5
print(20 in seq) # True
print(seq + [60, 70]) # [10, 20, 30, 40, 50, 60, 70]
print([0] * 3) # [0, 0, 0]
# 最小/最大/求和
print(min(seq), max(seq), sum(seq)) # 10 50 150
重要心智模型 :序列本质上是内存中一块连续(或近似连续)的存储,下标
i对应偏移量i × 元素指针大小,因此随机访问时间复杂度为 O(1)。
2. 列表:创建、索引与切片
2.1 列表的多种创建方式
python
# 方式 1:字面量
fruits = ['apple', 'banana', 'cherry']
# 方式 2:list() 构造函数(可接受任意可迭代对象)
chars = list('hello') # ['h', 'e', 'l', 'l', 'o']
nums = list(range(1, 6)) # [1, 2, 3, 4, 5]
# 方式 3:列表推导式(后面详述)
squares = [x**2 for x in range(1, 6)] # [1, 4, 9, 16, 25]
# 方式 4:重复操作
zeros = [0] * 5 # [0, 0, 0, 0, 0]
# 方式 5:二维列表(注意陷阱!)
# 错误做法 ------ 所有行指向同一个对象
matrix_bad = [[0] * 3] * 3
matrix_bad[0][0] = 1
print(matrix_bad) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] ← 三行都被改了!
# 正确做法 ------ 推导式创建独立行对象
matrix_ok = [[0] * 3 for _ in range(3)]
matrix_ok[0][0] = 1
print(matrix_ok) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]] ← 只改了第一行
陷阱解析 :
[[0]*3]*3创建的是三个指向同一列表对象的引用,修改其中一个会影响所有行。用推导式则每次创建新的独立列表。
2.2 索引访问
Python 支持正向索引 (从 0 开始)和反向索引(从 -1 开始):
列表: ['a', 'b', 'c', 'd', 'e']
正向: 0 1 2 3 4
反向: -5 -4 -3 -2 -1
python
data = ['a', 'b', 'c', 'd', 'e']
print(data[0]) # 'a'
print(data[4]) # 'e'
print(data[-1]) # 'e' ← 最后一个元素
print(data[-2]) # 'd' ← 倒数第二个
# 修改元素(列表可变)
data[0] = 'A'
print(data) # ['A', 'b', 'c', 'd', 'e']
# 越界访问会抛出 IndexError
# data[10] # IndexError: list index out of range
2.3 切片详解
切片语法:seq[start : stop : step]
start:起始下标(包含),默认 0stop:结束下标(不含),默认序列末尾step:步长,默认 1,可为负数(反向遍历)
python
s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 基础切片
print(s[2:6]) # [2, 3, 4, 5] 取第 2~5 个
print(s[:4]) # [0, 1, 2, 3] 取前 4 个
print(s[7:]) # [7, 8, 9] 取第 7 个到末尾
print(s[:]) # [0, 1, ..., 9] 浅拷贝整个列表
# 带步长
print(s[::2]) # [0, 2, 4, 6, 8] 每隔一个取一个
print(s[1::2]) # [1, 3, 5, 7, 9] 从 1 开始每隔一个
print(s[::-1]) # [9, 8, ..., 0] 反转列表
# 负数下标的切片
print(s[-3:]) # [7, 8, 9] 最后 3 个
print(s[:-3]) # [0, 1, 2, 3, 4, 5, 6] 除最后 3 个
# 切片赋值(原地修改)
s[2:5] = [20, 30, 40]
print(s) # [0, 1, 20, 30, 40, 5, 6, 7, 8, 9]
# 切片删除
del s[2:5]
print(s) # [0, 1, 5, 6, 7, 8, 9]
# 切片替换为不同长度的序列
s[1:3] = [100, 200, 300]
print(s) # [0, 100, 200, 300, 6, 7, 8, 9]
切片与原列表的关系 :
s[a:b]返回一个新列表(浅拷贝),修改切片结果不会影响原列表。但若列表中存储的是可变对象(如嵌套列表),浅拷贝只复制引用。
3. 列表常用方法详解
Python 列表内置了丰富的方法,按功能分为增 、删 、查 、改 、排序五类。
3.1 增加元素
python
lst = [1, 2, 3]
# append(item) ------ 末尾追加单个元素,O(1) 摊销时间复杂度
lst.append(4)
print(lst) # [1, 2, 3, 4]
# append 追加列表是"整体追加"(列表嵌套)
lst.append([5, 6])
print(lst) # [1, 2, 3, 4, [5, 6]]
# extend(iterable) ------ 追加可迭代对象的所有元素
lst2 = [1, 2, 3]
lst2.extend([4, 5, 6])
print(lst2) # [1, 2, 3, 4, 5, 6]
lst2.extend('abc') # 字符串也是可迭代对象
print(lst2) # [1, 2, 3, 4, 5, 6, 'a', 'b', 'c']
# += 等价于 extend
lst3 = [1, 2]
lst3 += [3, 4]
print(lst3) # [1, 2, 3, 4]
# insert(index, item) ------ 在指定位置插入,O(n) 时间复杂度
lst4 = [1, 2, 4, 5]
lst4.insert(2, 3) # 在下标 2 处插入 3
print(lst4) # [1, 2, 3, 4, 5]
lst4.insert(0, 0) # 头部插入
print(lst4) # [0, 1, 2, 3, 4, 5]
lst4.insert(100, 6) # 超出范围相当于 append
print(lst4) # [0, 1, 2, 3, 4, 5, 6]
append vs extend 对比:
| 操作 | lst.append([4,5]) |
lst.extend([4,5]) |
|---|---|---|
| 结果 | [1,2,3,[4,5]] |
[1,2,3,4,5] |
| 元素数 | 增加 1 个 | 增加 2 个 |
| 使用场景 | 追加整体对象 | 展开追加多个元素 |
3.2 删除元素
python
lst = [10, 20, 30, 20, 40, 50]
# pop(index=-1) ------ 删除并返回指定位置的元素
last = lst.pop() # 默认删末尾,O(1)
print(last, lst) # 50 [10, 20, 30, 20, 40]
item = lst.pop(1) # 删下标 1 处,O(n)
print(item, lst) # 20 [10, 30, 20, 40]
# remove(value) ------ 删除第一个值为 value 的元素,O(n)
lst.remove(20)
print(lst) # [10, 30, 40]
# 若元素不存在,会抛出 ValueError
# lst.remove(999) # ValueError: list.remove(x): x not in list
# 安全删除写法
if 999 in lst:
lst.remove(999)
# del 语句 ------ 可删除元素或切片
lst2 = [1, 2, 3, 4, 5]
del lst2[2]
print(lst2) # [1, 2, 4, 5]
del lst2[1:3]
print(lst2) # [1, 5]
# clear() ------ 清空列表
lst3 = [1, 2, 3]
lst3.clear()
print(lst3) # []
3.3 查找与统计
python
lst = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
# index(value, start, end) ------ 返回第一个匹配的下标
print(lst.index(1)) # 1
print(lst.index(1, 2)) # 3 (从下标 2 开始查)
print(lst.index(5, 4, 7)) # 4 (在 [4,7) 范围内查)
# count(value) ------ 统计出现次数
print(lst.count(1)) # 2
print(lst.count(5)) # 2
print(lst.count(0)) # 0
3.4 排序方法
这是面试高频考点,sort() 和 sorted() 必须分清!
python
nums = [3, 1, 4, 1, 5, 9, 2, 6, 5]
# sort() ------ 原地排序,返回 None,修改原列表
nums.sort()
print(nums) # [1, 1, 2, 3, 4, 5, 5, 6, 9]
nums.sort(reverse=True)
print(nums) # [9, 6, 5, 5, 4, 3, 2, 1, 1]
# sorted() ------ 返回新列表,原列表不变
original = [3, 1, 4, 1, 5]
new_sorted = sorted(original)
print(original) # [3, 1, 4, 1, 5] ← 未变
print(new_sorted) # [1, 1, 3, 4, 5] ← 新列表
# key 参数:自定义排序键
words = ['banana', 'apple', 'cherry', 'fig', 'date']
words.sort(key=len) # 按字符串长度排序
print(words) # ['fig', 'date', 'apple', 'banana', 'cherry']
words.sort(key=lambda w: w[-1]) # 按最后一个字母排序
print(words)
# 复杂对象排序
students = [
{'name': 'Alice', 'score': 88},
{'name': 'Bob', 'score': 95},
{'name': 'Carol', 'score': 72},
]
students.sort(key=lambda s: s['score'], reverse=True)
for s in students:
print(f"{s['name']}: {s['score']}")
# Bob: 95 / Alice: 88 / Carol: 72
# reverse() ------ 原地反转(不排序,只翻转顺序)
lst = [1, 2, 3, 4, 5]
lst.reverse()
print(lst) # [5, 4, 3, 2, 1]
Python 排序算法 :底层使用 Timsort,时间复杂度为 O(n log n),对几乎有序的数据有更好的性能表现。Timsort 是稳定排序,相等元素的相对顺序不变。
3.5 其他实用方法
python
lst = [1, 2, 3]
# copy() ------ 浅拷贝(等价于 lst[:])
lst_copy = lst.copy()
lst_copy.append(4)
print(lst) # [1, 2, 3] ← 原列表未受影响
print(lst_copy) # [1, 2, 3, 4]
# 注意:浅拷贝只复制一层,嵌套对象仍是引用
nested = [[1, 2], [3, 4]]
nested_copy = nested.copy()
nested_copy[0].append(99)
print(nested) # [[1, 2, 99], [3, 4]] ← 内层被影响了!
# 深拷贝用 copy.deepcopy()
import copy
nested_deep = copy.deepcopy(nested)
4. 列表推导式与嵌套推导
列表推导式(List Comprehension)是 Python 最具代表性的语法糖之一,简洁、Pythonic 且通常比 for 循环更快(内部用 C 实现)。
4.1 基础语法
[expression for variable in iterable if condition]
python
# 等价的传统写法 vs 推导式写法
# 传统
squares = []
for x in range(1, 11):
squares.append(x ** 2)
# 推导式
squares = [x ** 2 for x in range(1, 11)]
print(squares) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# 带条件过滤:只取偶数的平方
even_squares = [x ** 2 for x in range(1, 11) if x % 2 == 0]
print(even_squares) # [4, 16, 36, 64, 100]
# 字符串处理
words = [' hello ', ' world ', ' Python ']
cleaned = [w.strip().upper() for w in words]
print(cleaned) # ['HELLO', 'WORLD', 'PYTHON']
# 条件表达式(三元运算符)
nums = [1, -2, 3, -4, 5]
abs_nums = [x if x >= 0 else -x for x in nums]
print(abs_nums) # [1, 2, 3, 4, 5]
# 等价于 [abs(x) for x in nums]
4.2 多重 for 的嵌套推导
python
# 双重 for:笛卡尔积
pairs = [(x, y) for x in [1, 2, 3] for y in ['a', 'b']]
print(pairs)
# [(1,'a'), (1,'b'), (2,'a'), (2,'b'), (3,'a'), (3,'b')]
# 等价传统写法
pairs_trad = []
for x in [1, 2, 3]:
for y in ['a', 'b']:
pairs_trad.append((x, y))
# 矩阵转置(经典嵌套推导)
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
transposed = [[row[i] for row in matrix] for i in range(3)]
print(transposed)
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
# 展开(flatten)嵌套列表
nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat = [item for sublist in nested for item in sublist]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 带条件的多重推导
# 找出所有 i≠j 且 i+j 为偶数 的 (i,j) 对
pairs_even = [(i, j) for i in range(5) for j in range(5)
if i != j and (i + j) % 2 == 0]
print(pairs_even)
4.3 性能对比
python
import timeit
# 传统 for 循环
def loop_way():
result = []
for x in range(10000):
if x % 2 == 0:
result.append(x ** 2)
return result
# 推导式
def comp_way():
return [x ** 2 for x in range(10000) if x % 2 == 0]
# map + filter(函数式风格)
def map_way():
return list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, range(10000))))
t1 = timeit.timeit(loop_way, number=1000)
t2 = timeit.timeit(comp_way, number=1000)
t3 = timeit.timeit(map_way, number=1000)
print(f"loop: {t1:.3f}s")
print(f"comp: {t2:.3f}s") # 通常最快
print(f"map: {t3:.3f}s")
经验准则:
- 简单变换用推导式,可读性最佳
- 逻辑复杂(多层嵌套 + 多个条件)时,拆分为普通循环,避免可读性下降
- 超大数据集考虑生成器表达式
(x for x in ...)节省内存
4.4 生成器表达式(对比项)
python
# 列表推导式 ------ 立即生成所有元素,存储在内存中
lst_comp = [x ** 2 for x in range(10**6)] # 占用约 8 MB
# 生成器表达式 ------ 惰性求值,按需生成,内存极低
gen_exp = (x ** 2 for x in range(10**6)) # 占用约 112 字节
import sys
print(sys.getsizeof(lst_comp)) # ~8 MB
print(sys.getsizeof(gen_exp)) # ~112 B
# 用于 sum / max 等场景时优先用生成器
total = sum(x ** 2 for x in range(10**6)) # 无需构建中间列表
5. 元组:不可变性与应用场景
5.1 元组的创建
python
# 圆括号创建(圆括号本身不是元组的定义,逗号才是)
t1 = (1, 2, 3)
t2 = 1, 2, 3 # 不带括号也可以(打包)
t3 = (42,) # 单元素元组:必须有逗号!
t4 = (42) # 这不是元组,只是整数 42
print(type(t1)) # <class 'tuple'>
print(type(t4)) # <class 'int'>
# tuple() 构造函数
t5 = tuple([1, 2, 3]) # 从列表转换
t6 = tuple('hello') # ('h', 'e', 'l', 'l', 'o')
t7 = tuple(range(5)) # (0, 1, 2, 3, 4)
# 空元组
t_empty = ()
t_empty2 = tuple()
5.2 不可变性的本质
python
t = (1, 2, 3)
# 尝试修改会抛出 TypeError
# t[0] = 10 # TypeError: 'tuple' object does not support item assignment
# t.append(4) # AttributeError: 'tuple' object has no attribute 'append'
# 但是!元组的"不可变"是指元组本身的引用不可变
# 如果元素是可变对象,该对象内部仍可修改
t_mut = ([1, 2], [3, 4])
t_mut[0].append(99)
print(t_mut) # ([1, 2, 99], [3, 4]) ← 内层列表被修改了
# 这意味着含可变对象的元组不能作为 dict 的 key
# d = {([1,2], [3,4]): 'value'} # TypeError: unhashable type: 'list'
# 只含不可变元素的元组是 hashable 的
d = {(1, 2): 'point_a', (3, 4): 'point_b'}
print(d[(1, 2)]) # 'point_a'
5.3 元组的内存优势
python
import sys
lst = [1, 2, 3, 4, 5]
tup = (1, 2, 3, 4, 5)
print(sys.getsizeof(lst)) # 104 字节(Python 3.x,含预分配空间)
print(sys.getsizeof(tup)) # 80 字节(更紧凑)
元组比等长列表少约 15-20% 的内存,原因是列表需要预留额外空间以支持动态扩容。
5.4 元组的核心应用场景
场景一:函数多返回值
python
def minmax(seq):
return min(seq), max(seq) # 隐式打包为元组
lo, hi = minmax([3, 1, 4, 1, 5, 9])
print(lo, hi) # 1 9
场景二:字典的键
python
# 二维坐标系
grid = {}
grid[(0, 0)] = 'origin'
grid[(1, 2)] = 'point_a'
grid[(3, -1)] = 'point_b'
print(grid[(1, 2)]) # 'point_a'
场景三:命名元组(结构化数据)
python
from collections import namedtuple
# 定义结构
Point = namedtuple('Point', ['x', 'y'])
Student = namedtuple('Student', 'name age score') # 空格分隔也可以
p = Point(3.0, 4.0)
print(p.x, p.y) # 3.0 4.0
print(p[0], p[1]) # 3.0 4.0(仍支持下标)
dist = (p.x**2 + p.y**2)**0.5
print(f"距离原点: {dist}") # 5.0
s = Student('Alice', 20, 95)
print(s.name, s.score) # Alice 95
print(s._asdict()) # OrderedDict([('name', 'Alice'), ...])
# Python 3.6+ 推荐使用 typing.NamedTuple(更现代)
from typing import NamedTuple
class Point3D(NamedTuple):
x: float
y: float
z: float = 0.0 # 支持默认值
p3 = Point3D(1.0, 2.0)
print(p3) # Point3D(x=1.0, y=2.0, z=0.0)
场景四:常量配置(防止意外修改)
python
# 用元组存储不该被修改的配置
WEEKDAYS = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
HTTP_METHODS = ('GET', 'POST', 'PUT', 'DELETE', 'PATCH')
COLORS = ((255, 0, 0), (0, 255, 0), (0, 0, 255)) # RGB
# 可以直接用 in 检查
def is_weekday(day):
return day in WEEKDAYS[:5]
print(is_weekday('Mon')) # True
print(is_weekday('Sun')) # False
6. 序列解包与星号表达式
序列解包(Sequence Unpacking)是 Python 最优雅的特性之一,可以一次性将序列的元素赋值给多个变量。
6.1 基础解包
python
# 列表、元组、字符串均可解包
point = (3, 4)
x, y = point
print(x, y) # 3 4
rgb = [255, 128, 0]
r, g, b = rgb
print(r, g, b) # 255 128 0
# 字符串解包
a, b, c = 'xyz'
print(a, b, c) # x y z
# 交换变量(经典用法,无需临时变量)
a, b = 1, 2
a, b = b, a
print(a, b) # 2 1
# 嵌套解包
matrix_row = ((1, 2), (3, 4))
(a, b), (c, d) = matrix_row
print(a, b, c, d) # 1 2 3 4
# 函数返回多值的解包
def get_stats(data):
return min(data), max(data), sum(data) / len(data)
lo, hi, avg = get_stats([85, 92, 78, 96, 88])
print(f"最低:{lo} 最高:{hi} 平均:{avg:.1f}")
6.2 星号表达式(Extended Unpacking)
Python 3 引入的 *variable 语法,可以"贪婪地"捕获剩余元素:
python
# 基础用法
first, *rest = [1, 2, 3, 4, 5]
print(first) # 1
print(rest) # [2, 3, 4, 5] ← 注意:始终是列表
*head, last = [1, 2, 3, 4, 5]
print(head) # [1, 2, 3, 4]
print(last) # 5
first, *middle, last = [1, 2, 3, 4, 5]
print(first, middle, last) # 1 [2, 3, 4] 5
# 星号变量可以接收 0 个元素
a, *b = [1]
print(a, b) # 1 []
# 忽略某些值用 _ 约定
_, month, day = (2026, 6, 6)
print(f"{month}月{day}日") # 6月6日
# 解包应用于函数调用
def add(a, b, c):
return a + b + c
args = [1, 2, 3]
print(add(*args)) # 6 ← 等价于 add(1, 2, 3)
# ** 解包字典到关键字参数
def greet(name, age):
print(f"你好,{name},{age}岁")
info = {'name': '小明', 'age': 18}
greet(**info) # 你好,小明,18岁
6.3 在循环中解包
python
# 经典:zip + 解包
names = ['Alice', 'Bob', 'Carol']
scores = [88, 95, 72]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# enumerate + 解包
for i, (name, score) in enumerate(zip(names, scores), start=1):
print(f"第{i}名: {name} ({score}分)")
# 解包嵌套结构
records = [
('Alice', 20, 88),
('Bob', 21, 95),
('Carol', 19, 72),
]
for name, age, score in records:
print(f"{name}({age}岁): {score}分")
7. 列表 vs 元组:性能与选型对比
7.1 性能基准测试
python
import timeit
import sys
# 创建时间对比
t_list_create = timeit.timeit('[1, 2, 3, 4, 5]', number=10_000_000)
t_tup_create = timeit.timeit('(1, 2, 3, 4, 5)', number=10_000_000)
print(f"列表创建: {t_list_create:.3f}s")
print(f"元组创建: {t_tup_create:.3f}s")
# 元组创建比列表快约 10-30%(因 Python 会缓存小型元组)
# 内存占用
import sys
lst = list(range(100))
tup = tuple(range(100))
print(f"list(100): {sys.getsizeof(lst)} bytes") # ~856 bytes
print(f"tuple(100): {sys.getsizeof(tup)} bytes") # ~856 bytes(较小型大致相同)
# 索引访问速度(基本相同,都是 O(1))
t_list_idx = timeit.timeit('lst[50]', setup='lst=list(range(100))', number=10_000_000)
t_tup_idx = timeit.timeit('tup[50]', setup='tup=tuple(range(100))', number=10_000_000)
print(f"列表索引: {t_list_idx:.3f}s")
print(f"元组索引: {t_tup_idx:.3f}s")
7.2 全面对比表
| 维度 | list 列表 | tuple 元组 |
|---|---|---|
| 可变性 | 可变(Mutable) | 不可变(Immutable) |
| 内存占用 | 较大(预分配空间) | 较小(紧凑存储) |
| 创建速度 | 稍慢 | 更快(小元组有缓存) |
| 索引访问 | O(1) 相同 | O(1) 相同 |
| 末尾追加 | O(1) 摊销 | 不支持(需重建) |
| 中间插入 | O(n) | 不支持 |
| hashable | 否(不能作 dict key) | 是(元素全为不可变时) |
| 迭代性能 | 相近 | 略快 |
| 方法数量 | 11 个 | 2 个(count / index) |
| 典型用途 | 动态数据集合 | 固定数据、结构化记录 |
| 语义含义 | 同质元素的集合 | 异质元素的结构体 |
7.3 选型决策树
需要存储一组数据
│
├─ 数据会增删改吗?
│ ├─ 是 → 用 list
│ └─ 否 ↓
│
├─ 需要作为 dict 的 key 吗?
│ ├─ 是 → 用 tuple(元素须为不可变类型)
│ └─ 否 ↓
│
├─ 是结构化记录(名字/年龄/分数)吗?
│ ├─ 是 → 用 namedtuple 或 dataclass
│ └─ 否 ↓
│
└─ 是否关注内存/性能(大量数据)?
├─ 是 → 优先 tuple(更紧凑)
└─ 否 → list(更灵活)
Python 之禅的指导:元组表达"这几个东西组合在一起"(异质),列表表达"一堆相同类型的东西"(同质)。这不是语法规定,而是约定俗成的语义区分。
8. 实操 Demo:学生成绩管理系统
将本篇所有知识融合为一个完整的学生成绩管理系统,支持:增删查改、排名统计、成绩分析、数据导出。
python
"""
student_grade_system.py
学生成绩管理系统 ------ Python 系列第04篇实操 Demo
涵盖:列表操作、元组、列表推导式、序列解包
"""
from collections import namedtuple
from typing import List, Optional
import copy
# ── 数据结构定义 ──────────────────────────────────────────────
# 用命名元组定义"成绩记录"(不可变,适合作 key、传参)
ScoreRecord = namedtuple('ScoreRecord', 'subject score grade')
# 年级等级规则(元组常量,不可篡改)
GRADE_RULES = (
(90, 'A'),
(80, 'B'),
(70, 'C'),
(60, 'D'),
(0, 'F'),
)
def calc_grade(score: float) -> str:
"""根据分数计算等级"""
for threshold, grade in GRADE_RULES:
if score >= threshold:
return grade
return 'F'
# ── 核心数据类 ────────────────────────────────────────────────
class GradeBook:
"""学生成绩册(核心数据用列表存储,保证灵活性)"""
def __init__(self):
# 学生数据:[{'name': str, 'records': [ScoreRecord, ...]}]
self._students: List[dict] = []
# ── 增 ──────────────────────────────────────────────────
def add_student(self, name: str) -> bool:
"""添加学生(若已存在则返回 False)"""
if self._find(name) is not None:
print(f"[警告] 学生 '{name}' 已存在")
return False
self._students.append({'name': name, 'records': []})
print(f"[成功] 已添加学生: {name}")
return True
def add_score(self, name: str, subject: str, score: float) -> bool:
"""为学生添加科目成绩"""
if not (0 <= score <= 100):
print(f"[错误] 分数必须在 0~100 之间,收到: {score}")
return False
student = self._find(name)
if student is None:
print(f"[错误] 找不到学生: {name}")
return False
grade = calc_grade(score)
record = ScoreRecord(subject=subject, score=score, grade=grade)
student['records'].append(record)
print(f"[成功] {name} · {subject}: {score}分 ({grade})")
return True
# ── 删 ──────────────────────────────────────────────────
def remove_student(self, name: str) -> bool:
"""删除学生"""
student = self._find(name)
if student is None:
print(f"[错误] 找不到学生: {name}")
return False
self._students.remove(student)
print(f"[成功] 已删除学生: {name}")
return True
def remove_score(self, name: str, subject: str) -> bool:
"""删除某学生的某科目成绩(删除最新一条)"""
student = self._find(name)
if student is None:
return False
# 找到该科目的最新记录
for i in range(len(student['records']) - 1, -1, -1):
if student['records'][i].subject == subject:
removed = student['records'].pop(i)
print(f"[成功] 已删除 {name} · {subject}: {removed.score}分")
return True
print(f"[错误] 找不到 {name} 的 {subject} 成绩")
return False
# ── 查 ──────────────────────────────────────────────────
def get_student_report(self, name: str) -> Optional[dict]:
"""获取单个学生的详细报告"""
student = self._find(name)
if student is None:
print(f"[错误] 找不到学生: {name}")
return None
records = student['records']
if not records:
return {'name': name, 'records': [], 'avg': None, 'total': 0}
# 用推导式提取分数列表
scores = [r.score for r in records]
avg = sum(scores) / len(scores)
total = sum(scores)
return {
'name': name,
'records': records,
'scores': scores,
'avg': avg,
'total': total,
'grade': calc_grade(avg),
'best': max(records, key=lambda r: r.score),
'worst': min(records, key=lambda r: r.score),
}
def get_class_ranking(self, subject: Optional[str] = None) -> list:
"""
生成班级排名
subject=None: 按总平均分排名
subject='数学': 按指定科目排名
"""
rankings = []
for student in self._students:
if subject is None:
# 按平均分
scores = [r.score for r in student['records']]
if scores:
avg = sum(scores) / len(scores)
rankings.append((student['name'], avg, len(scores)))
else:
# 按指定科目
subject_scores = [r.score for r in student['records']
if r.subject == subject]
if subject_scores:
latest = subject_scores[-1] # 取最新成绩
rankings.append((student['name'], latest, 1))
# 按分数降序排列
rankings.sort(key=lambda x: x[1], reverse=True)
return rankings
def get_subject_stats(self, subject: str) -> dict:
"""获取某科目的班级统计数据"""
all_scores = [
r.score
for student in self._students
for r in student['records']
if r.subject == subject
]
if not all_scores:
return {}
all_scores.sort()
n = len(all_scores)
median = (all_scores[n//2] if n % 2 == 1
else (all_scores[n//2-1] + all_scores[n//2]) / 2)
# 用推导式统计各等级人数
grade_counts = {
g: sum(1 for s in all_scores if calc_grade(s) == g)
for g in ('A', 'B', 'C', 'D', 'F')
}
return {
'subject': subject,
'count': n,
'avg': sum(all_scores) / n,
'max': max(all_scores),
'min': min(all_scores),
'median': median,
'pass_rate': sum(1 for s in all_scores if s >= 60) / n * 100,
'grade_counts': grade_counts,
}
# ── 改 ──────────────────────────────────────────────────
def update_score(self, name: str, subject: str, new_score: float) -> bool:
"""更新成绩(追加新记录,保留历史)"""
return self.add_score(name, subject, new_score)
# ── 显示 ─────────────────────────────────────────────────
def print_student_report(self, name: str):
report = self.get_student_report(name)
if not report:
return
print(f"\n{'='*50}")
print(f" 学生报告:{report['name']}")
print(f"{'='*50}")
if not report['records']:
print(" 暂无成绩记录")
return
print(f" {'科目':<8} {'分数':>6} {'等级'}")
print(f" {'-'*30}")
for rec in report['records']:
print(f" {rec.subject:<8} {rec.score:>6.1f} {rec.grade}")
print(f" {'-'*30}")
print(f" 平均分: {report['avg']:.1f} 总分: {report['total']:.1f}"
f" 综合等级: {report['grade']}")
best, worst = report['best'], report['worst']
print(f" 最佳科目: {best.subject}({best.score}分)"
f" 最差科目: {worst.subject}({worst.score}分)")
def print_ranking(self, subject: Optional[str] = None):
title = f"{subject} 科目排名" if subject else "综合排名(按平均分)"
rankings = self.get_class_ranking(subject)
print(f"\n{'='*50}")
print(f" {title}")
print(f"{'='*50}")
print(f" {'排名':<4} {'姓名':<8} {'分数':>7}")
print(f" {'-'*30}")
for rank, (name, score, _) in enumerate(rankings, start=1):
medal = ('🥇','🥈','🥉')[rank-1] if rank <= 3 else f' {rank}.'
print(f" {medal} {name:<8} {score:>7.1f}")
def print_subject_stats(self, subject: str):
stats = self.get_subject_stats(subject)
if not stats:
print(f"暂无 {subject} 的成绩数据")
return
print(f"\n{'='*50}")
print(f" {subject} 科目统计")
print(f"{'='*50}")
print(f" 参考人数: {stats['count']} 平均分: {stats['avg']:.1f}")
print(f" 最高分: {stats['max']} 最低分: {stats['min']}"
f" 中位数: {stats['median']:.1f}")
print(f" 及格率: {stats['pass_rate']:.1f}%")
print(f" 等级分布: ", end="")
for g, cnt in stats['grade_counts'].items():
if cnt > 0:
print(f"{g}:{cnt}人", end=" ")
print()
# ── 导出 ─────────────────────────────────────────────────
def export_csv(self, filename: str = 'grades.csv'):
"""导出为 CSV 格式"""
lines = ['姓名,科目,分数,等级']
for student in self._students:
for rec in student['records']:
lines.append(f"{student['name']},{rec.subject},"
f"{rec.score},{rec.grade}")
with open(filename, 'w', encoding='utf-8-sig') as f:
f.write('\n'.join(lines))
print(f"[成功] 已导出 {len(lines)-1} 条记录到 {filename}")
# ── 私有方法 ─────────────────────────────────────────────
def _find(self, name: str) -> Optional[dict]:
"""内部查找学生(用推导式实现)"""
found = [s for s in self._students if s['name'] == name]
return found[0] if found else None
@property
def student_count(self) -> int:
return len(self._students)
# ── 主程序演示 ────────────────────────────────────────────────
def main():
print("=" * 60)
print(" 学生成绩管理系统 --- Python 系列第04篇 Demo")
print("=" * 60)
gb = GradeBook()
# 批量添加学生(用推导式 + 解包)
student_names = ['Alice', 'Bob', 'Carol', 'Dave', 'Eva']
for name in student_names:
gb.add_student(name)
print(f"\n已添加 {gb.student_count} 名学生\n")
# 添加成绩数据
score_data = [
# 姓名 科目 分数
('Alice', '数学', 92),
('Alice', '英语', 88),
('Alice', '物理', 95),
('Alice', '化学', 85),
('Bob', '数学', 78),
('Bob', '英语', 82),
('Bob', '物理', 70),
('Bob', '化学', 75),
('Carol', '数学', 65),
('Carol', '英语', 90),
('Carol', '物理', 58), # 不及格
('Carol', '化学', 72),
('Dave', '数学', 98),
('Dave', '英语', 74),
('Dave', '物理', 88),
('Dave', '化学', 91),
('Eva', '数学', 85),
('Eva', '英语', 96),
('Eva', '物理', 79),
('Eva', '化学', 83),
]
print("录入成绩...")
for name, subject, score in score_data:
gb.add_score(name, subject, score)
# 查看个人报告
gb.print_student_report('Alice')
gb.print_student_report('Carol')
# 班级排名
gb.print_ranking()
gb.print_ranking('数学')
# 科目统计
gb.print_subject_stats('数学')
gb.print_subject_stats('物理')
# 演示删除和更新
print("\n--- 演示成绩更新 ---")
gb.update_score('Carol', '物理', 68) # Carol 补考提升
gb.print_student_report('Carol')
# 导出 CSV
gb.export_csv('student_grades.csv')
# ── 高级用法展示 ─────────────────────────────────────────
print("\n--- 高级列表操作演示 ---")
# 用推导式生成分数矩阵
subjects = ['数学', '英语', '物理', '化学']
matrix = []
for student in student_names:
report = gb.get_student_report(student)
if report and report['records']:
row = [
next((r.score for r in report['records'] if r.subject == s), None)
for s in subjects
]
matrix.append((student, row))
print("\n成绩矩阵:")
print(f" {'姓名':<8}", end="")
for s in subjects:
print(f"{s:>8}", end="")
print()
for name, row in matrix:
print(f" {name:<8}", end="")
for score in row:
if score is not None:
print(f"{score:>8.0f}", end="")
else:
print(f"{'N/A':>8}", end="")
print()
# 序列解包综合演示
print("\n--- 序列解包演示 ---")
rankings = gb.get_class_ranking()
first, second, third, *others = rankings
name1, score1, _ = first
name2, score2, _ = second
name3, score3, _ = third
print(f"前三名: {name1}({score1:.1f}) > {name2}({score2:.1f}) > {name3}({score3:.1f})")
print(f"其余同学: {[n for n, _, _ in others]}")
if __name__ == '__main__':
main()
9. 总结与拓展
9.1 核心知识点回顾
| 知识点 | 关键结论 |
|---|---|
| 列表创建 | 字面量 / list() / 推导式 / *n 重复;二维列表必须用推导式 |
| 索引切片 | 支持负数下标;[start:stop:step];切片返回浅拷贝 |
| append vs extend | append 追加整体对象;extend 展开追加多元素 |
| sort vs sorted | sort() 原地修改返回 None;sorted() 返回新列表 |
| 列表推导式 | [expr for x in iter if cond];比 for 循环快,超复杂时降级 |
| 元组不可变 | 元组本身不可变,但内含可变对象时,对象内部可变 |
| 元组应用 | 多返回值 / dict key / 命名元组 / 常量配置 |
| 序列解包 | a, b = seq;first, *rest = seq;_, month, day = date |
| 列表 vs 元组 | 动态数据用 list;固定/结构化数据用 tuple;大量只读数据用 tuple 更省内存 |
9.2 常见误区总结
python
# 误区 1:[[0]*3]*3 陷阱(前文已讲,记住用推导式)
# 误区 2:在循环中修改正在遍历的列表
lst = [1, 2, 3, 4, 5]
# 错误
for item in lst:
if item % 2 == 0:
lst.remove(item) # 会跳过元素!
print(lst) # [1, 3, 5] ← 看似正确,但对复杂情况会出错
# 正确:用推导式创建新列表
lst = [1, 2, 3, 4, 5]
lst = [item for item in lst if item % 2 != 0]
print(lst) # [1, 3, 5]
# 误区 3:元组内的可变对象仍可被修改(前文已讲)
# 误区 4:sort() 返回 None
result = [3, 1, 2].sort() # None!常见新手错误
print(result) # None
# 正确
lst = [3, 1, 2]
lst.sort()
# 或
result = sorted([3, 1, 2])
# 误区 5:切片赋值的左右长度可以不同
lst = [1, 2, 3, 4, 5]
lst[1:3] = [10, 20, 30, 40] # 替换 2 个元素为 4 个,完全合法
print(lst) # [1, 10, 20, 30, 40, 4, 5]
9.3 拓展学习方向
collections.deque:双端队列,在两端操作都是 O(1),适合大量头部插入/删除array模块:同类型数值的紧凑数组,比 list 更省内存numpy.ndarray:科学计算的多维数组,支持向量化运算dataclasses:Python 3.7+ 的结构化数据类,结合了 dict 的灵活性和元组的语义清晰- 切片对象
slice():s = slice(1, 10, 2); lst[s],适合复用切片参数