跳转至

Day 2 练习骨架(只读展示)

怎么用

这里只是为了方便在手机/平板上看题。实际编码请在 VS Code 里打开 exercises/day2/ 下的对应文件,把 TODO 换成你的实现,然后运行:

uv run python exercises/day2/文件名.py(pytest 项目用 uv run pytest exercises/day2/目录名 -q

任务规格、测试用例与陷阱反例见对应的计划页

01_exceptions.py

exercises/day2/01_exceptions.py
"""Day 2 · Block 1 · 异常处理

学习目标:用 try/except 具体异常/else/finally 精确处理错误,用 raise 主动报错;
      把"可能失败的操作"包成返回 None 或默认值的安全函数,并写输入校验循环;
      知道裸 except 会连 Ctrl+C 和拼写错误一起吞掉,所以永远写具体异常。
用法:uv run python exercises/day2/01_exceptions.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK]。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

预测练习(先在纸上写出输出,再到 REPL 验证):
>>> def f(x):
>>>     try:
>>>         print("try")
>>>         r = 10 / x
>>>     except ZeroDivisionError as e:
>>>         print("except", e)
>>>         return "错误"
>>>     else:
>>>         print("else")
>>>         return r
>>>     finally:
>>>         print("finally")
>>> print(f(2))
>>> print(f(0))
>>> # int("abc") → ValueError    "3" + 1 → TypeError
>>> # [1][5] → IndexError    {}["k"] → KeyError
"""

from collections.abc import Callable


# ---- 任务 4:这个自定义异常已经写好了,后面的 validate_score 要用它 ----
class InvalidScoreError(ValueError):
    """分数不合法时抛出的自定义异常。

    继承 ValueError,所以 `except ValueError:` 也能捕获它。
    例:raise InvalidScoreError("分数 101 不在 0-100")
    """


# ---- 任务 1 ----
def parse_int(s: str) -> int | None:
    """把字符串转成整数;转换失败返回 None。

    例:parse_int(" 42 ") -> 42;parse_int("abc") -> None
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 2:make_fake_input 已经写好了,read_positive_int 要你写 ----
def make_fake_input(answers: list[str]) -> Callable[[str], str]:
    """做一个假的 input 函数:每次调用按顺序返回 answers 里的下一个答案。

    有了它,测试就不需要真的用键盘输入了。
    例:fake = make_fake_input(["abc", "5"]);fake("请输入:") -> "abc"
    """
    remaining = list(answers)

    def fake_input(prompt: str = "") -> str:
        return remaining.pop(0)

    return fake_input


def read_positive_int(prompt: str, input_func: Callable[[str], str] = input) -> int:
    """反复调用 input_func(prompt),直到拿到一个正整数为止。

    不是整数或不大于 0 时,先 print 一句提示,再继续问。
    例:read_positive_int("n=", make_fake_input(["abc", "-1", "5"])) -> 5
    """
    # TODO: 在这里实现(提示:while True + parse_int)
    raise NotImplementedError


# ---- 任务 3 ----
def safe_get(lst: list, index: int, default=None):
    """取 lst[index];下标越界(IndexError)时返回 default。

    注意负数下标是合法的:safe_get([1, 2], -1) 取到的是最后一个元素。
    例:safe_get([1, 2], 5, -1) -> -1;safe_get([1, 2], 1) -> 2
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 5 ----
def validate_score(score: int) -> int:
    """校验分数必须在 0-100 之间;合法则原样返回,不合法则主动报错。

    不合法时:raise InvalidScoreError(f"分数 {score} 不在 0-100")
    例:validate_score(88) -> 88;validate_score(101) 抛 InvalidScoreError
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 6 ----
def divide_all(pairs: list[tuple[float, float]]) -> list[float | None]:
    """逐对相除;除零的位置放 None,不让一个坏数据中断整批计算。

    例:divide_all([(6, 3), (1, 0), (5, 2)]) -> [2.0, None, 2.5]
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 7 ----
def get_config_int(config: dict, key: str) -> int | None:
    """从配置字典里读一个整数;键不存在或值转不成 int 时返回 None。

    必须写成一次捕获两种异常:except (KeyError, ValueError):
    例:get_config_int({"n": "7"}, "n") -> 7;get_config_int({}, "n") -> None
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 8 ----
def withdraw(balance: float, amount: float) -> float:
    """从余额里取钱,返回取款后的余额;金额不合法时 raise ValueError。

    amount <= 0 → ValueError("金额必须为正");amount > balance → ValueError("余额不足")
    例:withdraw(100, 30) -> 70
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ====================== 自测:不要改动下面的代码 ======================


def _check_parse_int() -> None:
    assert parse_int(" 42 ") == 42, "parse_int(' 42 ') 应为 42"
    assert parse_int("abc") is None, "parse_int('abc') 应为 None"
    assert parse_int("3.5") is None, "parse_int('3.5') 应为 None(int 不认小数点)"


def _check_read_positive_int() -> None:
    fake = make_fake_input(["abc", "-1", "5"])
    got = read_positive_int("请输入一个正整数:", fake)
    assert got == 5, f"跳过 'abc' 和 '-1' 后应返回 5,实际 {got}"


def _check_safe_get() -> None:
    assert safe_get([1, 2], 5, -1) == -1, "下标 5 越界,应返回 default -1"
    assert safe_get([1, 2], 1) == 2, "safe_get([1, 2], 1) 应为 2"
    assert safe_get([1, 2], -1) == 2, "下标 -1 是合法的,应为最后一个元素 2"
    assert safe_get([], 0) is None, "default 默认是 None"


def _check_validate_score() -> None:
    assert issubclass(InvalidScoreError, ValueError), (
        "InvalidScoreError 应继承 ValueError"
    )
    assert validate_score(88) == 88, "88 在 0-100 内,应原样返回"
    assert validate_score(0) == 0, "0 是合法分数(边界值)"
    assert validate_score(100) == 100, "100 是合法分数(边界值)"
    try:
        validate_score(101)
    except InvalidScoreError as e:
        assert "101" in str(e), f"错误消息里应包含分数 101,实际 {str(e)!r}"
    else:
        raise AssertionError("应抛出 InvalidScoreError")
    try:
        validate_score(-1)
    except ValueError:
        pass
    else:
        raise AssertionError("应抛出 ValueError(自定义异常也能被它捕获)")


def _check_divide_all() -> None:
    got = divide_all([(6, 3), (1, 0), (5, 2)])
    assert got == [2.0, None, 2.5], f"应为 [2.0, None, 2.5],实际 {got}"
    assert divide_all([]) == [], "空列表应返回空列表"


def _check_get_config_int() -> None:
    assert get_config_int({"n": "7"}, "n") == 7, "字符串 '7' 应转成 7"
    assert get_config_int({"n": "x"}, "n") is None, "'x' 转不成 int,应为 None"
    assert get_config_int({}, "n") is None, "键不存在,应为 None"
    assert get_config_int({"n": 9}, "n") == 9, "值本来就是 int 也要能处理"


def _check_withdraw() -> None:
    assert withdraw(100, 30) == 70, "100 取 30 应剩 70"
    try:
        withdraw(100, 0)
    except ValueError as e:
        assert str(e) == "金额必须为正", f"消息应为 '金额必须为正',实际 {str(e)!r}"
    else:
        raise AssertionError("应抛出 ValueError")
    try:
        withdraw(100, 200)
    except ValueError as e:
        assert str(e) == "余额不足", f"消息应为 '余额不足',实际 {str(e)!r}"
    else:
        raise AssertionError("应抛出 ValueError")


CHECKS = [
    _check_parse_int,
    _check_read_positive_int,
    _check_safe_get,
    _check_validate_score,
    _check_divide_all,
    _check_get_config_int,
    _check_withdraw,
]


def _run_checks() -> None:
    passed = 0
    for check in CHECKS:
        name = check.__name__.removeprefix("_check_")
        try:
            check()
        except NotImplementedError:
            print(f"[TODO] {name}: 还没实现")
        except AssertionError as e:
            print(f"[FAIL] {name}: {e}")
        except Exception as e:
            print(f"[ERROR] {name}: {type(e).__name__}: {e}")
        else:
            passed += 1
            print(f"[OK]   {name}")
    total = len(CHECKS)
    tail = ",本文件全部完成!" if passed == total else ""
    print(f"\n{passed}/{total} 通过{tail}")


if __name__ == "__main__":
    _run_checks()

02_files_paths.py

exercises/day2/02_files_paths.py
"""Day 2 · Block 2a + 2b · 文件、路径、JSON、CSV

学习目标:用 with open(..., encoding="utf-8") 和 pathlib.Path 读写文本文件;
      亲手复现 Windows 默认 GBK 造成的 UnicodeDecodeError 并修好它;
      用 json(ensure_ascii=False)存档、用 csv.DictReader/DictWriter 读写表格。
用法:uv run python exercises/day2/02_files_paths.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK]。2a 做前 6 题,2b 做后 4 题。
      下面只写了自测用得到的 import;csv 模块要你自己加到文件顶部。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。
      所有自测都在系统临时目录里进行,不会在仓库里留下任何文件。

预测练习(先在纸上写出输出,再到 REPL 验证):
>>> from pathlib import Path
>>> p = Path("exercises/day2/data/hello.txt")
>>> print(p.read_text(encoding="utf-8"))   # 正常
>>> print(p.read_text(encoding="gbk"))     # UnicodeDecodeError(或乱码)
>>> print(open(p).read())                  # 中文 Windows 上默认 gbk → 同样出错
"""

import json
import tempfile
from pathlib import Path


# ---- 任务 1 ----
def write_lines(path: Path, lines: list[str]) -> None:
    """用 UTF-8 把每行写进文件,每行末尾补一个换行符;父目录不存在就创建。

    例:write_lines(folder / "sub" / "a.txt", ["a", "你好"]) 会先建好 sub 目录
    """
    # TODO: 在这里实现(提示:path.parent.mkdir(parents=True, exist_ok=True))
    raise NotImplementedError


# ---- 任务 2 ----
def read_lines(path: Path) -> list[str]:
    """用 UTF-8 读取文件的每一行,去掉行尾换行符;文件不存在返回空列表。

    例:内容是两行 "a" 和 "你好" 的文件 -> ["a", "你好"]
    """
    # TODO: 在这里实现(提示:splitlines() + except FileNotFoundError)
    raise NotImplementedError


# ---- 任务 3 ----
def count_words(path: Path) -> int:
    """统计文件里用空白(空格、换行)分隔的词数。

    例:内容是 "a b" 和 "c d e" 两行的文件 -> 5
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 4 ----
def append_log(path: Path, message: str) -> None:
    """以追加模式("a")在文件末尾写一行日志,不覆盖原有内容。

    例:连续 append_log(p, "第一条") 和 append_log(p, "第二条") 后文件有两行
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 5 ----
def ensure_dir(path: Path) -> Path:
    """确保目录存在(连中间层一起创建),已存在也不报错;返回这个路径。

    例:ensure_dir(folder / "a" / "b") -> folder/a/b,且重复调用不会出错
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 6 ----
def demo_gbk_pitfall(folder: Path) -> str:
    """故意踩一次编码坑:UTF-8 写入中文,再用 encoding="gbk" 读回来。

    规则:读取时抛 UnicodeDecodeError -> 返回
        "UnicodeDecodeError: 请显式指定 encoding='utf-8'";
    没抛异常但读出的内容和原文不一样(乱码)也算踩中,返回的字符串同样以
        "UnicodeDecodeError" 开头;两种情况都没有才返回 "未触发"。
    例:demo_gbk_pitfall(folder) -> "UnicodeDecodeError: 请显式指定 ..."
    """
    # TODO: 在这里实现(写 "你好,世界",再 try: read_text(encoding="gbk"))
    raise NotImplementedError


# ---- 任务 7 ----
def save_json(path: Path, data) -> None:
    """把 data 存成 JSON 文件:UTF-8、缩进 2 空格、中文不转成 \\uXXXX。

    例:save_json(p, {"名字": "张三"}) 后文件里能直接看到 "张三"
    """
    # TODO: 在这里实现(提示:json.dump(..., ensure_ascii=False, indent=2))
    raise NotImplementedError


# ---- 任务 8 ----
def load_json(path: Path, default):
    """读回 JSON 文件;文件不存在或内容不是合法 JSON 时返回 default。

    例:load_json(不存在的路径, []) -> [];内容为 "{bad" 时也返回 default
    """
    # TODO: 在这里实现(提示:except (FileNotFoundError, json.JSONDecodeError))
    raise NotImplementedError


# ---- 任务 9 ----
def list_files_by_suffix(folder: Path, suffix: str) -> list[str]:
    """列出目录下所有指定后缀的文件名(只要名字,不要路径),按字母排序。

    例:目录里有 a.txt、b.txt、c.md -> list_files_by_suffix(f, ".txt")
        得到 ["a.txt", "b.txt"]
    """
    # TODO: 在这里实现(提示:folder.glob(f"*{suffix}") 和 p.name)
    raise NotImplementedError


# ---- 任务 10 ----
def read_csv_rows(path: Path) -> list[dict[str, str]]:
    """用 csv.DictReader 读 CSV,返回每行一个字典(值都是字符串)。

    例:表头 name,score 的两行文件 -> [{"name": "张三", "score": "90"}, ...]
    """
    # TODO: 在这里实现(import csv,记得 newline="" 和 encoding="utf-8")
    raise NotImplementedError


def write_csv_rows(path: Path, rows: list[dict]) -> None:
    """用 csv.DictWriter 写 CSV,表头取第一行字典的键;rows 为空则只建空文件。

    Windows 上必须写 newline="",否则每行之间会多出一个空行。
    例:write_csv_rows(p, [{"name": "张三", "score": "90"}])
    """
    # TODO: 在这里实现(提示:writeheader() 然后 writerows(rows))
    raise NotImplementedError


# ====================== 自测:不要改动下面的代码 ======================


def _check_write_lines() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        folder = Path(tmp)
        target = folder / "sub" / "out.txt"
        write_lines(target, ["a", "你好"])
        assert target.exists(), "父目录不存在时也要能写成功(要先 mkdir)"
        text = target.read_text(encoding="utf-8")
        assert text == "a\n你好\n", f"内容应为 'a\\n你好\\n',实际 {text!r}"


def _check_read_lines() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        folder = Path(tmp)
        path = folder / "in.txt"
        path.write_text("a\n你好\n", encoding="utf-8")
        got = read_lines(path)
        assert got == ["a", "你好"], f"应为 ['a', '你好'],实际 {got}"
        missing = read_lines(folder / "不存在.txt")
        assert missing == [], f"文件不存在应返回 [],实际 {missing}"


def _check_count_words() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        folder = Path(tmp)
        path = folder / "words.txt"
        path.write_text("a b\nc d e\n", encoding="utf-8")
        assert count_words(path) == 5, "'a b' + 'c d e' 一共 5 个词"
        empty = folder / "empty.txt"
        empty.write_text("", encoding="utf-8")
        assert count_words(empty) == 0, "空文件应为 0 个词"


def _check_append_log() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        path = Path(tmp) / "log.txt"
        append_log(path, "第一条")
        append_log(path, "第二条")
        lines = path.read_text(encoding="utf-8").splitlines()
        assert lines == ["第一条", "第二条"], f"应为两行日志,实际 {lines}"


def _check_ensure_dir() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        target = Path(tmp) / "a" / "b"
        got = ensure_dir(target)
        assert got == target, "应返回传入的路径"
        assert target.is_dir(), "目录应被创建(parents=True)"
        ensure_dir(target)
        assert target.is_dir(), "第二次调用不能报错(exist_ok=True)"


def _check_demo_gbk_pitfall() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        got = demo_gbk_pitfall(Path(tmp))
        assert isinstance(got, str), "应返回一个字符串"
        assert got.startswith("UnicodeDecodeError"), (
            f"应踩中编码坑并返回以 UnicodeDecodeError 开头的说明,实际 {got!r}"
        )


def _check_save_json() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        path = Path(tmp) / "data.json"
        data = {"名字": "张三", "分数": [90, 80]}
        save_json(path, data)
        text = path.read_text(encoding="utf-8")
        assert '"张三"' in text, "文件里应直接看到 张三(要 ensure_ascii=False)"
        assert "\\u5f20" not in text, "不应写成 \\uXXXX 转义"
        assert json.loads(text) == data, "写出的内容应能原样读回"


def _check_load_json() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        folder = Path(tmp)
        path = folder / "data.json"
        path.write_text('{"名字": "张三", "分数": [90, 80]}', encoding="utf-8")
        got = load_json(path, None)
        assert got == {"名字": "张三", "分数": [90, 80]}, f"读回的字典不对:{got}"
        assert load_json(folder / "无.json", "默认") == "默认", (
            "文件不存在应返回 default"
        )
        bad = folder / "bad.json"
        bad.write_text("{bad", encoding="utf-8")
        assert load_json(bad, {}) == {}, "内容不是合法 JSON 时应返回 default"


def _check_list_files_by_suffix() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        folder = Path(tmp)
        for name in ("b.txt", "a.txt", "c.md"):
            (folder / name).write_text("x", encoding="utf-8")
        got = list_files_by_suffix(folder, ".txt")
        assert got == ["a.txt", "b.txt"], f"应为 ['a.txt', 'b.txt'],实际 {got}"
        assert list_files_by_suffix(folder, ".md") == ["c.md"], ".md 只有 c.md"
        assert list_files_by_suffix(folder, ".csv") == [], "没有 .csv 应返回 []"


def _check_csv_rows() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        path = Path(tmp) / "scores.csv"
        rows = [
            {"name": "张三", "score": "90"},
            {"name": "李四", "score": "80"},
        ]
        original = [dict(r) for r in rows]
        write_csv_rows(path, rows)
        assert rows == original, "write_csv_rows 不应修改传进来的 rows"
        got = read_csv_rows(path)
        assert got == rows, f"读回的内容应和写入的一致,实际 {got}"
        line_count = len(path.read_text(encoding="utf-8").splitlines())
        assert line_count == 3, (
            f"表头 + 2 行数据 = 3 行(没写 newline='' 会多空行),实际 {line_count}"
        )


CHECKS = [
    _check_write_lines,
    _check_read_lines,
    _check_count_words,
    _check_append_log,
    _check_ensure_dir,
    _check_demo_gbk_pitfall,
    _check_save_json,
    _check_load_json,
    _check_list_files_by_suffix,
    _check_csv_rows,
]


def _run_checks() -> None:
    passed = 0
    for check in CHECKS:
        name = check.__name__.removeprefix("_check_")
        try:
            check()
        except NotImplementedError:
            print(f"[TODO] {name}: 还没实现")
        except AssertionError as e:
            print(f"[FAIL] {name}: {e}")
        except Exception as e:
            print(f"[ERROR] {name}: {type(e).__name__}: {e}")
        else:
            passed += 1
            print(f"[OK]   {name}")
    total = len(CHECKS)
    tail = ",本文件全部完成!" if passed == total else ""
    print(f"\n{passed}/{total} 通过{tail}")


if __name__ == "__main__":
    _run_checks()

03_modules_stdlib.py

exercises/day2/03_modules_stdlib.py
"""Day 2 · Block 3 · 模块与标准库

学习目标:import 自己写的 shapes 模块,也会用 random/math/datetime/collections;
      用 random.Random(seed) 得到可复现的"随机",用 Counter/defaultdict 统计分组;
      记住陷阱:练习文件千万别起名 random.py,否则 import random 会导入自己。
用法:uv run python exercises/day2/03_modules_stdlib.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK]。
      本文件开头 import 了同目录的 shapes.py,所以要先把 shapes.py 写完,
      最后那项 shapes_module 才会变成 [OK]。
      下面只写了自测用得到的 import;random、statistics、collections
      这几个模块要你自己按需加到文件顶部(这也是本块的练习内容)。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

