【第 04 篇】列表与元组 —— 序列类型核心详解

系列导读 :本文是《从零到精通 Python》系列第 04 篇。前三篇打好了变量、运算符和流程控制的基础,本篇深入 Python 中最常用的两大序列类型:列表(list)元组(tuple)。从底层内存模型到高阶技巧,配合完整的学生成绩管理系统实战,帮你真正掌握序列操作的精髓。


目录

  1. 序列类型概览
  2. 列表:创建、索引与切片
  3. 列表常用方法详解
  4. 列表推导式与嵌套推导
  5. 元组:不可变性与应用场景
  6. 序列解包与星号表达式
  7. [列表 vs 元组:性能与选型对比](#列表 vs 元组:性能与选型对比)
  8. [实操 Demo:学生成绩管理系统](#实操 Demo:学生成绩管理系统)
  9. 总结与拓展

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:起始下标(包含),默认 0
  • stop:结束下标(不含),默认序列末尾
  • 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 = seqfirst, *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],适合复用切片参数

相关推荐
idingzhi1 小时前
A股量化策略日报(2026年06月07日)
python
米核AI易山1 小时前
扣子工作流错误处理:用条件分支打造不崩的自动化流水线
人工智能·深度学习·自动化·coze·扣子工作流·米核ai易山
继续商行1 小时前
Go/Rust 系统编程与并发原语深度剖析
人工智能
码语智行1 小时前
Codex 新手安装教程(完全小白版)
java·人工智能
xingpanvip1 小时前
使用 Webwright 在 CSDN 自动发文:Python 浏览器自动化实践
开发语言·python·自动化
平原20181 小时前
2026 主流 AI 视频 API 渠道价格对比:Seedance 2.0 哪家最便宜
大数据·人工智能
薛定猫AI1 小时前
【深度解析】从无状态 ChatBot 到有状态 AI Companion:大模型记忆系统原理与工程落地
大数据·人工智能·gpt
armwind1 小时前
openISP学习7-CCM — Color Correction Matrix(色彩校正矩阵)
python·学习·矩阵
艺杯羹1 小时前
零成本!3步设置Windows动态壁纸,免费无广告
python