跳转至

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

怎么用

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

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

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

00_env_check.py

exercises/day1/00_env_check.py
"""Day 1 · Block 0 · 环境自检(范例文件,无 TODO,直接运行)

这个文件不需要填空,作用是确认环境没问题,顺便示范"能跑的脚本"长什么样。
sys.version 是解释器版本字符串,sys.version_info 是用来做比较的版本元组(要 >= 3.14)。
Path.cwd() 是当前工作目录,相对路径从这里算起,所以请在项目根目录运行。
sys.getfilesystemencoding() 是文件名编码,locale.getpreferredencoding(False)
是打开文本文件的默认编码(Windows 上若是 GBK 就是中文乱码的根源);
sys.flags.utf8_mode 为 1 表示解释器开启了 UTF-8 模式。

用法:uv run python exercises/day1/00_env_check.py
"""

import locale
import sys
from pathlib import Path


def main() -> None:
    """打印环境信息,并给出一行总结。"""
    version_ok = sys.version_info >= (3, 14)

    print(f"Python 版本      : {sys.version}")
    print(f"版本 >= 3.14     : {version_ok}  (version_info = {sys.version_info})")
    print(f"当前工作目录     : {Path.cwd()}")
    print(f"文件系统编码     : {sys.getfilesystemencoding()}")
    print(f"文本默认编码     : {locale.getpreferredencoding(False)}")
    print(f"UTF-8 模式       : {sys.flags.utf8_mode}")

    if version_ok:
        print("\n[OK] 环境正常")
    else:
        print(
            f"\n[FAIL] 需要 Python 3.14 或更高版本,当前是 {sys.version_info.major}."
            f"{sys.version_info.minor};请运行 uv python install 3.14 后重试"
        )


if __name__ == "__main__":
    main()

01_types_and_fstrings.py

exercises/day1/01_types_and_fstrings.py
"""Day 1 · Block 1 · 基础类型、运算、f-string

学习目标:分清 int/float/bool/str/None,掌握 / 与 //、%、** 的区别;
知道 input() 返回字符串必须转换,理解真值与短路;
会用 f-string 的 :.2f、:>8、:02d、{x=} 控制输出格式。
用法:uv run python exercises/day1/01_types_and_fstrings.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK]。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

陷阱反例(要能解释):input() 拿到的是字符串,"3" + 1 报 TypeError;
0.1 + 0.2 != 0.3(二进制浮点);/ 的结果永远是 float,10 / 2 是 5.0。

预测练习(先在纸上写出输出,再到 REPL 验证):
>>> print(7 / 2, 7 // 2, 7 % 2)
>>> print(-7 // 2, -7 % 2)          # 向下取整;余数与除数同号
>>> print(2 ** 10, 10 ** -1)
>>> print(int("42") + 1, str(42) + "1")
>>> print(True + True, bool(""), bool("0"), bool([]))
>>> print(0.1 + 0.2 == 0.3, round(0.1 + 0.2, 2) == 0.3)
>>> x = 3.14159
>>> print(f"{x:.2f}|{x:>10.3f}|{x:<8}|{x=}")
>>> print(f"{7:02d}:{5:02d}", f"{1234567:,}", f"{0.256:.1%}")
"""


# ---- 任务 1 ----
def celsius_to_fahrenheit(c: float) -> float:
    """摄氏度转华氏度,公式 c * 9 / 5 + 32,结果保留 1 位小数。

    例:celsius_to_fahrenheit(100) -> 212.0
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 2 ----
def describe_number(n: int) -> str:
    """描述整数 n:零 / 正奇数 / 正偶数 / 负奇数 / 负偶数。

    例:describe_number(0) -> "零";describe_number(-3) -> "负奇数"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 3 ----
def seconds_to_hms(total: int) -> str:
    """把总秒数用 // 和 % 拆成时:分:秒,每段补零到 2 位。

    例:seconds_to_hms(3661) -> "01:01:01"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 4 ----
def format_receipt(item: str, price: float, qty: int) -> str:
    """生成一行小票,格式必须精确到每一个空格。

    格式:f"{item:<10}{qty:>3} x {price:>8.2f} = {price * qty:>9.2f}"
    例:format_receipt("pen", 1.25, 10)
        -> "pen        10 x     1.25 =     12.50"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 5 ----