预测练习(先在纸上写出输出,再到 REPL 验证):
>>> import random
>>> print(random.Random(42).randint(1, 6) == random.Random(42).randint(1, 6))
>>> from collections import Counter
>>> print(Counter("abracadabra".split()).most_common(1))
>>> from datetime import date
>>> print(date(2026, 9, 5) - date(2026, 9, 1))
"""

import math
from datetime import date

from shapes import circle_area, rectangle_area, triangle_area


# ---- 任务 1 ----
def roll_dice(n: int, sides: int = 6, seed: int | None = None) -> list[int]:
    """掷 n 次 sides 面骰子,返回结果列表;传同一个 seed 两次结果完全相同。

    用 random.Random(seed) 造一个独立的随机数发生器,不影响全局 random。
    例:roll_dice(3, seed=42) -> 每次运行都是同样的三个数
    """
    # TODO: 在这里实现(import random,rng = random.Random(seed),rng.randint)
    raise NotImplementedError


# ---- 任务 2 ----
def most_common_words(text: str, n: int) -> list[tuple[str, int]]:
    """统计出现次数最多的 n 个词(先转小写,按空白切分)。

    例:most_common_words("a b a c b a", 2) -> [("a", 3), ("b", 2)]
    """
    # TODO: 在这里实现(from collections import Counter,再 .most_common(n))
    raise NotImplementedError


# ---- 任务 3 ----
def group_by_length(words: list[str]) -> dict[int, list[str]]:
    """按词的长度分组,保持原来的先后顺序。

    例:group_by_length(["hi", "yo", "hey"]) -> {2: ["hi", "yo"], 3: ["hey"]}
    """
    # TODO: 在这里实现(defaultdict(list),最后用 dict(...) 转回普通字典)
    raise NotImplementedError


# ---- 任务 4 ----
def days_between(d1: str, d2: str) -> int:
    """两个 ISO 格式日期字符串相差多少天(取绝对值,不分先后)。

    例:days_between("2026-09-05", "2026-10-01") -> 26
    """
    # TODO: 在这里实现(提示:date.fromisoformat,两个日期相减得到 timedelta)
    raise NotImplementedError


# ---- 任务 5 ----
def format_chinese_date(d: date) -> str:
    """把日期格式化成中文形式;月、日补足两位。

    不要用 strftime 输出中文(不同系统结果不一样),用 f-string 自己拼。
    例:format_chinese_date(date(2026, 9, 5)) -> "2026年09月05日"
    """
    # TODO: 在这里实现(提示:{d.month:02d})
    raise NotImplementedError


# ---- 任务 6 ----
def median_and_mean(nums: list[float]) -> tuple[float, float]:
    """返回 (中位数, 平均数)。

    例:median_and_mean([1, 2, 3, 10]) -> (2.5, 4.0)
    """
    # TODO: 在这里实现(import statistics,用 median / mean,结果转成 float)
    raise NotImplementedError


# ---- 任务 7 ----
def lcm(a: int, b: int) -> int:
    """求最小公倍数:a * b // gcd(a, b)。

    例:lcm(4, 6) -> 12;lcm(7, 3) -> 21
    """
    # TODO: 在这里实现(提示:math.gcd)
    raise NotImplementedError


# ====================== 自测:不要改动下面的代码 ======================


def _check_roll_dice() -> None:
    got = roll_dice(5, seed=42)
    assert len(got) == 5, f"应掷 5 次,实际 {len(got)} 次"
    assert all(1 <= x <= 6 for x in got), f"每个点数应在 1-6,实际 {got}"
    assert roll_dice(5, seed=42) == got, "同一个 seed 两次结果应完全相同"
    twenty = roll_dice(20, sides=20, seed=1)
    assert all(1 <= x <= 20 for x in twenty), "sides=20 时点数应在 1-20"
    assert roll_dice(0) == [], "掷 0 次应返回空列表"


def _check_most_common_words() -> None:
    got = most_common_words("a b a c b a", 2)
    assert got == [("a", 3), ("b", 2)], f"应为 [('a', 3), ('b', 2)],实际 {got}"
    upper = most_common_words("Hi hi HI yo", 1)
    assert upper == [("hi", 3)], f"应先转小写再统计,实际 {upper}"


def _check_group_by_length() -> None:
    got = group_by_length(["hi", "yo", "hey"])
    want = {2: ["hi", "yo"], 3: ["hey"]}
    assert got == want, f"应为 {want},实际 {got}"
    assert group_by_length([]) == {}, "空列表应返回空字典"


def _check_days_between() -> None:
    assert days_between("2026-09-05", "2026-10-01") == 26, "9-05 到 10-01 是 26 天"
    assert days_between("2026-10-01", "2026-09-05") == 26, "顺序反过来结果一样"
    assert days_between("2026-09-05", "2026-09-05") == 0, "同一天相差 0 天"


def _check_format_chinese_date() -> None:
    got = format_chinese_date(date(2026, 9, 5))
    assert got == "2026年09月05日", f"应为 '2026年09月05日',实际 {got!r}"
    got = format_chinese_date(date(2026, 12, 31))
    assert got == "2026年12月31日", f"应为 '2026年12月31日',实际 {got!r}"


def _check_median_and_mean() -> None:
    got = median_and_mean([1, 2, 3, 10])
    assert got == (2.5, 4.0), f"应为 (2.5, 4.0),实际 {got}"
    assert median_and_mean([5]) == (5.0, 5.0), "只有一个数时中位数和平均数都是它"


def _check_lcm() -> None:
    assert lcm(4, 6) == 12, "lcm(4, 6) 应为 12"
    assert lcm(7, 3) == 21, "互质的两数最小公倍数是它们的乘积"
    assert lcm(6, 6) == 6, "lcm(6, 6) 应为 6"


def _check_shapes_module() -> None:
    assert triangle_area(3, 4, 5) == 6.0, "3/4/5 直角三角形面积应为 6.0"
    assert rectangle_area(3, 4) == 12, "3 × 4 矩形面积应为 12"
    assert math.isclose(circle_area(1), math.pi), "半径 1 的圆面积应为 π"
    try:
        triangle_area(1, 1, 5)
    except ValueError:
        pass
    else:
        raise AssertionError("1/1/5 构不成三角形,应抛出 ValueError")


CHECKS = [
    _check_roll_dice,
    _check_most_common_words,
    _check_group_by_length,
    _check_days_between,
    _check_format_chinese_date,
    _check_median_and_mean,
    _check_lcm,
    _check_shapes_module,
]


def _run_checks() -> None:
    passed = 0
    for check in CHECKS:
        name = check.__name__.removeprefix("_check_")
        try:
            check()
        except NotImplementedError:
            print(f"[TODO] {name}: 还没实现")
        except AssertionError as e:
            print(f"[FAIL] {name}: {e}")
        except Exception as e:
            print(f"[ERROR] {name}: {type(e).__name__}: {e}")
        else:
            passed += 1
            print(f"[OK]   {name}")
    total = len(CHECKS)
    tail = ",本文件全部完成!" if passed == total else ""
    print(f"\n{passed}/{total} 通过{tail}")


if __name__ == "__main__":
    _run_checks()

04_oop_dataclass.py

exercises/day2/04_oop_dataclass.py
"""Day 2 · Block 4a + 4b · 面向对象与 dataclass

