案例:表格合并 List Merge

(date: 2025-11-18)

学习建议:建议先完成 dict-tutorial-1.md 的基础学习,再学习本案例。

提出问题,需求分析,技术方案,掌握工具,实践验证——五大环节,完成一个案例。

学习目标

  • 使用二维 List 解决 Table 类型数据的合并问题

  • 练习解包, zip 等操作

  • 体会 List 的能力 (优点与缺点),为进一步学习 Dict 作准备

问题与分析

原数据

# 语文老师提供
chinese_scores = [["张三", "S001", 85], ["李四", "S002", 92], ["王五", "S003", 78]]

# 数学老师提供
math_scores = [["张三", "S001", 90], ["李四", "S002", 87], ["王五", "S003", 85]]

# 英语老师提供
english_scores = [["S001", "张三", 88], ["S002", "李四", 85], ["S003", "王五", 80]]

期望的结果

期望得到一个合并后的成绩表,包含每个学生的所有科目成绩:

[
    ["学号", "姓名", "语文", "数学", "英语"],
    ["S001", "张三", 85, 90, 88],
    ["S002", "李四", 92, 87, 85],
    ["S003", "王五", 78, 85, 80]
]

初步思路

先按行打印数组,看看怎么做?

>>> for student in chinese_scores:
...     print(student)
... 

# 语文
# ['张三', 'S001', 85]
# ['李四', 'S002', 92]
# ['王五', 'S003', 78]

练习要求: 继续完成数学和英语科目的表格打印

数学

['张三', 'S001', 90]
['李四', 'S002', 87]
['王五', 'S003', 85]

英语

['S001', '张三', 88]
['S002', '李四', 85]
['S003', '王五', 80]

结论: 先把表格中的字段按相同的顺序排列,补齐可能的缺失值,然后合并。

需求分析

用表格法 (二维 List) 方法来合并成绩,怎样解决问题?

  1. 合并前字段要统一排序,不能有乱序

  2. 不能有成员缺失的情况,否则无法合并 (暂时不存在)

  3. 选择合并的主键必须是唯一的,否则合并结果会出错(姓名是不合适的,学号是合适的)

  4. 使用下标索引访问数据

调整数据的顺序

阶段性期望结果

首先需要统一所有数据表的字段顺序,这里我们选择以学号为主键, 希望得到下面的效果:

# 统一为 [学号, 姓名, 成绩] 的顺序
chinese_scores = [["S001", "张三", 85], ["S002", "李四", 92], ["S003", "王五", 78]]
math_scores = [["S001", "张三", 90], ["S002", "李四", 87], ["S003", "王五", 85]]
english_scores = [["S001", "张三", 88], ["S002", "李四", 85], ["S003", "王五", 80]]

核心观察

  1. 对于语文和数学,记录顺序为:

name, sid, score = record
  1. 对于英语科目,记录顺序为:

sid, name, score = record
  1. 英语不变,但语文和数学两科要交换 sid, name 的顺序

ordered_record = sid, name, score

一次性把语文的顺序调整了:

for record in chinese_scores:
    name, sid, score = record
    ordered_record = sid, name, score

采用解包的写法更简洁:

for name, sid, score in chinese_scores:
    new_record = sid, name, score

练习: 用解包的方法调整数学的顺序

调序函数

将以上重排序写成函数,方便将相同的逻辑应用到不同的数据 (数学):

def rearrange(scores):
    ordered_records = []
	for name, sid, score in scores:
    	ordered_records.append((sid, name, score))
    	
rearrange(chinese_scores)

练习: 采用函数调整数学科目的顺序

rearrange(math_scores)

整合数据

输入为整齐的三个数据表

# 语文
['S001', '张三', 85]
['S002', '李四', 92]
['S003', '王五', 78]

# 数学
['S001', '张三', 90]
['S002', '李四', 87]
['S003', '王五', 85]

# 英语
['S001', '张三', 88]
['S002', '李四', 85]
['S003', '王五', 80]

如何合并?

取语文表的第1,2,3列,数学的第3列,英语的第3列,拼合。下面涉及到3种不同的思路和方法。

方法1 行列转换法

从 Excel 的解法类比入手(图示)

(1) 整体取其中某1列(行列转换)

def select_column(table, index):
    colmun = []
    for row in table:
        column.append(row[index])
    return column

用 List comprehension 改写:

def select_column(table, index):
    return [row[index] for row in table]

(2) 拼合

column1 = select_column(chinese_scores, 0)
column2 = select_column(chinese_scores, 1)
column3 = select_column(chinese_scores, 2)
column4 = select_column(math_scores, 2)
column5 = select_column(english_scores, 2)

columns = [column1, column2, column3, column4, column5]

# [学号行, 姓名行, 分数1行, 分数2行]

(3) 行列转换,二层 for 循环,把第i行,第j列取出来,重新组合

table = [[None] * len(columns) for _ in range(len(columns[0]))]  # 3个人,每人5个记录
for i in range(len(columns)):      # 5列
    column = columns[i]
    for j in range(len(column)):   # 3行
        table[j][i] = column[j]

