跳转至

Day 1(周六 09-05):地基——从"会写几行"到"会用数据结构和函数"

今日目标:晚上 21:30 前,exercises/day1/ 的 8 个文件全部 [OK],07_mini_project_grades.py 能打印出一份成绩报表;用 VS Code 断点单步跟过至少一个函数;能不看资料说出"列表别名"和"可变默认参数"为什么会坑人。

今日节奏:8 个 50+10 分钟块 + 45 分钟环境块 + 60 分钟综合块。从零写代码的时间约 4.5 小时,其余是读、预测、追踪、找 bug。钟点是参考,晚起就整体平移;宁可砍掉 Block 7 的后半段,也不要熬夜。

钟点 内容
09:00–09:15 准备 README.md 第 0 节 + ai-guide.md 七条规则;把今天目标抄在纸上
09:15–10:00 Block 0 环境与命令行(45 分钟)
10:10–11:00 Block 1 基础类型、运算、f-string
11:10–12:00 Block 2 字符串
12:00–13:30 午餐 + 闭眼 20–30 分钟 不看屏幕
13:30–14:20 Block 3a 列表与元组(一):增删改查、切片、遍历
14:30–15:20 Block 3b 列表与元组(二):别名与拷贝(Python Tutor)、嵌套、元组解包
15:30–16:20 Block 4 字典与集合
16:20–16:40 加餐 + 散步
16:40–17:30 Block 5a 函数(一):定义、参数、返回值、拆分
17:40–18:30 Block 5b 函数(二):作用域、可变默认参数、*args、类型注解
18:30–19:30 晚餐
19:30–20:20 Block 6 调试:读报错、断点单步、找 bug
20:30–21:30 Block 7 综合小程序:成绩单分析(60–80 行)
21:30–22:00 复盘 写学习日志(checklist.md);预览明早晨测题目范围;22:30 前睡

Block 0(45 分钟):环境与命令行

知识点:A 在终端运行脚本与 REPL;A VS Code 打开项目、选解释器、运行;B uv 与 pyproject.toml;B ruff。详细来源见 research/02