学习目标:用 @dataclass 快速建模(配 field(default_factory=list) 避开可变默认值坑);
      手写 class:__init__/self、类属性 vs 实例属性、@property 做只读属性;
      写 __str__/__repr__/__eq__,用 super().__init__() 继承,用组合装下一堆对象。
用法:uv run python exercises/day2/04_oop_dataclass.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK]。
      4a 做 Student 与 BankAccount,4b 做 SavingsAccount 与 Classroom。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

预测练习(先在纸上写出输出,再到 REPL 验证):
>>> from dataclasses import dataclass, field
>>> @dataclass
>>> class P:
>>>     name: str
>>>     tags: list[str] = field(default_factory=list)
>>> a = P("x"); b = P("x")
>>> print(a == b, a is b)          # ?
>>> print(a)                       # dataclass 自动生成的 __repr__
>>> class Acc:
>>>     def __init__(self, n): self.n = n
>>> c = Acc(1); d = c; d.n = 5
>>> print(c.n)                     # ?  对象也有别名
"""

from dataclasses import dataclass, field


# ---- 任务 1:@dataclass 建模(字段已给出,方法要你写) ----
@dataclass
class Student:
    """一个学生:名字 + 一串分数。

    例:Student("小明", [90, 80]).average() -> 85.0
    """

    name: str
    scores: list[int] = field(default_factory=list)

    def add_score(self, score: int) -> None:
        """添加一个分数;不在 0-100 之间时 raise ValueError。

        例:add_score(90) 之后 scores 多了一个 90;add_score(101) 抛 ValueError
        """
        # TODO: 在这里实现
        raise NotImplementedError

    def average(self) -> float:
        """平均分,保留 1 位小数;没有分数时返回 0.0。

        例:Student("A", [90, 80]).average() -> 85.0
        """
        # TODO: 在这里实现(提示:round(..., 1))
        raise NotImplementedError

    def best(self) -> int | None:
        """最高分;没有分数时返回 None。

        例:Student("A", [90, 80]).best() -> 90
        """
        # TODO: 在这里实现
        raise NotImplementedError


# ---- 任务 2:手写 class(类属性和方法签名已给出,方法体要你写) ----
class BankAccount:
    """一个银行账户:户主 + 余额(余额只读,只能通过存取款改变)。

    例:acc = BankAccount("张三");acc.deposit(100);acc.withdraw(30) -> 70.0
    """

    bank_name = "Python 银行"  # 类属性:所有账户共享

    def __init__(self, owner: str, balance: float = 0.0) -> None:
        """记下户主和初始余额;余额存在 self._balance 里(下划线表示"别乱动")。"""
        # TODO: 在这里实现(要给 self.owner 和 self._balance 赋值)
        raise NotImplementedError

    @property
    def balance(self) -> float:
        """只读的余额。只有 getter 没有 setter,所以赋值会抛 AttributeError。"""
        # TODO: 在这里实现(返回 self._balance)
        raise NotImplementedError

    def deposit(self, amount: float) -> float:
        """存钱,返回新余额;amount <= 0 时 raise ValueError("金额必须为正")。

        例:BankAccount("张三").deposit(100) -> 100.0
        """
        # TODO: 在这里实现
        raise NotImplementedError

    def withdraw(self, amount: float) -> float:
        """取钱,返回新余额;金额非正或超过余额时 raise ValueError。

        amount <= 0 → "金额必须为正";amount > 余额 → "余额不足"
        例:余额 100 时 withdraw(30) -> 70.0;withdraw(200) 抛 ValueError
        """
        # TODO: 在这里实现
        raise NotImplementedError

    def __str__(self) -> str:
        """给人看的样子:'张三: 70.00 元'(金额两位小数)。"""
        # TODO: 在这里实现
        raise NotImplementedError

    def __repr__(self) -> str:
        """给程序员看的样子:BankAccount(owner='张三', balance=70.0)。"""
        # TODO: 在这里实现(提示:用 {self.owner!r} 带出引号)
        raise NotImplementedError

    def __eq__(self, other) -> bool:
        """户主和余额都相同才算相等;和别的类型比较返回 NotImplemented。"""
        # TODO: 在这里实现(提示:先 isinstance(other, BankAccount))
        raise NotImplementedError


# ---- 任务 3:继承 ----
class SavingsAccount(BankAccount):
    """储蓄账户:在普通账户上多一个利率,可以结算利息。

    例:SavingsAccount("李四", 1000).add_interest() -> 20.0
    """

    def __init__(self, owner: str, balance: float = 0.0, rate: float = 0.02) -> None:
        """先用 super().__init__ 复用父类的初始化,再记下自己的利率。"""
        # TODO: 在这里实现(要给 self.rate 赋值)
        raise NotImplementedError

    def add_interest(self) -> float:
        """按利率算出利息、存入账户,并返回这笔利息。

        例:余额 1000、利率 0.02 -> 返回 20.0,之后余额 1020.0
        """
        # TODO: 在这里实现
        raise NotImplementedError

    def __str__(self) -> str:
        """在父类的样子后面追加利率:'李四: 1020.00 元(利率 2.0%)'。"""
        # TODO: 在这里实现(提示:super().__str__())
        raise NotImplementedError


# ---- 任务 4:组合(一个班里装很多学生) ----
@dataclass
class Classroom:
    """一个班级:名字 + 一群 Student 对象(组合优先于继承)。

    例:room = Classroom("一班");room.add(Student("A", [90]))
    """

    name: str
    students: list[Student] = field(default_factory=list)

    def add(self, student: Student) -> None:
        """把一个学生加进班级。"""
        # TODO: 在这里实现
        raise NotImplementedError

    def find(self, name: str) -> Student | None:
        """按名字找学生;找不到返回 None。

        例:room.find("A").name -> "A";room.find("不存在") -> None
        """
        # TODO: 在这里实现
        raise NotImplementedError

    def top_student(self) -> Student | None:
        """平均分最高的学生;班里没人时返回 None。

        例:A 平均 90、B 平均 75 -> 返回 A
        """
        # TODO: 在这里实现(提示:max(..., key=lambda s: s.average()))
        raise NotImplementedError

    def class_average(self) -> float:
        """全班"学生平均分"的平均值,保留 1 位小数;没人时返回 0.0。

        例:A 平均 90.0、B 平均 75.0 -> 82.5
        """
        # TODO: 在这里实现
        raise NotImplementedError


# ====================== 自测:不要改动下面的代码 ======================


def _check_student() -> None:
    a = Student("A")
    a.add_score(90)
    assert a.scores == [90], f"add_score(90) 后 scores 应为 [90],实际 {a.scores}"
    try:
        a.add_score(101)
    except ValueError:
        pass
    else:
        raise AssertionError("add_score(101) 应抛出 ValueError")
    assert Student("A") == Student("A"), "dataclass 会按字段自动生成 __eq__"
    assert Student("A") is not Student("A"), "相等不等于同一个对象"
    assert Student("X").scores is not Student("X").scores, (
        "两个实例的 scores 不能是同一个列表(要用 field(default_factory=list))"
    )


def _check_student_stats() -> None:
    s = Student("B", [90, 80])
    assert s.average() == 85.0, f"[90, 80] 的平均分应为 85.0,实际 {s.average()}"
    assert s.best() == 90, "最高分应为 90"
    assert Student("C", [70, 80, 86]).average() == 78.7, "应 round 到 1 位小数"
    empty = Student("D")
    assert empty.average() == 0.0, "没有分数时平均分应为 0.0"
    assert empty.best() is None, "没有分数时最高分应为 None"


def _check_bank_account() -> None:
    acc = BankAccount("张三")
    assert acc.owner == "张三", "owner 应被 __init__ 存下来"
    assert acc.balance == 0.0, "默认余额应为 0.0"
    assert BankAccount.bank_name == "Python 银行", "bank_name 应是类属性"
    assert acc.bank_name == "Python 银行", "实例也能读到类属性"
    try:
        acc.balance = 1
    except AttributeError:
        pass
    else:
        raise AssertionError("balance 应只读(@property 不写 setter)")


def _check_deposit_withdraw() -> None:
    acc = BankAccount("张三")
    assert acc.deposit(100) == 100.0, "deposit 应返回新余额 100.0"
    assert acc.withdraw(30) == 70.0, "withdraw 应返回新余额 70.0"
    assert acc.balance == 70.0, "两笔操作后余额应为 70.0"
    try:
        acc.deposit(0)
    except ValueError as e:
        assert str(e) == "金额必须为正", f"消息应为 '金额必须为正',实际 {str(e)!r}"
    else:
        raise AssertionError("deposit(0) 应抛出 ValueError")
    try:
        acc.withdraw(1000)
    except ValueError as e:
        assert str(e) == "余额不足", f"消息应为 '余额不足',实际 {str(e)!r}"
    else:
        raise AssertionError("余额不够时 withdraw 应抛出 ValueError")
    assert acc.balance == 70.0, "失败的存取款不应改变余额"


def _check_bank_account_dunder() -> None:
    acc = BankAccount("张三", 70.0)
    assert str(acc) == "张三: 70.00 元", f"str 应为 '张三: 70.00 元',实际 {str(acc)!r}"
    want = "BankAccount(owner='张三', balance=70.0)"
    assert repr(acc) == want, f"repr 应为 {want},实际 {repr(acc)}"
    assert acc == BankAccount("张三", 70.0), "同户主同余额应相等"
    assert acc != BankAccount("李四", 70.0), "户主不同不应相等"
    assert acc != BankAccount("张三", 10.0), "余额不同不应相等"
    assert acc != "张三", "和别的类型比较应为不相等(返回 NotImplemented)"


def _check_savings_account() -> None:
    s = SavingsAccount("李四", 1000)
    assert isinstance(s, BankAccount), "SavingsAccount 应继承 BankAccount"
    assert s.rate == 0.02, "默认利率应为 0.02"
    interest = s.add_interest()
    assert interest == 20.0, f"利息应为 20.0,实际 {interest}"
    assert s.balance == 1020.0, f"结息后余额应为 1020.0,实际 {s.balance}"
    got = str(s)
    assert got == "李四: 1020.00 元(利率 2.0%)", f"__str__ 不对:{got!r}"


def _check_classroom() -> None:
    room = Classroom("一班")
    room.add(Student("A", [90]))
    room.add(Student("B", [70, 80]))
    assert len(room.students) == 2, "加了两个学生,students 应有 2 个元素"
    top = room.top_student()
    assert top is not None and top.name == "A", "平均分最高的是 A"
    got = room.class_average()
    assert got == 82.5, f"(90.0 + 75.0) / 2 应为 82.5,实际 {got}"
    found = room.find("B")
    assert found is not None and found.scores == [70, 80], "find('B') 应找到 B"
    assert room.find("不存在") is None, "找不到应返回 None"
    empty = Classroom("空班")
    assert empty.class_average() == 0.0, "空班平均分应为 0.0"
    assert empty.top_student() is None, "空班没有第一名,应为 None"


CHECKS = [
    _check_student,
    _check_student_stats,
    _check_bank_account,
    _check_deposit_withdraw,
    _check_bank_account_dunder,
    _check_savings_account,
    _check_classroom,
]


def _run_checks() -> None:
    passed = 0
    for check in CHECKS:
        name = check.__name__.removeprefix("_check_")
        try:
            check()
        except NotImplementedError:
            print(f"[TODO] {name}: 还没实现")
        except AssertionError as e:
            print(f"[FAIL] {name}: {e}")
        except Exception as e:
            print(f"[ERROR] {name}: {type(e).__name__}: {e}")
        else:
            passed += 1
            print(f"[OK]   {name}")
    total = len(CHECKS)
    tail = ",本文件全部完成!" if passed == total else ""
    print(f"\n{passed}/{total} 通过{tail}")


if __name__ == "__main__":
    _run_checks()

05_pythonic.py

exercises/day2/05_pythonic.py
"""Day 2 · Block 5 · Pythonic 写法、迭代与类型注解