def is_close(a: float, b: float, tol: float = 1e-9) -> bool:
    """判断两个浮点数是否足够接近,即 abs(a - b) < tol。

    例:is_close(0.1 + 0.2, 0.3) -> True
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 6 ----
def average(nums: list[float]) -> float:
    """求平均值;空列表返回 0.0,否则返回 sum(nums) / len(nums)。

    例:average([1, 2, 3]) -> 2.0;average([]) -> 0.0
    """
    # TODO: 在这里实现
    raise NotImplementedError


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


def _check_celsius_to_fahrenheit() -> None:
    assert celsius_to_fahrenheit(0) == 32.0, "celsius_to_fahrenheit(0) 应为 32.0"
    assert celsius_to_fahrenheit(100) == 212.0, "celsius_to_fahrenheit(100) 应为 212.0"
    assert celsius_to_fahrenheit(-40) == -40.0, "celsius_to_fahrenheit(-40) 应为 -40.0"
    assert celsius_to_fahrenheit(36.6) == 97.9, "celsius_to_fahrenheit(36.6) 应为 97.9"


def _check_describe_number() -> None:
    assert describe_number(0) == "零", 'describe_number(0) 应为 "零"'
    assert describe_number(4) == "正偶数", 'describe_number(4) 应为 "正偶数"'
    assert describe_number(7) == "正奇数", 'describe_number(7) 应为 "正奇数"'
    assert describe_number(-3) == "负奇数", 'describe_number(-3) 应为 "负奇数"'
    assert describe_number(-8) == "负偶数", 'describe_number(-8) 应为 "负偶数"'


def _check_seconds_to_hms() -> None:
    assert seconds_to_hms(0) == "00:00:00", 'seconds_to_hms(0) 应为 "00:00:00"'
    assert seconds_to_hms(3661) == "01:01:01", 'seconds_to_hms(3661) 应为 "01:01:01"'
    assert seconds_to_hms(86399) == "23:59:59", 'seconds_to_hms(86399) 应为 "23:59:59"'
    assert seconds_to_hms(90061) == "25:01:01", (
        'seconds_to_hms(90061) 应为 "25:01:01"(小时不封顶到 24)'
    )


def _check_format_receipt() -> None:
    expected_cn = "苹果" + " " * 8 + "  4 x     3.50 =     14.00"
    assert format_receipt("苹果", 3.5, 4) == expected_cn, (
        f"format_receipt('苹果', 3.5, 4) 应为 {expected_cn!r}"
    )
    expected_en = "pen        10 x     1.25 =     12.50"
    assert format_receipt("pen", 1.25, 10) == expected_en, (
        f"format_receipt('pen', 1.25, 10) 应为 {expected_en!r}"
    )


def _check_is_close() -> None:
    assert is_close(0.1 + 0.2, 0.3) is True, "is_close(0.1 + 0.2, 0.3) 应为 True"
    assert is_close(1.0, 1.1) is False, "is_close(1.0, 1.1) 应为 False"
    assert is_close(1.0, 1.1, 0.5) is True, "容差放大到 0.5 时应为 True"


def _check_average() -> None:
    assert average([1, 2, 3]) == 2.0, "average([1, 2, 3]) 应为 2.0"
    assert average([]) == 0.0, "average([]) 应为 0.0(不能除零)"
    assert average([2.5]) == 2.5, "average([2.5]) 应为 2.5"


CHECKS = [
    _check_celsius_to_fahrenheit,
    _check_describe_number,
    _check_seconds_to_hms,
    _check_format_receipt,
    _check_is_close,
    _check_average,
]


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_strings.py

exercises/day1/02_strings.py
"""Day 1 · Block 2 · 字符串

学习目标:熟练使用索引、负索引、切片和 [::-1],理解字符串不可变;
掌握 strip/split/join/replace/upper/lower/startswith/find/count 等常用方法;
知道 ord/chr 与 Unicode,明白字符数和 UTF-8 字节数不是一回事。
用法:uv run python exercises/day1/02_strings.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK]。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

陷阱反例:s.upper() 之后 s 本身没变(要写 s = s.upper());
"abc"[3] 报 IndexError 但 "abc"[3:] 是空串;
len("你好") 是 2 个字符,len("你好".encode()) 是 6 个字节。

预测练习(先在纸上写出输出,再到 REPL 验证):
>>> s = "Python"
>>> print(s[0], s[-1], s[1:4], s[::-1], s[::2], s[10:])   # 切片越界不报错
>>> print(s.upper(), s)                                     # s 本身不变
>>> print("a,b,,c".split(","), "  x  ".strip(), "-".join(["a", "b"]))
>>> print("3" * 3, "3" + "4", len("你好"), "你好".encode("utf-8"))
>>> # s[0] = "J"      → TypeError: 'str' object does not support item assignment
>>> # s[10]           → IndexError(索引越界报错,切片越界不报错)
"""


# ---- 任务 1 ----
def is_palindrome(s: str) -> bool:
    """判断是否回文,忽略大小写与空格。

    例:is_palindrome("Never odd or even") -> True
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 2 ----
def count_vowels(s: str) -> int:
    """统计 a/e/i/o/u 的个数,不分大小写。

    例:count_vowels("Hello World") -> 3
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 3 ----
def capitalize_words(s: str) -> str:
    """每个词首字母大写,多余空白压成单个空格(用 split() + join())。

    注意:不要用 .title(),自己动手才练到 split/join。
    例:capitalize_words("hello   world") -> "Hello World"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 4 ----
def mask_phone(phone: str) -> str:
    """把 11 位手机号中间 4 位换成 *。

    长度不是 11 位,或含有非数字字符,抛 ValueError。
    例:mask_phone("13812345678") -> "138****5678"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 5 ----
def caesar_shift(s: str, k: int) -> str:
    """凯撒密码:只移动英文字母,保留大小写,其他字符原样返回。

    提示:用 ord/chr 加 % 26 让 z 绕回 a。
    例:caesar_shift("Hello, World!", 13) -> "Uryyb, Jbeyq!"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 6 ----