步骤(每步有预期结果,不符合就停下来解决,不要带着坏环境往下走)

  1. 关闭 Windows 商店别名(5 分钟):Win + I → 搜索"应用执行别名" → 关掉 python.exepython3.exe。预期:以后在终端输入 python 不会弹出 Microsoft Store。
  2. 确认 uv 与 Python(5 分钟):打开 PowerShell(推荐 Windows Terminal),输入 uv --versionuv python list --only-installed。预期:看到 uv 版本号和一个 3.14.x。若提示 uv 不是命令:运行 powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex",关掉终端重开,再 uv python install 3.14
  3. VS Code 与扩展(10 分钟):安装 VS Code;扩展面板搜索并安装 ms-python.python(会带上 Pylance 与 ms-python.debugpy)、charliermarsh.ruff、中文语言包 MS-CEINTL.vscode-language-pack-zh-hans。用 VS Code "打开文件夹" 打开 learn-python
  4. 初始化并运行(10 分钟):在 VS Code 终端(Ctrl+`)中:
    uv sync                                    # 创建 .venv,装 pytest 与 ruff
    uv run python exercises/day1/00_env_check.py
    
    预期:打印 Python 版本(3.14.x)、当前目录、编码信息和一行 [OK] 环境正常。VS Code 右下角应显示解释器为 .venv;若没有,Ctrl+Shift+P → "Python: Select Interpreter" → 选 .venv\Scripts\python.exe
  5. REPL 五分钟uv run python 进入交互式解释器(3.14 有彩色提示和语法高亮),逐行试:2 ** 100"你好" * 3len("你好")、按 ↑ 翻历史、多行输入一个 for 循环、输入 exit 退出。预期:直接输入 exit(不用括号)能退出。
  6. 命令行三板斧(5 分钟):cd exercisesls(PowerShell 里等于 dir)→ cd ..;Tab 键补全路径;用 uv run python -c "while True: pass" 制造死循环,再用 Ctrl+C 中断。
  7. ruff 与断点预演(5 分钟):在 00_env_check.py 里故意把一行写成 import sys,os,保存(若已配置保存即格式化会自动修正),再运行 uv run ruff check exercises/day1/00_env_check.py 看提示。点击行号左侧打一个红点断点,按 F5 → 选 "Python File" → 程序停在断点,看左侧"变量"面板;F10 走一行;Shift+F5 停止。

不做的事:不手动创建/激活虚拟环境(uv run 会自动处理);不装 Anaconda;不升级到 3.14.7。 常见坑速查research/02 第四节(商店别名、Activate.ps1 执行策略、GBK 编码、路径反斜杠、No Python interpreter is selected)。


Block 1(50 分钟):基础类型、运算、f-string

知识点:A int/float/bool/str/None;A ///%**;A input() 返回字符串与类型转换;A 比较与链式比较、and/or/not 短路;A 真值;A f-string:.2f:>8:02d{x=});B round/abs/divmod;C % 格式化。

阅读(10 分钟):廖雪峰《数据类型和变量》《字符串和编码》(只读格式化部分);官方教程 3.1.1 数字;Automate 3e Ch1 的 "Math Operators" 与 Ch1 末的 f-string。

预测(写在纸上,再到 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%}")

练习exercises/day1/01_types_and_fstrings.py

函数 行为 测试用例(自测断言)
celsius_to_fahrenheit(c: float) -> float c * 9 / 5 + 32round(..., 1) 0→32.0100→212.0-40→-40.036.6→97.9
describe_number(n: int) -> str 返回 "零"/"正奇数"/"正偶数"/"负奇数"/"负偶数" 0→"零"4→"正偶数"7→"正奇数"-3→"负奇数"-8→"负偶数"
seconds_to_hms(total: int) -> str //% 拆成时分秒,f"{h:02d}:{m:02d}:{s:02d}" 0→"00:00:00"3661→"01:01:01"86399→"23:59:59"90061→"25:01:01"
format_receipt(item: str, price: float, qty: int) -> str 精确格式 f"{item:<10}{qty:>3} x {price:>8.2f} = {price * qty:>9.2f}" ("苹果", 3.5, 4)"苹果" + " " * 8 + " 4 x 3.50 = 14.00"("pen", 1.25, 10)"pen 10 x 1.25 = 12.50"
is_close(a: float, b: float, tol: float = 1e-9) -> bool abs(a - b) < tol is_close(0.1 + 0.2, 0.3) 为 True;is_close(1.0, 1.1) 为 False
average(nums: list[float]) -> float 空列表返回 0.0,否则 sum / len [1, 2, 3]→2.0[]→0.0[2.5]→2.5

陷阱反例(在文件注释里出现,你要能解释):input() 得到的 "3" + 1TypeError0.1 + 0.2 != 0.3(二进制浮点,官方教程第 15 章);/ 的结果永远是 float,10 / 25.0

复盘问题// 对负数为什么是 -4 而不是 -3?什么时候该用 round,什么时候该用 is_close


Block 2(50 分钟):字符串

知识点:A 索引/负索引/切片/[::-1];A 不可变;A strip/split/join/replace/upper/lower/startswith/endswith/find/count/isdigit/isalpha;A 转义、r"..."、三引号;B ord/chr、Unicode 与 UTF-8;A(陷阱)方法返回新串。

阅读:廖雪峰《字符串和编码》全篇、《切片》;官方教程 3.1.2 文本;Automate 3e Ch8 前半。

预测

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(索引越界报错,切片越界不报错)

练习exercises/day1/02_strings.py

函数 行为 测试用例
is_palindrome(s: str) -> bool 忽略大小写与空格 "上海自来水来自海上"→True"Never odd or even"→True"python"→False""→True
count_vowels(s: str) -> int 统计 a/e/i/o/u(不分大小写) "Hello World"→3"xyz"→0"AEIOU"→5
capitalize_words(s: str) -> str split() + join(),每个词首字母大写、多余空白压成单个空格(不要用 .title() "hello world"→"Hello World"" python is fun "→"Python Is Fun"""→""
mask_phone(phone: str) -> str 11 位手机号中间 4 位换 *;长度不为 11 或含非数字 → raise ValueError "13812345678"→"138****5678""123"ValueError
caesar_shift(s: str, k: int) -> str 只移动英文字母、保留大小写、其他字符原样(用 ord/chr% ("abc", 1)→"bcd"("xyz", 3)→"abc"("Hello, World!", 13)→"Uryyb, Jbeyq!"("abc", -1)→"zab"
word_lengths(sentence: str) -> list[int] 每个词的长度 "I love Python"→[1, 4, 6]""→[]
extract_domain(email: str) -> str @ 后面的部分;没有 @ 或有多个 @ValueError "user@example.com"→"example.com""bad.email"ValueError

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


Block 3a + 3b(2 × 50 分钟):列表与元组

知识点:A 创建/索引/切片/嵌套;A append/extend/insert/pop/remove/index/count;A sort() 原地返回 None vs sorted();A for / enumerate / zip / rangeA 别名与拷贝;A 元组与解包;A(陷阱)遍历时修改;B [[0]*3]*3 陷阱;B 深拷贝。

阅读:廖雪峰《使用 list 和 tuple》《切片》《迭代》;官方教程 5.1、5.3;Automate 3e Ch6(尤其 "References" 一节)。

预测(3b 开头,把前三段贴到 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)              # ?  ← 两行是同一个列表

练习exercises/day1/03_lists_tuples.py(3a 做前 5 题,3b 做后 4 题)

函数 行为 测试用例
running_total(nums: list[int]) -> list[int] 前缀和 [1, 2, 3, 4]→[1, 3, 6, 10][]→[]
remove_duplicates_keep_order(items: list) -> list 去重且保持首次出现顺序 [3, 1, 3, 2, 1]→[3, 1, 2][]→[]
rotate(lst: list, k: int) -> list 返回新列表,右旋 k 位;k 可大于长度(用 %);空列表返回 [] ([1, 2, 3, 4, 5], 2)→[4, 5, 1, 2, 3]([1, 2, 3], 4)→[3, 1, 2]([], 3)→[]
top_n(scores: list[int], n: int) -> list[int] 降序前 n 个,不能修改入参(用 sorted ([70, 95, 88, 60], 2)→[95, 88];调用后原列表仍为 [70, 95, 88, 60]
pair_up(names: list[str], scores: list[int]) -> list[tuple[str, int]] zip;长度不同 → ValueError (["张三", "李四"], [90, 85])→[("张三", 90), ("李四", 85)]
min_max(nums: list[int]) -> tuple[int, int] 返回 (最小, 最大);空 → ValueError lo, hi = min_max([3, 1, 2])1, 3
safe_copy_and_append(lst: list, item) -> list 返回追加后的新列表,原列表不变 orig = [1, 2]; new = safe_copy_and_append(orig, 3)new == [1, 2, 3]orig == [1, 2]
transpose(matrix: list[list[int]]) -> list[list[int]] 行列互换 [[1, 2, 3], [4, 5, 6]]→[[1, 4], [2, 5], [3, 6]]
make_grid(rows: int, cols: int, fill: int = 0) -> list[list[int]] 每行必须是独立对象 g = make_grid(2, 2); g[0][0] = 1g[1][0] == 0

陷阱反例b = a 不是复制;lst.sort() 返回 Nonefor x in lst: lst.remove(x) 会漏删;[[0]*3]*3


Block 4(50 分钟):字典与集合

知识点:A 增删改查、ingetkeys/values/items;A 计数模式与分组模式;A 键必须可哈希;A 嵌套结构(列表里装字典);B 集合运算;B Counter/defaultdict;B 字典/集合推导式。

阅读:廖雪峰《使用 dict 和 set》;官方教程 5.4–5.6;Automate 3e Ch7。

预测

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'

练习exercises/day1/04_dicts_sets.py

函数 行为 测试用例
word_frequency(text: str) -> dict[str, int] 转小写、按空白切分、每个词 strip(".,!?")、用 get 计数 "the cat and the hat."→{"the": 2, "cat": 1, "and": 1, "hat": 1}""→{}
invert_dict(d: dict) -> dict 键值互换 {"a": 1, "b": 2}→{1: "a", 2: "b"}
merge_scores(a: dict[str, int], b: dict[str, int]) -> dict[str, int] 同键相加,不修改入参 ({"x": 1, "y": 2}, {"y": 3, "z": 4})→{"x": 1, "y": 5, "z": 4}
group_by_first_letter(words: list[str]) -> dict[str, list[str]] setdefaultdefaultdict 分组 ["apple", "avocado", "banana"]→{"a": ["apple", "avocado"], "b": ["banana"]}
common_and_unique(a: list, b: list) -> tuple[set, set] (交集, 对称差) ([1, 2, 3], [2, 3, 4])→({2, 3}, {1, 4})
top_student(grades: dict[str, list[int]]) -> str 平均分最高者;空字典 → ValueError {"张三": [90, 80], "李四": [95, 85]}→"李四"
count_by_category(records: list[dict], key: str) -> dict[str, int] 按某字段计数(列表里装字典) ([{"name": "苹果", "type": "水果"}, {"name": "白菜", "type": "蔬菜"}, {"name": "梨", "type": "水果"}], "type")→{"水果": 2, "蔬菜": 1}
unique_sorted(items: list[int]) -> list[int] 去重后升序 [3, 1, 3, 2]→[1, 2, 3]

陷阱反例d["不存在"] 崩溃 vs d.get(...);列表不能当键;遍历字典时增删键会 RuntimeError


Block 5a + 5b(2 × 50 分钟):函数与作用域

知识点:A def/参数/return/多值返回/docstring;A 位置与关键字参数、默认值;A 可变默认参数陷阱;A 局部 vs 全局、global 为什么要避免;A 把 50 行拆成 4–6 个函数;B 类型注解;B *args/**kwargs、仅关键字参数;B lambda、函数作为参数;C 闭包/装饰器/递归。

阅读:廖雪峰《调用函数》《定义函数》《函数的参数》;官方教程 4.8–4.9;Automate 3e Ch4。

预测(5b 开头,贴到 Python Tutor 看栈帧)

total = 0
def add(n):
    total = total + n      # ?  ← UnboundLocalError:函数内赋值使 total 成为局部变量
    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))          # ?

练习exercises/day1/05_functions.py(5a 做前 4 题,5b 做后 5 题)

函数 行为 测试用例
safe_divide(a: float, b: float) -> float | None b == 0 返回 None (10, 4)→2.5(1, 0)→None
clamp(x: int, lo: int = 0, hi: int = 100) -> int 夹在区间内 150→100-5→050→50clamp(7, lo=1, hi=5)→5
stats(nums: list[float]) -> tuple[float, float, float] (最小, 最大, 平均);空 → ValueError [2, 4, 6]→(2, 6, 4.0)
apply_discount(price: float, percent: float = 10) -> float 打折并 round(..., 2) 100→90.0(59.9, 15)→50.9150.915 在二进制里略小于 50.915,所以舍到 50.91——这就是 Block 1 讲的浮点问题)
append_item(item, items: list | None = None) -> list 正确处理可变默认参数 append_item(1)append_item(2) 分别得到 [1][2]
greet(name: str, *, greeting: str = "你好") -> str 仅关键字参数;返回 f"{greeting},{name}!" greet("小明")→"你好,小明!"greet("Tom", greeting="Hi")→"Hi,Tom!"greet("A", "B")TypeError
summarize(*nums: float) -> dict[str, float] {"count", "total", "avg"},无参数时 avg 为 0.0 summarize(1, 2, 3)→{"count": 3, "total": 6, "avg": 2.0}summarize()→{"count": 0, "total": 0, "avg": 0.0}
apply_twice(func, x) func(func(x)) apply_twice(lambda v: v * 2, 3)→12
event_report(events: list[str]) -> str 拆分练习:必须另写 count_events(events) -> dict[str, int]format_line(name, n) -> str(返回 f"{name}: {n} 次"),再由 event_report 按名称升序拼成多行 ["登录", "下单", "登录"]"下单: 1 次\n登录: 2 次"[]→""

陷阱反例:可变默认参数;函数没写 return 得到 None;在函数里给全局变量赋值;把变量命名为 liststrsuminput(遮蔽内置名)。


Block 6(50 分钟):调试——读报错、断点单步、找 bug

知识点:A 读 Traceback(最后一行看异常名,往上找自己文件的行号);A 常见异常含义;A print(f"{x=}");A VS Code 断点/F10/F11/变量面板;B assert

阅读:Automate 3e Ch5 前半(Raising Exceptions 之前);官方教程 8.1–8.2;research/04 第四节的陷阱清单挑 5 条读。

练习exercises/day1/06_debugging_bughunt.py —— 文件里有 7 个带 bug 的函数,测试是正确的。规则:每个函数先在 VS Code 里打断点单步跟到出错处,写下"现象 → 原因",再改。

函数 植入的 bug 正确行为(测试)
average(nums) 空列表除零 []→0.0[1, 2, 3]→2.0
find_max(nums) 初始值 best = 0,全负数时出错 [-5, -2, -9]→-2[3, 7, 1]→7
remove_negatives(nums) 遍历时 remove,漏删相邻负数 [1, -2, -3, 4]→[1, 4][-1, -1]→[]
is_leap_year(year) 漏了 400 年规则 2000→True1900→False2024→True2023→False
count_down(n) range 边界差一 3→[3, 2, 1, 0]0→[0]
add_student(name, roster=[]) 可变默认参数 两次不传 roster 的调用各得长度 1 的列表
next_year_age(text) 忘了 int()"17" + 1TypeError "17"→18

复盘问题TypeErrorValueError 的区别?IndexErrorKeyError 分别来自哪种数据结构?


Block 7(60 分钟):综合小程序——成绩单分析

目标:把今天所有东西串起来,写一个 60–80 行、有 7 个函数的程序。数据先用多行字符串(文件读写是明天的内容)。

练习exercises/day1/07_mini_project_grades.py

数据(文件里已给出):

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

函数 行为 测试用例
parse_report(text: str) -> list[dict] 首行是表头;每行一个字典,分数转 int;跳过空行 第一条为 {"姓名": "张三", "语文": 85, "数学": 92, "英语": 78};共 4 条
student_average(record: dict) -> float 数值字段平均,round(..., 1) 张三 85.0、王五 71.7
subject_averages(records: list[dict]) -> dict[str, float] 各科平均,round(..., 1) {"语文": 83.2, "数学": 81.0, "英语": 86.0}(注意 83.25 四舍五入到 83.2 是"银行家舍入",文件注释里解释)
letter_grade(score: float) -> str A≥90,B≥80,C≥70,D≥60,其余 F 91.0→"A"85.0→"B"71.7→"C"59.9→"F"
grade_distribution(records: list[dict]) -> dict[str, int] 按学生平均分的等级计数;A–F 五个键都要有 {"A": 1, "B": 2, "C": 1, "D": 0, "F": 0}
top_students(records: list[dict], n: int = 3) -> list[tuple[str, float]] 按平均分降序 [("李四", 91.0), ("赵六", 86.0), ("张三", 85.0)]
build_report(records: list[dict]) -> str 多行报表:标题行、每人一行 f"{姓名:<4}{平均:>6.1f} {等级}"、各科平均、等级分布 断言结果包含子串 "李四""91.0""A""语文""83.2"
main() print(build_report(parse_report(RAW))) 运行文件能看到报表

要求:每个函数 ≤ 15 行;不用全局变量传数据;先写 parse_report 并打印结果确认再写下一个。做完后运行 uv run ruff check exercises/day1uv run ruff format exercises/day1


复盘(30 分钟)

  1. checklist.md 勾掉今天完成的块,写学习日志(模板见 ai-guide.md 第三节)。
  2. 合上电脑,在纸上默写:for k, v in d.items()、列表切片 lst[a:b:c]def f(x, lst=None) 的写法、f"{x:>8.2f}" 的含义。写不出来的就是明早晨测会考的。
  3. 明早晨测范围:Block 1–5 的"预测"代码与陷阱反例。22:30 前睡觉。

若时间不够:Block 7 只完成 parse_reportstudent_averageletter_grade 三个函数即可,其余明天 Block 7 综合 v2 会重做一次。