学习目标:用推导式、sorted(key=)、any/all、enumerate 把循环写短写清楚;
      会写生成器函数与 match 语句;给每个函数补上完整类型注解。
用法:uv run python exercises/day2/05_pythonic.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK]。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

预测练习(先在纸上写出输出,再到 REPL 验证):
>>> print([x * 2 for x in range(3)], {x % 3 for x in range(10)})
>>> print({w: len(w) for w in ["a", "bb"]})
>>> g = (x * x for x in range(3))
>>> print(next(g), next(g), next(g))     # 再 next(g) → StopIteration
>>> print(sorted(["b", "A", "c"]), sorted(["b", "A", "c"], key=str.lower))
>>> print(sorted([("a", 3), ("b", 1)], key=lambda t: t[1]))
>>> print(any(x > 5 for x in [1, 7]), all(x > 5 for x in [1, 7]))
"""

from collections.abc import Iterator
from typing import Any


# ---- 任务 1 ----
def squares_of_evens(nums: list[int]) -> list[int]:
    """偶数的平方(用列表推导式一行完成)。例:[1, 2, 3, 4] -> [4, 16]"""
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 2 ----
def word_length_map(words: list[str]) -> dict[str, int]:
    """词 -> 词长的字典(用字典推导式)。

    例:["hi", "hey"] -> {"hi": 2, "hey": 3}
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 3 ----
def flatten(matrix: list[list[int]]) -> list[int]:
    """把二维列表拉平(用嵌套推导式一行完成)。

    例:[[1, 2], [3], []] -> [1, 2, 3]
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 4 ----
def sort_by_score(
    records: list[dict[str, Any]], reverse: bool = True
) -> list[dict[str, Any]]:
    """按 record["score"] 排序,返回新列表,不修改入参。

    要求用 sorted(..., key=lambda r: r["score"])。
    例:[{"n": "a", "score": 1}, {"n": "b", "score": 5}] -> b 在前
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 5 ----
def all_passed(scores: list[int], threshold: int = 60) -> bool:
    """是否全部达到 threshold(用 all)。例:[60, 70] -> True"""
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 6 ----
def any_perfect(scores: list[int]) -> bool:
    """是否有满分 100(用 any)。例:[99, 100] -> True"""
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 7 ----
def countdown(n: int) -> Iterator[int]:
    """生成器:依次 yield n, n-1, ..., 0。

    例:list(countdown(3)) -> [3, 2, 1, 0]
    """
    # TODO: 在这里实现(用 yield 或 yield from,不要 return 列表)
    raise NotImplementedError