print("使用循环转换的结果:")
for record in table:
    print(record)

练习:将解法1的步骤 (1), (2), (3) 所述思路整合成可以运行的正确代码

(4) 优化: 使用 zip() 实现行列转换

table = zip(*columns)
for record in table:
    print(record)        

总结: 从excel 表格处理的角度讲,这个解法最直观,但最后一步需要对结果进行行列转换,对有些人来讲则最难理解。

解法2 查找写入法

为了避免行列转换,可采起查找写入法,其核心是: 主表要实现向某个指定学号第 i 个位置写入 score

# 向某个学号写入第 i 个成绩
append_score_to_table(merged_tables, sid, score)

然后,顺序将语文、数学、英语分数表中的每个成绩按 sid 插入

# 初始化:用语文表创建基础结构
merged_table = []
for name, sid, score in chinese_scores:
    merged_table.append([sid, name, score])  # [学号, 姓名, 语文]

for sid, name, score in math_scores:
    append_score_to_table(merged_table, sid, score)
    
for sid, name, score in english_scores:
    append_score_to_table(merged_table, sid, score)

提示: 可以对上面重复的代码重构

定义和实现核心 api: append_score_to_table(merged_table, sid, score)

# 行索引表
# s001 -> 0
# s002 -> 1
# s003 -> 2

def append_score_to_table(merged_table, sid, score):
    for i, record in enumerate(merged_table):
        if sid in record:
            merged_table[i].append(score)

练习:将解法2的思路整合编写成可以运行的正确代码,保持思路不变

解法3 简洁但抽象(选学)

把数据弄整齐需要的工具: 解包

# 使用解包重新排列字段顺序
def rearrange_scores(scores_list, order_type):
    """根据类型重新排列字段顺序"""
    rearranged = []
    for item in scores_list:
        if order_type == "name_first":  # [姓名, 学号, 成绩]
            name, sid, score = item
            rearranged.append([sid, name, score])
        elif order_type == "sid_first":  # [学号, 姓名, 成绩]
            sid, name, score = item
            rearranged.append([sid, name, score])
    return rearranged

# 重新排列数据
chinese_rearranged = rearrange_scores(chinese_scores, "name_first")
math_rearranged = rearrange_scores(math_scores, "name_first")
english_rearranged = rearrange_scores(english_scores, "sid_first")

将数据整合的工具: for in 循环

# 使用循环合并数据
def merge_scores(chinese_list, math_list, english_list):
    """合并多个成绩列表"""
    merged = []
    # 添加表头
    merged.append(["学号", "姓名", "语文", "数学", "英语"])
    
    # 假设三个列表的顺序是一致的,按顺序遍历
    for i in range(len(chinese_list)):
        chinese_item = chinese_list[i]
        math_item = math_list[i]
        english_item = english_list[i]
        
        sid, name, chinese_score = chinese_item
        _, _, math_score = math_item
        _, _, english_score = english_item
        
        # 验证学号和姓名是否一致
        if (chinese_item[0] == math_item[0] == english_item[0] and
            chinese_item[1] == math_item[1] == english_item[1]):
            merged.append([sid, name, chinese_score, math_score, english_score])
        else:
            print(f"数据不一致: {chinese_item} vs {math_item} vs {english_item}")
    
    return merged

# 合并数据
final_result = merge_scores(chinese_rearranged, math_rearranged, english_rearranged)
print(final_result)

进阶:使用 zip 的简化版本(选学)

如果你已经掌握了基础循环,可以学习使用 zip 函数来简化代码:

# 使用 zip 合并数据(进阶版本)
def merge_scores_with_zip(chinese_list, math_list, english_list):
    """使用 zip 合并多个成绩列表"""
    merged = []
    # 添加表头
    merged.append(["学号", "姓名", "语文", "数学", "英语"])
    
    # 使用 zip 并行遍历所有列表
    for chinese_item, math_item, english_item in zip(chinese_list, math_list, english_list):
        sid, name, chinese_score = chinese_item
        _, _, math_score = math_item
        _, _, english_score = english_item
        
        # 验证学号和姓名是否一致
        if (chinese_item[0] == math_item[0] == english_item[0] and
            chinese_item[1] == math_item[1] == english_item[1]):
            merged.append([sid, name, chinese_score, math_score, english_score])
        else:
            print(f"数据不一致: {chinese_item} vs {math_item} vs {english_item}")
    
    return merged

# 使用 zip 版本合并数据
final_result_zip = merge_scores_with_zip(chinese_rearranged, math_rearranged, english_rearranged)
print("使用 zip 版本的结果:")
print(final_result_zip)

zip 函数说明

  • zip 函数可以将多个列表"打包"在一起,同时遍历

  • 比如 zip([1,2,3], ['a','b','c']) 会生成 (1,'a'), (2,'b'), (3,'c')

  • 这样可以在一个循环中同时处理多个列表的对应元素