def word_lengths(sentence: str) -> list[int]:
    """返回每个词的长度组成的列表。

    例:word_lengths("I love Python") -> [1, 4, 6]
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 7 ----
def extract_domain(email: str) -> str:
    """取出邮箱 @ 后面的域名;没有 @ 或有多个 @ 抛 ValueError。

    例:extract_domain("user@example.com") -> "example.com"
    """
    # TODO: 在这里实现
    raise NotImplementedError


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


def _check_is_palindrome() -> None:
    assert is_palindrome("上海自来水来自海上") is True, "中文回文应为 True"
    assert is_palindrome("Never odd or even") is True, "忽略大小写与空格后是回文"
    assert is_palindrome("python") is False, 'is_palindrome("python") 应为 False'
    assert is_palindrome("") is True, "空串应为 True"


def _check_count_vowels() -> None:
    assert count_vowels("Hello World") == 3, 'count_vowels("Hello World") 应为 3'
    assert count_vowels("xyz") == 0, 'count_vowels("xyz") 应为 0'
    assert count_vowels("AEIOU") == 5, "大写元音也要算,应为 5"


def _check_capitalize_words() -> None:
    assert capitalize_words("hello   world") == "Hello World", (
        '多余空格要压成一个,应为 "Hello World"'
    )
    assert capitalize_words("  python is fun ") == "Python Is Fun", (
        '首尾空格要去掉,应为 "Python Is Fun"'
    )
    assert capitalize_words("") == "", "空串应返回空串"


def _check_mask_phone() -> None:
    assert mask_phone("13812345678") == "138****5678", (
        'mask_phone("13812345678") 应为 "138****5678"'
    )
    try:
        mask_phone("123")
    except ValueError:
        pass
    else:
        raise AssertionError("长度不是 11 位时应抛出 ValueError")
    try:
        mask_phone("1381234567a")
    except ValueError:
        pass
    else:
        raise AssertionError("含非数字字符时应抛出 ValueError")


def _check_caesar_shift() -> None:
    assert caesar_shift("abc", 1) == "bcd", 'caesar_shift("abc", 1) 应为 "bcd"'
    assert caesar_shift("xyz", 3) == "abc", "越过 z 要绕回 a,应为 abc"
    assert caesar_shift("Hello, World!", 13) == "Uryyb, Jbeyq!", (
        "大小写要保留,标点原样,应为 Uryyb, Jbeyq!"
    )
    assert caesar_shift("abc", -1) == "zab", "负数位移也要能绕回,应为 zab"


def _check_word_lengths() -> None:
    assert word_lengths("I love Python") == [1, 4, 6], "应为 [1, 4, 6]"
    assert word_lengths("") == [], "空串应返回空列表"


def _check_extract_domain() -> None:
    assert extract_domain("user@example.com") == "example.com", (
        'extract_domain("user@example.com") 应为 "example.com"'
    )
    try:
        extract_domain("bad.email")
    except ValueError:
        pass
    else:
        raise AssertionError("没有 @ 时应抛出 ValueError")
    try:
        extract_domain("a@b@c.com")
    except ValueError:
        pass
    else:
        raise AssertionError("有多个 @ 时应抛出 ValueError")


CHECKS = [
    _check_is_palindrome,
    _check_count_vowels,
    _check_capitalize_words,
    _check_mask_phone,
    _check_caesar_shift,
    _check_word_lengths,
    _check_extract_domain,
]


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_lists_tuples.py

exercises/day1/03_lists_tuples.py
"""Day 1 · Block 3a + 3b · 列表与元组

学习目标:掌握列表的增删改查、切片、嵌套,分清 sort() 原地改动返回 None
与 sorted() 返回新列表;会用 for / enumerate / zip / range 遍历;
真正理解"别名不是拷贝"、元组解包,以及 [[0] * 3] * 3 为什么是同一行。
用法:uv run python exercises/day1/03_lists_tuples.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK]。
      3a 先做前 5 题,3b 再做后 4 题。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

陷阱反例:b = a 不是复制;lst.sort() 返回 None;
for x in lst: lst.remove(x) 会漏删;[[0] * 3] * 3 三行是同一个列表。

预测练习(先在纸上写出输出,再到 REPL 验证;前三段贴到 Python Tutor 看动画):
>>> a = [1, 2, 3]
>>> b = a
>>> b.append(4)
>>> print(a)                 # ?
>>> c = a[:]
>>> c.append(5)
>>> print(a, c)              # ?
>>> x = [3, 1, 2].sort()
>>> print(x)                 # ?  ← sort() 返回 None
>>> t = (1,)
>>> print(type(t), type((1)))
>>> for i in range(1, 10, 3):
...     print(i, end=" ")
>>> grid = [[0] * 3] * 2
>>> grid[0][0] = 9
>>> print(grid)              # ?  ← 两行是同一个列表
"""


# ---- 任务 1 ----
def running_total(nums: list[int]) -> list[int]:
    """返回前缀和列表。

    例:running_total([1, 2, 3, 4]) -> [1, 3, 6, 10]
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 2 ----
def remove_duplicates_keep_order(items: list) -> list:
    """去重,并保持每个元素首次出现的顺序。

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


# ---- 任务 3 ----
def rotate(lst: list, k: int) -> list:
    """返回右旋 k 位的新列表;k 可以大于长度(用 %);空列表返回 []。

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