# ---- 任务 8 ----
def chunks(items: list[Any], size: int) -> Iterator[list[Any]]:
    """生成器:把 items 切成每段 size 个,最后一段可能不足。

    size <= 0 时抛出 ValueError("size 必须为正")。
    例:list(chunks([1, 2, 3, 4, 5], 2)) -> [[1, 2], [3, 4], [5]]
    """
    # TODO: 在这里实现(用 yield)
    raise NotImplementedError


# ---- 任务 9 ----
def even_sum_below(limit: int) -> int:
    """小于 limit 的偶数之和(用 sum(生成器表达式))。例:10 -> 20"""
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 10 ----
def describe_command(cmd: str) -> str:
    """解析一条命令,必须用 match cmd.split(): 实现。

    ["add", a, b] -> f"结果:{int(a) + int(b)}"
    ["greet", name] -> f"你好,{name}"
    ["quit"] -> "再见";[] -> "空命令";其他 -> "未知命令"
    例:"add 3 4" -> "结果:7"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 11 ----
def find_user(users: list[dict[str, Any]], name: str) -> dict[str, Any] | None:
    """按 name 查用户,找不到返回 None(可用 next(..., None))。

    例:find_user([{"name": "小明"}], "小刚") -> None
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 12 ----
def numbered(words: list[str]) -> list[tuple[int, str]]:
    """给每个词编号,从 1 开始(用 enumerate(start=1))。

    例:["a", "b"] -> [(1, "a"), (2, "b")]
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ====================== 自测:不要改动下面的代码 ======================


def _check_squares_of_evens() -> None:
    assert squares_of_evens([1, 2, 3, 4]) == [4, 16], (
        "squares_of_evens([1,2,3,4]) 应为 [4, 16]"
    )
    assert squares_of_evens([]) == [], "空列表应返回空列表"


def _check_word_length_map() -> None:
    assert word_length_map(["hi", "hey"]) == {"hi": 2, "hey": 3}, (
        'word_length_map(["hi","hey"]) 应为 {"hi": 2, "hey": 3}'
    )


def _check_flatten() -> None:
    assert flatten([[1, 2], [3], []]) == [1, 2, 3], (
        "flatten([[1,2],[3],[]]) 应为 [1, 2, 3]"
    )


def _check_sort_by_score() -> None:
    data = [{"n": "a", "score": 1}, {"n": "b", "score": 5}]
    got = sort_by_score(data)
    assert [r["n"] for r in got] == ["b", "a"], "默认降序,b 应在前"
    assert [r["n"] for r in sort_by_score(data, reverse=False)] == ["a", "b"], (
        "reverse=False 时应升序"
    )
    assert [r["n"] for r in data] == ["a", "b"], "不能修改入参 records"


def _check_all_passed() -> None:
    assert all_passed([60, 70]) is True, "[60, 70] 应为 True"
    assert all_passed([59, 100]) is False, "[59, 100] 应为 False"
    assert all_passed([]) is True, "空列表的 all 为 True"


def _check_any_perfect() -> None:
    assert any_perfect([99, 100]) is True, "[99, 100] 应为 True"
    assert any_perfect([99, 98]) is False, "[99, 98] 应为 False"


def _check_countdown() -> None:
    assert list(countdown(3)) == [3, 2, 1, 0], "countdown(3) 应为 [3, 2, 1, 0]"
    assert list(countdown(0)) == [0], "countdown(0) 应为 [0]"


def _check_chunks() -> None:
    assert list(chunks([1, 2, 3, 4, 5], 2)) == [[1, 2], [3, 4], [5]], (
        "chunks([1,2,3,4,5], 2) 应为 [[1,2],[3,4],[5]]"
    )
    try:
        list(chunks([1, 2], 0))
    except ValueError:
        pass
    else:
        raise AssertionError("size <= 0 时应抛出 ValueError")


def _check_even_sum_below() -> None:
    assert even_sum_below(10) == 20, "even_sum_below(10) 应为 20"
    assert even_sum_below(1) == 0, "even_sum_below(1) 应为 0"


def _check_describe_command() -> None:
    assert describe_command("add 3 4") == "结果:7", '"add 3 4" 应为 "结果:7"'
    assert describe_command("greet 小明") == "你好,小明", "greet 分支不对"
    assert describe_command("quit") == "再见", '"quit" 应为 "再见"'
    assert describe_command("") == "空命令", '"" 应为 "空命令"'
    assert describe_command("jump") == "未知命令", '"jump" 应为 "未知命令"'


def _check_find_user() -> None:
    users = [{"name": "小明", "age": 15}, {"name": "小红", "age": 16}]
    assert find_user(users, "小红") == {"name": "小红", "age": 16}, "应找到小红"
    assert find_user(users, "小刚") is None, "找不到时应返回 None"


def _check_numbered() -> None:
    assert numbered(["a", "b"]) == [(1, "a"), (2, "b")], "编号应从 1 开始"
    assert numbered([]) == [], "空列表应返回空列表"


CHECKS = [
    _check_squares_of_evens,
    _check_word_length_map,
    _check_flatten,
    _check_sort_by_score,
    _check_all_passed,
    _check_any_perfect,
    _check_countdown,
    _check_chunks,
    _check_even_sum_below,
    _check_describe_command,
    _check_find_user,
    _check_numbered,
]


def _run_checks() -> None:
    passed = 0
    for check in CHECKS:
        name = check.__name__.removeprefix("_check_")
        try:
            check()
        except NotImplementedError:
            print(f"[TODO] {name}: 还没实现")
        except AssertionError as e:
            print(f"[FAIL] {name}: {e}")
        except Exception as e:
            print(f"[ERROR] {name}: {type(e).__name__}: {e}")
        else:
            passed += 1
            print(f"[OK]   {name}")
    total = len(CHECKS)
    tail = ",本文件全部完成!" if passed == total else ""
    print(f"\n{passed}/{total} 通过{tail}")


if __name__ == "__main__":
    _run_checks()

06_testing/test_wordtools.py

exercises/day2/06_testing/test_wordtools.py
"""Day 2 · Block 6 · wordtools 的 pytest 测试(已写好,你只补最后两个)

