实验 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 -v2026-09-28 | python | 45 min实验步骤
- 01
运行基线
解压完整课程包,在包含 lab01.py 的目录创建虚拟环境。执行本页运行命令,确认输出日期、项目和 45 min。
- 02
逐层阅读校验
依次定位 required、date.fromisoformat、strip 和 minutes 转换。尝试把示例的 minutes 改为 -1,观察 ValueError,再恢复示例。
- 03
完成独立练习
在本地草稿里重新实现 parse_row,再和完整实现对照。不要只改期望结果来让测试通过;解释为什么 20260928 和 1.5 都应该被拒绝。
先约定输入,再实现函数
输入字典的三个值都来自文本:date 使用 YYYY-MM-DD,project 去除首尾空格后不能为空,minutes 只接受大于零的十进制整数。把这份约定写进测试,函数就有可验证的边界。
解析与展示分离
parse_row 只返回 Session,失败时抛出 ValueError,不直接打印。后续 CSV 读取器和命令行可以复用它,并决定怎样向使用者解释错误。frozen=True 防止解析完成的记录被意外修改。
完整代码
以下为课程包中的实际源码。后续实验复用前序模块,首次运行请下载完整课程包。
"""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
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 说明每个新增边界。