# ---- 任务 4 ----
def top_n(scores: list[int], n: int) -> list[int]:
    """返回降序排列的前 n 个分数,不能修改入参(用 sorted 而不是 sort)。

    例:top_n([70, 95, 88, 60], 2) -> [95, 88]
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 5 ----
def pair_up(names: list[str], scores: list[int]) -> list[tuple[str, int]]:
    """把姓名和分数用 zip 配成元组列表;两个列表长度不同时抛 ValueError。

    例:pair_up(["张三"], [90]) -> [("张三", 90)]
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 6 ----
def min_max(nums: list[int]) -> tuple[int, int]:
    """返回 (最小值, 最大值) 元组;空列表抛 ValueError。

    例:lo, hi = min_max([3, 1, 2])  # lo 是 1,hi 是 3
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 7 ----
def safe_copy_and_append(lst: list, item) -> list:
    """返回"追加了 item 的新列表",原列表保持不变。

    例:orig = [1, 2]; safe_copy_and_append(orig, 3) -> [1, 2, 3],orig 仍是 [1, 2]
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 8 ----
def transpose(matrix: list[list[int]]) -> list[list[int]]:
    """行列互换。

    例:transpose([[1, 2, 3], [4, 5, 6]]) -> [[1, 4], [2, 5], [3, 6]]
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 9 ----
def make_grid(rows: int, cols: int, fill: int = 0) -> list[list[int]]:
    """生成 rows × cols 的二维列表,每一行必须是彼此独立的对象。

    例:g = make_grid(2, 2); g[0][0] = 1 之后 g[1][0] 仍是 0
    """
    # TODO: 在这里实现
    raise NotImplementedError


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


def _check_running_total() -> None:
    assert running_total([1, 2, 3, 4]) == [1, 3, 6, 10], "应为 [1, 3, 6, 10]"
    assert running_total([]) == [], "空列表应返回空列表"


def _check_remove_duplicates_keep_order() -> None:
    assert remove_duplicates_keep_order([3, 1, 3, 2, 1]) == [3, 1, 2], (
        "去重要保持首次出现顺序,应为 [3, 1, 2]"
    )
    assert remove_duplicates_keep_order([]) == [], "空列表应返回空列表"


def _check_rotate() -> None:
    original = [1, 2, 3, 4, 5]
    assert rotate(original, 2) == [4, 5, 1, 2, 3], "右旋 2 位应为 [4, 5, 1, 2, 3]"
    assert original == [1, 2, 3, 4, 5], "rotate 不能修改入参"
    assert rotate([1, 2, 3], 4) == [3, 1, 2], "k 大于长度要先取模,应为 [3, 1, 2]"
    assert rotate([], 3) == [], "空列表应返回 [](注意不要除以 0)"


def _check_top_n() -> None:
    scores = [70, 95, 88, 60]
    assert top_n(scores, 2) == [95, 88], "应为 [95, 88]"
    assert scores == [70, 95, 88, 60], "top_n 不能修改入参(用 sorted 而不是 sort)"


def _check_pair_up() -> None:
    assert pair_up(["张三", "李四"], [90, 85]) == [("张三", 90), ("李四", 85)], (
        '应为 [("张三", 90), ("李四", 85)]'
    )
    assert pair_up([], []) == [], "两个空列表应返回空列表"
    try:
        pair_up(["张三"], [90, 85])
    except ValueError:
        pass
    else:
        raise AssertionError("两个列表长度不同时应抛出 ValueError")


def _check_min_max() -> None:
    lo, hi = min_max([3, 1, 2])
    assert (lo, hi) == (1, 3), "min_max([3, 1, 2]) 应为 (1, 3)"
    assert min_max([5]) == (5, 5), "只有一个元素时最小值等于最大值"
    try:
        min_max([])
    except ValueError:
        pass
    else:
        raise AssertionError("空列表应抛出 ValueError")


def _check_safe_copy_and_append() -> None:
    orig = [1, 2]
    new = safe_copy_and_append(orig, 3)
    assert new == [1, 2, 3], "新列表应为 [1, 2, 3]"
    assert orig == [1, 2], "原列表必须保持 [1, 2] 不变"
    assert new is not orig, "必须返回新列表,不能返回同一个对象"


def _check_transpose() -> None:
    assert transpose([[1, 2, 3], [4, 5, 6]]) == [[1, 4], [2, 5], [3, 6]], (
        "应为 [[1, 4], [2, 5], [3, 6]]"
    )
    assert transpose([[1]]) == [[1]], "1×1 矩阵转置还是自己"


def _check_make_grid() -> None:
    g = make_grid(2, 2)
    assert g == [[0, 0], [0, 0]], "make_grid(2, 2) 应为 [[0, 0], [0, 0]]"
    g[0][0] = 1
    assert g[1][0] == 0, "每行必须是独立对象(别用 [[0] * cols] * rows)"
    assert make_grid(1, 3, 7) == [[7, 7, 7]], "fill 参数应能改默认填充值"


CHECKS = [
    _check_running_total,
    _check_remove_duplicates_keep_order,
    _check_rotate,
    _check_top_n,
    _check_pair_up,
    _check_min_max,
    _check_safe_copy_and_append,
    _check_transpose,
    _check_make_grid,
]


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_dicts_sets.py

exercises/day1/04_dicts_sets.py
"""Day 1 · Block 4 · 字典与集合

学习目标:掌握字典的增删改查、in、get、keys/values/items,写出计数与分组两种套路;
知道字典的键必须可哈希,会处理"列表里装字典"这种嵌套结构;
会用集合去重与交集、并集、差集、对称差。
用法:uv run python exercises/day1/04_dicts_sets.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK]。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

陷阱反例:d["不存在"] 直接崩 KeyError,d.get("不存在") 返回 None;
列表不能当字典的键(unhashable);遍历字典时增删键会 RuntimeError。

