跳到正文

实验 01 / 04 · 45 分钟

数据建模与输入校验

把一行原始记录变成有明确类型和约束的学习记录。

学习目标

  • 配置独立 Python 环境,运行第一个实验。
  • 用 dataclass 表达日期、项目和投入时间。
  • 为缺失字段和非法输入定义稳定的错误行为。

前置知识

  • 能使用变量、函数和字典。
  • 知道字符串与整数的区别。无需 CSV 或测试框架经验。

环境配置与运行

Python 3.11+ · 仅标准库 · 在本机终端运行

下载并解压课程包,打开其中的 python-engineering 目录。以下命令均在包含 lab01.py 的目录执行。首次学习时创建环境,后续实验复用同一个 .venv。

首次配置
py -3 --version
py -3 -m venv .venv
.\.venv\Scripts\python.exe --version
运行实验
.\.venv\Scripts\python.exe -X utf8 lab01.py
运行测试
.\.venv\Scripts\python.exe -X utf8 -m unittest test_lab01 -v
预期结果
2026-09-28 | python | 45 min

实验步骤

  1. 01

    运行基线

    解压完整课程包,在包含 lab01.py 的目录创建虚拟环境。执行本页运行命令,确认输出日期、项目和 45 min。

  2. 02

    逐层阅读校验

    依次定位 required、date.fromisoformat、strip 和 minutes 转换。尝试把示例的 minutes 改为 -1,观察 ValueError,再恢复示例。

  3. 03

    完成独立练习

    在本地草稿里重新实现 parse_row,再和完整实现对照。不要只改期望结果来让测试通过;解释为什么 20260928 和 1.5 都应该被拒绝。

先约定输入,再实现函数

输入字典的三个值都来自文本:date 使用 YYYY-MM-DD,project 去除首尾空格后不能为空,minutes 只接受大于零的十进制整数。把这份约定写进测试,函数就有可验证的边界。

解析与展示分离

parse_row 只返回 Session,失败时抛出 ValueError,不直接打印。后续 CSV 读取器和命令行可以复用它,并决定怎样向使用者解释错误。frozen=True 防止解析完成的记录被意外修改。

完整代码

以下为课程包中的实际源码。后续实验复用前序模块,首次运行请下载完整课程包。

lab01.py
"""Lab 01: validate one study session."""

from dataclasses import dataclass
from datetime import date


@dataclass(frozen=True)
class Session:
    day: date
    project: str
    minutes: int


def parse_row(row: dict[str, str]) -> Session:
    required = ("date", "project", "minutes")
    if any(not isinstance(row.get(key), str) for key in required):
        raise ValueError("required fields: date, project, minutes")

    day_text = row["date"].strip()
    try:
        day = date.fromisoformat(day_text)
    except ValueError:
        raise ValueError("date must be a valid YYYY-MM-DD date") from None
    if day.isoformat() != day_text:
        raise ValueError("date must use YYYY-MM-DD")

    project = row["project"].strip()
    if not project:
        raise ValueError("project must not be empty")

    minutes_text = row["minutes"].strip()
    if not minutes_text.isascii() or not minutes_text.isdecimal():
        raise ValueError("minutes must be a positive integer")
    minutes = int(minutes_text)
    if minutes <= 0:
        raise ValueError("minutes must be a positive integer")
    return Session(day, project, minutes)


if __name__ == "__main__":
    session = parse_row(
        {"date": "2026-09-28", "project": "python", "minutes": "45"}
    )
    print(f"{session.day} | {session.project} | {session.minutes} min")

测试用例

执行上面的测试命令,应看到 6 个测试通过和 OK。先理解断言,再尝试引入错误验证测试有效。

test_lab01.py
test_lab01.py
import unittest
from datetime import date

from lab01 import Session, parse_row


class ParseRowTests(unittest.TestCase):
    def test_valid_row(self):
        self.assertEqual(
            parse_row({"date": "2026-09-28", "project": "python", "minutes": "45"}),
            Session(date(2026, 9, 28), "python", 45),
        )

    def test_surrounding_whitespace(self):
        row = {"date": " 2026-09-28 ", "project": " python ", "minutes": " 15 "}
        self.assertEqual(parse_row(row).project, "python")
        self.assertEqual(parse_row(row).minutes, 15)

    def test_invalid_minutes(self):
        for value in ["0", "-1", "1.5", "", "abc", "+2", "1_000"]:
            with self.subTest(value=value), self.assertRaisesRegex(ValueError, "minutes"):
                parse_row({"date": "2026-09-28", "project": "python", "minutes": value})

    def test_missing_field(self):
        with self.assertRaisesRegex(ValueError, "required fields"):
            parse_row({"date": "2026-09-28", "project": "python"})

    def test_invalid_date(self):
        for value in ["2026-02-30", "20260928", "28/09/2026"]:
            with self.subTest(value=value), self.assertRaisesRegex(ValueError, "date"):
                parse_row({"date": value, "project": "python", "minutes": "30"})

    def test_empty_project(self):
        with self.assertRaisesRegex(ValueError, "project"):
            parse_row({"date": "2026-09-28", "project": "  ", "minutes": "30"})


if __name__ == "__main__":
    unittest.main()

常见错误

python 或 py 命令不存在

安装 Python 3.11+ 后重新打开终端。Windows 用 py -3,macOS / Linux 用 python3;先检查版本。

ModuleNotFoundError: lab01

进入包含全部实验文件的 python-engineering 目录,再执行测试命令。不要在仓库根目录运行单模块测试。

ValueError: minutes must be a positive integer

检查是否输入了 0、负数、小数或文字。这个错误代表约定正在生效,不应把异常全部吞掉。

验收清单

自评记录仅保存在当前浏览器,不代表自动测试结果或 Issue 认领状态。

从实验到贡献

为输入校验补充边界测试

这是完成基线后的扩展任务。先查看 Issue 当前状态和留言,与维护者确认认领范围。

  • 新增有效闰日与无效闰日测试。
  • 新增非 ASCII 数字输入测试,保持当前输入约定。
  • 原有测试全部通过,PR 说明每个新增边界。
查看并认领 Issue #1