用法:uv run pytest exercises/day2/06_testing -q
读输出:. 表示通过,F 表示断言失败,E 表示测试里抛了异常。
先看懂这些测试想验证什么,再去 wordtools.py 里实现它们。
"""

import pytest
from wordtools import censor, count_words, longest_word, normalize


def test_normalize_basic() -> None:
    """最常见的一种:大小写 + 首尾空白 + 中间多个空格。"""
    assert normalize("  Hello   World  ") == "hello world"


@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        ("Hello", "hello"),
        ("  a  b  ", "a b"),
        ("ABC DEF", "abc def"),
        ("", ""),
    ],
)
def test_normalize_parametrized(raw: str, expected: str) -> None:
    """parametrize:一个测试函数跑 4 组数据,失败时会告诉你是哪一组。"""
    assert normalize(raw) == expected


def test_count_words_basic() -> None:
    """词数就是 normalize 之后按空格切出来的段数。"""
    assert count_words("a b  c") == 3


def test_count_words_empty() -> None:
    """空串和纯空白都算 0 个词。"""
    assert count_words("") == 0
    assert count_words("     ") == 0


def test_count_words_rejects_none() -> None:
    """pytest.raises:断言"这里必须抛 TypeError"。"""
    with pytest.raises(TypeError):
        count_words(None)


def test_longest_word() -> None:
    """最长的词。"""
    assert longest_word("I love programming") == "programming"


def test_longest_word_tie_takes_first() -> None:
    """长度并列时取先出现的那个。"""
    assert longest_word("aa bb c") == "aa"


def test_longest_word_empty() -> None:
    """空串没有词,返回空串。"""
    assert longest_word("") == ""


def test_censor_replaces_banned() -> None:
    """命中的整词换成 ***,其余保持 normalize 后的样子。"""
    assert censor("Hello Stupid world", ["stupid"]) == "hello *** world"


def test_censor_is_case_insensitive() -> None:
    """banned 和原文都不分大小写,整词才算命中(stupidly 不算)。"""
    assert censor("STUPID stupidly ok", ["Stupid"]) == "*** stupidly ok"


# ====================== 下面两个由你补充 ======================


def test_normalize_tabs_and_newlines() -> None:
    """normalize 也要能处理制表符和换行:它们同样算空白。

    提示:输入里放一个制表符和一个换行符,期望结果是 "a b c"。
    """
    # TODO: 补充测试
    assert True  # 删掉这行,写你的断言


def test_censor_empty_banned() -> None:
    """banned 是空列表时,censor 的结果应该和 normalize 一样。"""
    # TODO: 补充测试
    assert True  # 删掉这行,写你的断言

06_testing/wordtools.py

exercises/day2/06_testing/wordtools.py
"""Day 2 · Block 6 · wordtools:一个可被 pytest 测试的小模块(你来实现)