预测练习(先在纸上写出输出,再到 REPL 验证):
>>> d = {"a": 1}
>>> d["b"] = 2
>>> print(len(d), d.get("z"), d.get("z", 0))
>>> for k in d: print(k, d[k])
>>> for k, v in d.items(): print(k, v)
>>> print({1, 2, 2, 3}, len({1, 2, 2, 3}))
>>> print({1, 2, 3} & {2, 3, 4}, {1, 2, 3} - {2})
>>> # d["z"]          → KeyError
>>> # {[1, 2]: "x"}   → TypeError: unhashable type: 'list'
"""


# ---- 任务 1 ----
def word_frequency(text: str) -> dict[str, int]:
    """统计词频:先转小写,按空白切分,每个词去掉首尾的 .,!? 再用 get 计数。

    例:word_frequency("the cat and the hat.")
        -> {"the": 2, "cat": 1, "and": 1, "hat": 1}
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 2 ----
def invert_dict(d: dict) -> dict:
    """键值互换。

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


# ---- 任务 3 ----
def merge_scores(a: dict[str, int], b: dict[str, int]) -> dict[str, int]:
    """合并两个分数字典,同键相加;不能修改任何一个入参。

    例:merge_scores({"x": 1, "y": 2}, {"y": 3}) -> {"x": 1, "y": 5}
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 4 ----
def group_by_first_letter(words: list[str]) -> dict[str, list[str]]:
    """按首字母分组(用 setdefault 或 defaultdict)。

    例:group_by_first_letter(["apple", "avocado", "banana"])
        -> {"a": ["apple", "avocado"], "b": ["banana"]}
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 5 ----
def common_and_unique(a: list, b: list) -> tuple[set, set]:
    """返回 (两边都有的元素集合, 只在一边出现的元素集合)。

    例:common_and_unique([1, 2, 3], [2, 3, 4]) -> ({2, 3}, {1, 4})
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 6 ----
def top_student(grades: dict[str, list[int]]) -> str:
    """返回平均分最高的学生姓名;空字典抛 ValueError。

    例:top_student({"张三": [90, 80], "李四": [95, 85]}) -> "李四"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 7 ----
def count_by_category(records: list[dict], key: str) -> dict[str, int]:
    """列表里装字典:按某个字段的取值计数。

    例:count_by_category([{"type": "水果"}, {"type": "蔬菜"}], "type")
        -> {"水果": 1, "蔬菜": 1}
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 8 ----
def unique_sorted(items: list[int]) -> list[int]:
    """去重后升序排列。

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


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


def _check_word_frequency() -> None:
    assert word_frequency("the cat and the hat.") == {
        "the": 2,
        "cat": 1,
        "and": 1,
        "hat": 1,
    }, '句尾的 "." 要去掉,"the" 应计 2 次'
    assert word_frequency("") == {}, "空串应返回空字典"
    assert word_frequency("Hi hi HI!") == {"hi": 3}, "要先转小写再计数,应为 {'hi': 3}"


def _check_invert_dict() -> None:
    assert invert_dict({"a": 1, "b": 2}) == {1: "a", 2: "b"}, "应为 {1: 'a', 2: 'b'}"
    assert invert_dict({}) == {}, "空字典应返回空字典"


def _check_merge_scores() -> None:
    a = {"x": 1, "y": 2}
    b = {"y": 3, "z": 4}
    assert merge_scores(a, b) == {"x": 1, "y": 5, "z": 4}, (
        "同键要相加,应为 {'x': 1, 'y': 5, 'z': 4}"
    )
    assert a == {"x": 1, "y": 2}, "不能修改第一个入参"
    assert b == {"y": 3, "z": 4}, "不能修改第二个入参"


def _check_group_by_first_letter() -> None:
    assert group_by_first_letter(["apple", "avocado", "banana"]) == {
        "a": ["apple", "avocado"],
        "b": ["banana"],
    }, "分组结果应为 {'a': ['apple', 'avocado'], 'b': ['banana']}"
    assert group_by_first_letter([]) == {}, "空列表应返回空字典"


def _check_common_and_unique() -> None:
    assert common_and_unique([1, 2, 3], [2, 3, 4]) == ({2, 3}, {1, 4}), (
        "应为 ({2, 3}, {1, 4}),第二个是对称差"
    )
    assert common_and_unique([], []) == (set(), set()), "两个空列表应返回两个空集合"


def _check_top_student() -> None:
    assert top_student({"张三": [90, 80], "李四": [95, 85]}) == "李四", (
        '平均分最高的是 "李四"'
    )
    try:
        top_student({})
    except ValueError:
        pass
    else:
        raise AssertionError("空字典应抛出 ValueError")


def _check_count_by_category() -> None:
    records = [
        {"name": "苹果", "type": "水果"},
        {"name": "白菜", "type": "蔬菜"},
        {"name": "梨", "type": "水果"},
    ]
    assert count_by_category(records, "type") == {"水果": 2, "蔬菜": 1}, (
        "应为 {'水果': 2, '蔬菜': 1}"
    )
    assert count_by_category([], "type") == {}, "空列表应返回空字典"


def _check_unique_sorted() -> None:
    assert unique_sorted([3, 1, 3, 2]) == [1, 2, 3], "应为 [1, 2, 3]"
    assert unique_sorted([]) == [], "空列表应返回空列表"


CHECKS = [
    _check_word_frequency,
    _check_invert_dict,
    _check_merge_scores,
    _check_group_by_first_letter,
    _check_common_and_unique,
    _check_top_student,
    _check_count_by_category,
    _check_unique_sorted,
]


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_functions.py

exercises/day1/05_functions.py
"""Day 1 · Block 5a + 5b · 函数与作用域

学习目标:会用 def 定义函数、写 docstring、返回多个值,分清位置参数与关键字参数;
理解局部变量与全局变量、可变默认参数为什么是坑;
会写类型注解、*args 和仅关键字参数,并把一段长代码拆成几个小函数。
用法:uv run python exercises/day1/05_functions.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK]。
      5a 先做前 4 题,5b 再做后 5 题。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

陷阱反例:可变默认参数在定义时只创建一次;函数没写 return 时返回 None;
在函数里给全局变量赋值会变成局部变量;别把变量命名成 list/str/sum/input。

