P03第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

函数与模块

函数与模块 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这一课把 P02 那种「一大段脚本」拆成函数和模块:解析、校验、统计各管各的,每个函数都能单独调用验证,并且导入模块时不会带出一堆副作用。

问题场景

P02 的清洗统计写在一个文件里还好,现在加需求:既要支持本站导出的 JSON 记录,又要支持手写的 CSV 记录,还得在数据非法时给出提示。全塞一个函数,改一处崩三处,想测「只测统计」都做不到——因为解析代码就长在统计旁边。

前端的直觉在这里照样适用:一个组件只做一件事,工具函数抽出去,模块按职责分文件。

核心解释

拆的标准不是行数,是每个函数一条能一句话说清的职责,并且输入输出可预期:

# parsing.py —— 只负责把原始数据变成标准记录
def parse_record(raw: dict) -> dict:
    """把任意来源的一条记录归一化,缺字段显式报错。"""
    course = raw["course"]
    minutes = raw.get("minutes")
    return {"course": course.upper(), "minutes": minutes or 0, "done": bool(raw.get("done"))}
# stats.py —— 只负责统计,输入必须是已清洗数据
def count_by_course(records: list) -> dict:
    counts: dict = {}
    for r in records:
        counts[r["course"]] = counts.get(r["course"], 0) + 1
    return counts

模块级的两条纪律比拆函数本身更重要:

  1. 入口逻辑必须放在 if __name__ == "__main__": 守卫里。模块被 import 时整个文件会从头执行一遍——不守卫,import stats 就会触发统计和打印。FastAPI 项目里这条同样成立:模块导入时应该只定义路由和函数,不连数据库、不读文件。
  2. 依赖方向要单向。main.py 导入 parsing 和 stats,后两者不回头 import main。一旦形成循环导入,说明职责切分错了。

用法上看效果:

# main.py
from parsing import parse_record
from stats import count_by_course

RAW = [{"course": "p07", "minutes": 25}]

if __name__ == "__main__":
    clean = [parse_record(r) for r in RAW]
    print(count_by_course(clean))    # {'P07': 1}

示例拆解

验证「可测试」和「无副作用」两件事:

python main.py                 # {'P07': 1},入口逻辑只在直接运行时执行
python -c "import parsing; print(parsing.parse_record({'course':'x','minutes':None}))"
# {'course': 'X', 'minutes': 0, 'done': False}——单函数验证通过

第二条命令就是最朴素的「单元测试」:不跑主流程,只调用一个函数对照输出。拆完之后,parse_record 负责归一化、count_by_course 负责统计、main 负责组装,三个文件的职责边界一眼可见。到 P19 你会用 APIRouter 把这个思路搬到接口层:路由文件管 HTTP,业务模块管逻辑,结构完全同构。

动手任务

  1. 把 P02 的 total_minutes 挪进 stats.py,在 main.py 里导入使用,确认运行结果不变。
  2. 给 parse_record 补一条校验:course 缺失时抛 ValueError("记录缺少 course"),并写两行调用验证正常/异常两种输入。
  3. 故意在 stats.py 顶层加一行 print("stats 被导入"),观察 python main.py 输出几次,再把它移进函数体,理解「导入即执行」。

常见错误

  • import 后行为怪异:模块顶层有读文件、连库、input 等语句,导入即执行;全部挪进函数或 __main__ 守卫。
  • 一个函数既解析又统计又打印:职责混在一起没法单独测;打印交给调用方或 main。
  • 文件名和标准库冲突:给自己的模块起名 json.py、stats 撞上第三方包,导入顺序一变就出错;模块名避开常见库。
  • 循环导入:A import B、B import A,一跑就 ImportError;把共享的东西抽到第三个模块。

检查与复盘

自查清单:

  • 每个函数能用一句话说清职责,且能脱离主程序单独调用
  • python main.py 和 import stats 的行为差异说得清
  • 新增功能时先想清楚它该落在哪个文件,而不是顺手塞 main

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码中等

阅读代码(Python 3.11+,独立脚本可直接运行)。连续三次调用,输出什么?

def add_task(task, tasks=[]):
    tasks.append(task)
    return tasks

print(add_task('a'))
print(add_task('b'))
print(add_task('c'))
Q2单选简单

项目里 utils.py 定义了 format_price(p)(把 12.5 变成 '¥12.50'),main.py 与它同目录。下列导入写法正确的是?

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...