学习目标:把"算"和"打印"分开,写成纯函数才好测试;
      让 test_wordtools.py 里的测试从红变绿。
用法:uv run pytest exercises/day2/06_testing -q
做法:把每个 TODO 换成你的实现,反复运行 pytest 直到全绿;
      再运行 uv run ruff check exercises/day2 保证零警告。
"""


def normalize(text: str) -> str:
    """规范化文本:转小写、去首尾空白、把多个连续空白压成一个空格。

    text 不是字符串时抛 TypeError("text 必须是字符串")。
    例:"  Hello   World  " -> "hello world"
    提示:str.split() 不带参数时会按任意空白切分并丢掉空串。
    """
    # TODO: 在这里实现
    raise NotImplementedError


def count_words(text: str) -> int:
    """统计词数(先 normalize,再按空格切分)。空串 -> 0。

    text 不是字符串时抛 TypeError("text 必须是字符串")。
    例:"a b  c" -> 3;"   " -> 0
    """
    # TODO: 在这里实现
    raise NotImplementedError


def longest_word(text: str) -> str:
    """返回最长的词(先 normalize);并列时取先出现的;空串 -> ""。

    例:"I love programming" -> "programming"
    例:"aa bb c" -> "aa"
    """
    # TODO: 在这里实现
    raise NotImplementedError


def censor(text: str, banned: list[str]) -> str:
    """把 banned 里的词替换成 "***",其余保持 normalize 后的形式。

    整词匹配、不分大小写;banned 为空列表时等价于 normalize。
    例:censor("Hello Stupid world", ["stupid"]) -> "hello *** world"
    """
    # TODO: 在这里实现
    raise NotImplementedError

07_grades_v2/data/grades.csv

exercises/day2/07_grades_v2/data/grades.csv
1
2
3
4
5
6
7
8
9
姓名,班级,语文,数学,英语
张三,1班,85,92,78
李四,1班,90,88,95
王五,1班,70,65,80
赵六,2班,88,79,91
孙七,2班,95,90,93
周八,2班,60,缺考,70
吴九,2班,82,77,85
郑十,1班,,88,90

07_grades_v2/grades_v2.py

exercises/day2/07_grades_v2/grades_v2.py
"""Day 2 · Block 7 · 成绩单 v2(CSV -> dataclass -> 统计 -> 报表文件)

学习目标:把 Day 1 的成绩单升级成一个有数据文件、有建模、有测试的小项目;
      练习 csv.DictReader、dataclass、脏数据容错和"算与打印分离"。
用法:uv run pytest exercises/day2/07_grades_v2 -q
      uv run python exercises/day2/07_grades_v2/grades_v2.py
做法:把每个 TODO 换成你的实现,让 test_grades_v2.py 从红变绿;
      main() 已经写好,不用改。需要 import csv(自己加)。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问。