预测练习(先在纸上写出输出,再到 REPL 验证;贴到 Python Tutor 看栈帧):
>>> total = 0
>>> def add(n):
...     total = total + n      # ?  ← UnboundLocalError
...     return total
>>>
>>> x = 10
>>> def show():
...     print(x)               # 只读全局变量没问题
>>> show()
>>>
>>> def f(item, lst=[]):       # 默认值只在定义时创建一次!
...     lst.append(item)
...     return lst
>>> print(f(1), f(2))          # ?
>>>
>>> def g(item, lst=None):
...     if lst is None:
...         lst = []
...     lst.append(item)
...     return lst
>>> print(g(1), g(2))          # ?
"""


# ---- 任务 1 ----
def safe_divide(a: float, b: float) -> float | None:
    """做除法;除数为 0 时返回 None 而不是抛异常。

    例:safe_divide(10, 4) -> 2.5;safe_divide(1, 0) -> None
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 2 ----
def clamp(x: int, lo: int = 0, hi: int = 100) -> int:
    """把 x 夹在 [lo, hi] 区间内。

    例:clamp(150) -> 100;clamp(7, lo=1, hi=5) -> 5
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 3 ----
def stats(nums: list[float]) -> tuple[float, float, float]:
    """返回 (最小值, 最大值, 平均值);空列表抛 ValueError。

    例:stats([2, 4, 6]) -> (2, 6, 4.0)
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 4 ----
def apply_discount(price: float, percent: float = 10) -> float:
    """按百分比打折,结果 round 到 2 位小数。

    例:apply_discount(100) -> 90.0
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 5 ----
def append_item(item, items: list | None = None) -> list:
    """把 item 追加进 items 并返回;不传 items 时每次都要是全新的列表。

    这是可变默认参数陷阱的正确写法:默认值写 None,进函数再新建列表。
    例:append_item(1) -> [1],紧接着 append_item(2) -> [2]
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 6 ----
def greet(name: str, *, greeting: str = "你好") -> str:
    """打招呼;greeting 是仅关键字参数,只能写成 greeting="..."。

    例:greet("小明") -> "你好,小明!"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 7 ----
def summarize(*nums: float) -> dict[str, float]:
    """接收任意多个数字,返回 {"count", "total", "avg"};没有参数时 avg 为 0.0。

    例:summarize(1, 2, 3) -> {"count": 3, "total": 6, "avg": 2.0}
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 8 ----
def apply_twice(func, x):
    """把函数 func 连用两次,即返回 func(func(x))。

    例:apply_twice(lambda v: v * 2, 3) -> 12
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 9(拆分练习:必须写满下面三个函数)----
def count_events(events: list[str]) -> dict[str, int]:
    """统计每个事件名出现的次数。

    例:count_events(["登录", "下单", "登录"]) -> {"登录": 2, "下单": 1}
    """
    # TODO: 在这里实现
    raise NotImplementedError


def format_line(name: str, n: int) -> str:
    """把一条统计结果排成一行文字,格式是 f"{name}: {n} 次"。

    例:format_line("登录", 2) -> "登录: 2 次"
    """
    raise NotImplementedError


def event_report(events: list[str]) -> str:
    """调用上面两个函数,按事件名升序拼成多行报表;空列表返回空串。

    例:event_report(["登录", "下单", "登录"]) -> "下单: 1 次\\n登录: 2 次"
    """
    raise NotImplementedError


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


def _check_safe_divide() -> None:
    assert safe_divide(10, 4) == 2.5, "safe_divide(10, 4) 应为 2.5"
    assert safe_divide(1, 0) is None, "除数为 0 时应返回 None"


def _check_clamp() -> None:
    assert clamp(150) == 100, "clamp(150) 应为 100"
    assert clamp(-5) == 0, "clamp(-5) 应为 0"
    assert clamp(50) == 50, "clamp(50) 应为 50"
    assert clamp(7, lo=1, hi=5) == 5, "clamp(7, lo=1, hi=5) 应为 5"


def _check_stats() -> None:
    assert stats([2, 4, 6]) == (2, 6, 4.0), "stats([2, 4, 6]) 应为 (2, 6, 4.0)"
    assert stats([5]) == (5, 5, 5.0), "只有一个元素时三个值都由它决定"
    try:
        stats([])
    except ValueError:
        pass
    else:
        raise AssertionError("空列表应抛出 ValueError")


def _check_apply_discount() -> None:
    assert apply_discount(100) == 90.0, "默认打 9 折,apply_discount(100) 应为 90.0"
    assert apply_discount(59.9, 15) == 50.91, (
        "59.9 打 85 折是 50.915,round 到 2 位后是 50.91"
    )


def _check_append_item() -> None:
    assert append_item(1) == [1], "append_item(1) 应为 [1]"
    assert append_item(2) == [2], "第二次调用还应是 [2](可变默认参数陷阱)"
    existing = [0]
    assert append_item(1, existing) == [0, 1], "传入列表时应追加到它后面"


def _check_greet() -> None:
    assert greet("小明") == "你好,小明!", 'greet("小明") 应为 "你好,小明!"'
    assert greet("Tom", greeting="Hi") == "Hi,Tom!", (
        'greet("Tom", greeting="Hi") 应为 "Hi,Tom!"'
    )
    try:
        greet("A", "B")
    except TypeError:
        pass
    else:
        raise AssertionError("greeting 是仅关键字参数,位置传参应抛出 TypeError")


def _check_summarize() -> None:
    assert summarize(1, 2, 3) == {"count": 3, "total": 6, "avg": 2.0}, (
        "summarize(1, 2, 3) 应为 {'count': 3, 'total': 6, 'avg': 2.0}"
    )
    assert summarize() == {"count": 0, "total": 0, "avg": 0.0}, (
        "没有参数时 avg 应为 0.0,不能除以 0"
    )


def _check_apply_twice() -> None:
    assert apply_twice(lambda v: v * 2, 3) == 12, "翻倍两次,3 应变成 12"
    assert apply_twice(str.strip, "  x  ") == "x", "函数可以当参数传进来"


def _check_event_report() -> None:
    assert count_events(["登录", "下单", "登录"]) == {"登录": 2, "下单": 1}, (
        "count_events 应为 {'登录': 2, '下单': 1}"
    )
    assert format_line("登录", 2) == "登录: 2 次", 'format_line 应为 "登录: 2 次"'
    assert event_report(["登录", "下单", "登录"]) == "下单: 1 次\n登录: 2 次", (
        "报表要按事件名升序,两行之间用 \\n 连接"
    )
    assert event_report([]) == "", "空列表应返回空串"


CHECKS = [
    _check_safe_divide,
    _check_clamp,
    _check_stats,
    _check_apply_discount,
    _check_append_item,
    _check_greet,
    _check_summarize,
    _check_apply_twice,
    _check_event_report,
]


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_debugging_bughunt.py

exercises/day1/06_debugging_bughunt.py
"""Day 1 · Block 6 · 调试:读报错、断点单步、找 bug

学习目标:会读 Traceback(最后一行看异常名,往上找自己文件的行号);
认识 ZeroDivisionError / TypeError / ValueError / IndexError / KeyError 各来自哪里;
会用 print(f"{x=}") 和 VS Code 的断点、F10 单步、变量面板定位问题。
用法:uv run python exercises/day1/06_debugging_bughunt.py
做法:这里的 7 个函数都写完了,但每个都有 bug,下面的测试是正确的。
      每个函数先在 VS Code 里打断点单步跟到出错处,在纸上写下
      "现象 → 原因"两句话,再动手修;不要一上来就凭直觉改代码。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

复盘问题:TypeError 和 ValueError 的区别是什么?
IndexError 和 KeyError 分别来自哪种数据结构?
"""


# ---- bug 1:空列表会怎样?----
def average(nums: list[float]) -> float:
    """求平均值;空列表应返回 0.0。

    例:average([1, 2, 3]) -> 2.0
    """
    return sum(nums) / len(nums)


# ---- bug 2:全是负数会怎样?----
def find_max(nums: list[int]) -> int:
    """返回列表中的最大值。

    例:find_max([3, 7, 1]) -> 7
    """
    best = 0
    for n in nums:
        if n > best:
            best = n
    return best


# ---- bug 3:一边遍历一边删除会怎样?----
def remove_negatives(nums: list[int]) -> list[int]:
    """去掉所有负数,返回剩下的数。

    例:remove_negatives([1, -2, -3, 4]) -> [1, 4]
    """
    for n in nums:
        if n < 0:
            nums.remove(n)
    return nums


# ---- bug 4:闰年规则少了一条 ----
def is_leap_year(year: int) -> bool:
    """判断闰年:4 年一闰,百年不闰,四百年再闰。

    例:is_leap_year(2000) -> True
    """
    return year % 4 == 0 and year % 100 != 0


# ---- bug 5:range 的边界差一 ----
def count_down(n: int) -> list[int]:
    """从 n 一直倒数到 0(含 0)。

    例:count_down(3) -> [3, 2, 1, 0]
    """
    return list(range(n, 0, -1))


# ---- bug 6:默认参数为什么会"记住"上一次的结果?----
def add_student(name: str, roster: list[str] = []) -> list[str]:  # noqa: B006
    """把 name 加进名单并返回;不传 roster 时应该每次都是新名单。

    例:add_student("张三") -> ["张三"]
    """
    roster.append(name)
    return roster


# ---- bug 7:字符串和整数不能相加 ----
def next_year_age(text: str) -> int:
    """输入是字符串形式的年龄,返回明年的年龄(整数)。

    例:next_year_age("17") -> 18
    """
    return text + 1


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


def _check_average() -> None:
    assert average([1, 2, 3]) == 2.0, "average([1, 2, 3]) 应为 2.0"
    assert average([]) == 0.0, "average([]) 应为 0.0,不能除以 0"


def _check_find_max() -> None:
    assert find_max([3, 7, 1]) == 7, "find_max([3, 7, 1]) 应为 7"
    assert find_max([-5, -2, -9]) == -2, "全是负数时应为 -2,初始值不能写 0"


def _check_remove_negatives() -> None:
    assert remove_negatives([1, -2, -3, 4]) == [1, 4], "应为 [1, 4],两个负数都要去掉"
    assert remove_negatives([-1, -1]) == [], "全是负数时应返回空列表"


def _check_is_leap_year() -> None:
    assert is_leap_year(2000) is True, "2000 能被 400 整除,是闰年"
    assert is_leap_year(1900) is False, "1900 是百年但不能被 400 整除,不是闰年"
    assert is_leap_year(2024) is True, "2024 能被 4 整除且不是百年,是闰年"
    assert is_leap_year(2023) is False, "2023 不是闰年"


def _check_count_down() -> None:
    assert count_down(3) == [3, 2, 1, 0], "应为 [3, 2, 1, 0],要含 0"
    assert count_down(0) == [0], "count_down(0) 应为 [0]"


def _check_add_student() -> None:
    first = add_student("张三")
    second = add_student("李四")
    assert first == ["张三"], '第一次调用的结果应始终是 ["张三"]'
    assert second == ["李四"], '第二次调用应是全新名单 ["李四"]'
    existing = ["王五"]
    assert add_student("赵六", existing) == ["王五", "赵六"], "显式传名单时应追加"


def _check_next_year_age() -> None:
    assert next_year_age("17") == 18, 'next_year_age("17") 应为整数 18'
    assert next_year_age("0") == 1, 'next_year_age("0") 应为整数 1'


CHECKS = [
    _check_average,
    _check_find_max,
    _check_remove_negatives,
    _check_is_leap_year,
    _check_count_down,
    _check_add_student,
    _check_next_year_age,
]


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()

07_mini_project_grades.py

exercises/day1/07_mini_project_grades.py
"""Day 1 · Block 7 · 综合小程序:成绩单分析

学习目标:把字符串切分、列表、字典、函数拆分和 f-string 对齐串成一个完整程序;
练习"先写 parse_report 并打印结果确认,再写下一个函数"的增量开发节奏;
每个函数 ≤ 15 行,不用全局变量传数据,数据一律靠参数和返回值流动。
用法:uv run python exercises/day1/07_mini_project_grades.py
做法:把每个 TODO 换成你的实现,反复运行直到全部 [OK],最后能打印出报表。
      卡住 8 分钟以上再按 ai-guide.md 的模板提问;拿到提示后自己重写。