"""

from dataclasses import dataclass, field
from pathlib import Path

SUBJECTS = ["语文", "数学", "英语"]


@dataclass
class StudentRecord:
    """一名学生:姓名、班级、各科分数。"""

    name: str
    class_name: str
    scores: dict[str, int] = field(default_factory=dict)

    def average(self) -> float:
        """平均分,round(..., 1);没有任何分数时返回 0.0。

        例:{"语文": 85, "数学": 92, "英语": 78} -> 85.0
        """
        # TODO: 在这里实现
        raise NotImplementedError


def load_records(path: Path) -> tuple[list[StudentRecord], list[str]]:
    """读 CSV(UTF-8),返回 (记录列表, 警告列表)。

    用 csv.DictReader;某行任一科分数无法 int() 就整行跳过,并记一条
    f"第 {行号} 行 {姓名}:分数无效,已跳过"(数据首行算第 2 行)。
    提示:enumerate(reader, start=2) 就能拿到行号。
    """
    # TODO: 在这里实现
    raise NotImplementedError


def letter_grade(score: float) -> str:
    """分数转等级:A≥90,B≥80,C≥70,D≥60,其余 F。例:85.0 -> "B"。"""
    # TODO: 在这里实现
    raise NotImplementedError


def class_averages(records: list[StudentRecord]) -> dict[str, float]:
    """每个班级"学生平均分"的平均,round(..., 1),按班级名排序。

    注意:先算每人的**未四舍五入**平均,再求平均,最后才 round 一次。
    例:{"1班": 82.6, "2班": 86.7}
    """
    # TODO: 在这里实现
    raise NotImplementedError


def subject_averages(records: list[StudentRecord]) -> dict[str, float]:
    """各科平均分,round(..., 1)。例:{"语文": 85.0, ...}"""
    # TODO: 在这里实现
    raise NotImplementedError


def rank_students(records: list[StudentRecord]) -> list[tuple[int, str, float]]:
    """按平均分降序、同分按姓名升序,返回 (名次, 姓名, 平均分),名次从 1 起。

    提示:sorted(records, key=lambda r: (-r.average(), r.name))
    """
    # TODO: 在这里实现
    raise NotImplementedError


def build_report(records: list[StudentRecord], warnings: list[str]) -> str:
    """把统计结果拼成多行报表文本:标题、排名表、班级平均、各科平均、警告。

    排名表每行的格式:f"{名次:>2}. {姓名:<4}{平均:>6.1f} {等级}"
    提示:先把每行放进一个 list,最后用换行符 join 起来。
    """
    # TODO: 在这里实现
    raise NotImplementedError


def save_report(path: Path, text: str) -> None:
    """UTF-8 写入报表文本,父目录不存在则创建。"""
    # TODO: 在这里实现
    raise NotImplementedError


def main() -> None:
    """读 data/grades.csv,打印报表并写入 data/report.txt。"""
    data_path = Path(__file__).parent / "data" / "grades.csv"
    records, warnings = load_records(data_path)
    report = build_report(records, warnings)
    print(report)
    report_path = data_path.parent / "report.txt"
    save_report(report_path, report)
    print(f"\n报表已写入:{report_path}")


if __name__ == "__main__":
    main()

07_grades_v2/test_grades_v2.py

exercises/day2/07_grades_v2/test_grades_v2.py
"""Day 2 · Block 7 · 成绩单 v2 的 pytest 测试(已给出,不用改)

用法:uv run pytest exercises/day2/07_grades_v2 -q
这些测试直接读 data/grades.csv 这份真实数据,
所以你的实现必须能正确跳过"缺考"和空分数那两行。
"""

from pathlib import Path

import pytest
from grades_v2 import (
    StudentRecord,
    build_report,
    class_averages,
    letter_grade,
    load_records,
    rank_students,
    save_report,
    subject_averages,
)

DATA = Path(__file__).parent / "data" / "grades.csv"


def test_load_records_counts() -> None:
    """8 行数据里有 2 行脏数据,应得到 6 条记录和 2 条警告。"""
    records, warnings = load_records(DATA)
    assert len(records) == 6
    assert len(warnings) == 2
    assert "周八" in warnings[0]
    assert "郑十" in warnings[1]


def test_load_records_fields() -> None:
    """第一条记录是张三,班级和三科分数都要读对。"""
    records, _ = load_records(DATA)
    first = records[0]
    assert first.name == "张三"
    assert first.class_name == "1班"
    assert first.scores == {"语文": 85, "数学": 92, "英语": 78}


def test_student_record_average() -> None:
    """平均分保留一位小数;没有分数时是 0.0。"""
    record = StudentRecord("王五", "1班", {"语文": 70, "数学": 65, "英语": 80})
    assert record.average() == 71.7
    assert StudentRecord("空", "1班").average() == 0.0


@pytest.mark.parametrize(
    ("score", "expected"),
    [
        (100.0, "A"),
        (90.0, "A"),
        (89.9, "B"),
        (80.0, "B"),
        (79.9, "C"),
        (70.0, "C"),
        (69.9, "D"),
        (60.0, "D"),
        (59.9, "F"),
        (0.0, "F"),
    ],
)
def test_letter_grade(score: float, expected: str) -> None:
    """五档等级的边界值:>= 90/80/70/60,其余 F。"""
    assert letter_grade(score) == expected


def test_rank_students() -> None:
    """按平均分降序排名,第一名是孙七。"""
    records, _ = load_records(DATA)
    ranking = rank_students(records)
    assert ranking[0] == (1, "孙七", 92.7)
    assert [name for _, name, _ in ranking] == [
        "孙七",
        "李四",
        "赵六",
        "张三",
        "吴九",
        "王五",
    ]


def test_class_averages() -> None:
    """班级平均 = 该班学生"未四舍五入平均分"的平均,最后 round 一次。"""
    records, _ = load_records(DATA)
    assert class_averages(records) == {"1班": 82.6, "2班": 86.7}


def test_subject_averages() -> None:
    """各科平均只统计有效的 6 条记录。"""
    records, _ = load_records(DATA)
    assert subject_averages(records) == {"语文": 85.0, "数学": 81.8, "英语": 87.0}


def test_load_records_skips_dirty_rows(tmp_path: Path) -> None:
    """用 tmp_path 造一份含脏行的临时 CSV,验证跳过逻辑与行号。"""
    csv_path = tmp_path / "dirty.csv"
    csv_path.write_text(
        "姓名,班级,语文,数学,英语\n甲,3班,80,80,80\n乙,3班,90,缺考,70\n丙,3班,,60,60\n",
        encoding="utf-8",
    )
    records, warnings = load_records(csv_path)
    assert [record.name for record in records] == ["甲"]
    assert warnings == [
        "第 3 行 乙:分数无效,已跳过",
        "第 4 行 丙:分数无效,已跳过",
    ]


def test_build_report() -> None:
    """报表里要能看到第一名和警告区。"""
    records, warnings = load_records(DATA)
    report = build_report(records, warnings)
    assert "孙七" in report
    assert "92.7" in report
    assert "警告" in report


def test_save_report(tmp_path: Path) -> None:
    """写进去的文本应该能原样读回来(UTF-8)。"""
    target = tmp_path / "out" / "report.txt"
    save_report(target, "你好\n报表")
    assert target.read_text(encoding="utf-8") == "你好\n报表"

shapes.py

exercises/day2/shapes.py
"""Day 2 · Block 3 · 自己写的第一个模块:图形面积

学习目标:把一组相关函数放进自己的模块,供别的文件 import;
      用 if __name__ == "__main__": 区分"直接运行"和"被别人 import";
      参数不合法时 raise ValueError,而不是返回一个假的数字。
用法:uv run python exercises/day2/shapes.py(直接运行会打印三个示例)
做法:把每个 TODO 换成你的实现(记得 import math),
      再运行 uv run python exercises/day2/03_modules_stdlib.py 看 shapes 那项是否 [OK]。
      被 03_modules_stdlib.py 导入时,文件末尾的示例不应该被打印。
"""


def circle_area(r: float) -> float:
    """圆面积 = π r²;半径为负数时 raise ValueError。

    例:circle_area(1) -> 3.141592653589793
    """
    # TODO: 在这里实现
    raise NotImplementedError


def rectangle_area(w: float, h: float) -> float:
    """矩形面积 = 宽 × 高;任一边为负数时 raise ValueError。

    例:rectangle_area(3, 4) -> 12
    """
    # TODO: 在这里实现
    raise NotImplementedError


def triangle_area(a: float, b: float, c: float) -> float:
    """用海伦公式算三角形面积;三边构不成三角形时 raise ValueError。

    构成条件:三边都为正,且任意两边之和大于第三边。
    例:triangle_area(3, 4, 5) -> 6.0;triangle_area(1, 1, 5) 抛 ValueError
    """
    # TODO: 在这里实现(海伦公式:s = (a+b+c)/2,面积 = √(s(s-a)(s-b)(s-c)))
    raise NotImplementedError


if __name__ == "__main__":
    print(f"半径 1 的圆面积:{circle_area(1):.4f}")
    print(f"3 × 4 矩形面积:{rectangle_area(3, 4)}")
    print(f"边长 3/4/5 三角形面积:{triangle_area(3, 4, 5)}")