关于 round:83.25 保留 1 位小数得到的是 83.2 而不是 83.3。
这不是 bug,是"银行家舍入":round 遇到正好一半时向偶数靠,
再加上 83.25 在二进制里其实略小于 83.25,所以结果是 83.2。

预测练习(先在纸上写出输出,再到 REPL 验证):
>>> print(round(83.25, 1), round(0.5), round(1.5), round(2.5))
>>> print(round(2.675, 2))
>>> print("a,b,c".split(","), "1,2".split(",")[1], int("07"))
"""

RAW = """姓名,语文,数学,英语
张三,85,92,78
李四,90,88,95
王五,70,65,80
赵六,88,79,91
"""


# ---- 任务 1 ----
def parse_report(text: str) -> list[dict]:
    """把多行文本解析成"每行一个字典";首行是表头,分数转成 int,空行跳过。

    例:parse_report(RAW)[0]
        -> {"姓名": "张三", "语文": 85, "数学": 92, "英语": 78}
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 2 ----
def student_average(record: dict) -> float:
    """求一个学生所有数值字段的平均分,round 到 1 位小数。

    例:student_average({"姓名": "张三", "语文": 85, "数学": 92, "英语": 78}) -> 85.0
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 3 ----
def subject_averages(records: list[dict]) -> dict[str, float]:
    """求各科平均分,round 到 1 位小数。

    例:subject_averages(parse_report(RAW))
        -> {"语文": 83.2, "数学": 81.0, "英语": 86.0}
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 4 ----
def letter_grade(score: float) -> str:
    """分数转等级:>=90 是 A,>=80 是 B,>=70 是 C,>=60 是 D,其余 F。

    例:letter_grade(71.7) -> "C"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 5 ----
def grade_distribution(records: list[dict]) -> dict[str, int]:
    """按每个学生的平均分统计等级人数;A 到 F 五个键都必须存在。

    例:grade_distribution(parse_report(RAW))
        -> {"A": 1, "B": 2, "C": 1, "D": 0, "F": 0}
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 6 ----
def top_students(records: list[dict], n: int = 3) -> list[tuple[str, float]]:
    """按平均分从高到低取前 n 名,返回 (姓名, 平均分) 的列表。

    例:top_students(parse_report(RAW))[0] -> ("李四", 91.0)
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 7 ----
def build_report(records: list[dict]) -> str:
    """拼出多行报表字符串。

    至少要包含:标题行;每人一行 f"{姓名:<4}{平均:>6.1f}  {等级}";
    各科平均;等级分布。
    例:build_report(parse_report(RAW)) 里能找到 "李四"、"91.0"、"语文"、"83.2"
    """
    # TODO: 在这里实现
    raise NotImplementedError


# ---- 任务 8 ----
def main() -> None:
    """打印完整报表,即 print(build_report(parse_report(RAW)))。"""
    # TODO: 在这里实现
    raise NotImplementedError


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


def _check_parse_report() -> None:
    records = parse_report(RAW)
    assert len(records) == 4, f"应解析出 4 条记录,实际 {len(records)} 条"
    assert records[0] == {"姓名": "张三", "语文": 85, "数学": 92, "英语": 78}, (
        "第一条应为 {'姓名': '张三', '语文': 85, '数学': 92, '英语': 78}"
    )
    assert isinstance(records[0]["语文"], int), "分数必须转成 int,不能留成字符串"
    assert len(parse_report("姓名,语文\n\n张三,85\n\n")) == 1, "空行要跳过"


def _check_student_average() -> None:
    records = parse_report(RAW)
    assert student_average(records[0]) == 85.0, "张三的平均分应为 85.0"
    assert student_average(records[2]) == 71.7, "王五的平均分应为 71.7"


def _check_subject_averages() -> None:
    assert subject_averages(parse_report(RAW)) == {
        "语文": 83.2,
        "数学": 81.0,
        "英语": 86.0,
    }, "各科平均应为 {'语文': 83.2, '数学': 81.0, '英语': 86.0}"


def _check_letter_grade() -> None:
    assert letter_grade(91.0) == "A", "91.0 应为 A"
    assert letter_grade(85.0) == "B", "85.0 应为 B"
    assert letter_grade(71.7) == "C", "71.7 应为 C"
    assert letter_grade(60.0) == "D", "60.0 应为 D"
    assert letter_grade(59.9) == "F", "59.9 应为 F"


def _check_grade_distribution() -> None:
    assert grade_distribution(parse_report(RAW)) == {
        "A": 1,
        "B": 2,
        "C": 1,
        "D": 0,
        "F": 0,
    }, "等级分布应为 {'A': 1, 'B': 2, 'C': 1, 'D': 0, 'F': 0},五个键都要有"


def _check_top_students() -> None:
    assert top_students(parse_report(RAW)) == [
        ("李四", 91.0),
        ("赵六", 86.0),
        ("张三", 85.0),
    ], "前三名应为 [('李四', 91.0), ('赵六', 86.0), ('张三', 85.0)]"
    assert len(top_students(parse_report(RAW), 1)) == 1, "n=1 时只返回 1 条"


def _check_build_report() -> None:
    report = build_report(parse_report(RAW))
    for piece in ("李四", "91.0", "A", "语文", "83.2"):
        assert piece in report, f"报表里应包含 {piece!r}"
    assert "\n" in report, "报表应该是多行字符串"


CHECKS = [
    _check_parse_report,
    _check_student_average,
    _check_subject_averages,
    _check_letter_grade,
    _check_grade_distribution,
    _check_top_students,
    _check_build_report,
]


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()
    print()
    try:
        main()
    except NotImplementedError:
        print("[提示] 还有函数没实现,等上面全部变成 [OK] 再回来看报表。")