Leon · 学习主页 ↗下载完整学习包 ↓ 文字版讲义 ↓ 浏览器未允许本地保存;笔记仍可在本次使用中编辑,请手动导出进度。
请启用浏览器 JavaScript 来切换章节,或阅读同目录的 textbook.md 文字备份。
本章学会什么 区分浏览器、编辑器、终端和 Python 解释器 在 Windows 与 Mac 上运行同一个文件 掌握教材、练习、答案和进度的使用方法
KNOWLEDGE CONNECTIONS · 从知识到用途
Python 知识图谱
遇到新语法时,问三句话:它接在哪个知识后面?现在解决什么问题?以后用在哪里?下面把这三件事连起来。
实线箭头表示本课程建议的学习联系,不是唯一学习顺序;虚线联想表示可相互对照。场景链路中的箭头才表示数据处理先后。点击节点不会自动勾选学习进度。
从零打基础 做成绩分析 整理 AI 数据 理解 Agent 为大模型做准备
展开全部 24 个知识点 · 按主题查找 查找知识点、问题或联想词
把知识用在同一个场景里 沿着输入 → 处理 → 输出看数据怎样变化,再点击知识点补上卡住的环节。先口头预测下一步,再运行教材中的 Python。
成绩表 → 清洗 → 报告 用原成绩项目的字段约定来拆解。先做能手算的小数据,再运行项目中的完整文件。下面的表是数据追踪,不是程序执行器。
01 · 读文件 输入: student_id,name,score 的 CSV 文本
处理: DictReader 按表头解析
输出: {"student_id": "S1", "name": " 小林 ", "score": "60"}
02 · 清洗一条 输入: name=" 小林 ",score="60"
处理: strip 清理;int 转换;检查 0 ≤ score ≤ 100
输出: {"student_id": "S1", "name": "小林", "score": 60}
03 · 收集与去重 输入: 一条无效 S2/abc,随后一条有效 S2/0
处理: 无效条记错误;只有有效且未见过的学号才加入 seen
输出: S2 的 0 分保留;seen 不被前面的坏记录占用
04 · 统计小样例 输入: 有效分数 [60, 0, 90]
处理: count=3,total=150,passed=2
输出: mean=50.0,pass_rate=2/3;空列表时均值和及格率为 None
05 · 排名与验收 输入: 有效记录和错误列表
处理: 按分数降序、学号升序排名;写报告;对照手算
输出: JSON 报告与错误 CSV;先确认有效数、均值、比例再读排名
迁移自测: 第一条 S2 分数是 abc,第二条 S2 分数是 0。为什么不能在字段校验前就 seen.add("S2")?
先解释,再对照解析 第一条无效,不应占用 S2。提前登记会让第二条合法零分被误判重复。按项目约定,seen 只记录已保留的有效学号。扩展:两条都有效时保留第一条;若需求改成保留最新一条,就必须修改规则和测试。
打开对应示例或项目 →
任务 → 工具 → 观察 → 判断 沿用第 16 章的规则工具示例。每一步的中间值都应可以查看;真实模型只会让决策来源更复杂,不会替你自动完成校验。
01 · 描述任务 输入: 用户想查询指定条目的资料
处理: 保存任务文本和明确的目标
输出: 任务与目标;尚未执行工具
02 · 决定动作 输入: 任务与当前状态
处理: 规则选出工具名和参数
输出: 一个动作字典;它只是执行请求
03 · 检查并执行 输入: 工具名、参数字典
处理: 检查工具是否存在、参数是否合约定,再调用函数
输出: 返回一个值或明确的失败;工具返回值是观察数据
04 · 保存观察 输入: 本轮工具结果
处理: 将结果放入轨迹;更新轮数和下一步状态
输出: 新轨迹、新轮数;参数改绑需要外层接住返回值
05 · 判断结束 输入: 目标、观察和剩余预算
处理: 核对结果是否回答原问题;失败时决定继续还是停止
输出: 完成 / 未完成;达到次数上限不自动变成完成
迁移自测: 工具返回 {"ok": True, "text": "未找到相关资料"},仅凭 ok 能宣布任务正确吗?
先解释,再对照解析 不能。ok 可能只说明工具顺利运行;还要检查内容是否满足任务。试着把失败原因拆成参数错误、工具执行失败、结果不满足任务,分别说明该在哪一步处理。这里没有调用真实模型。
打开对应示例或项目 →
保存完整文字图谱与联想自测 ↓ · 图谱无需联网,不保存额外个人数据。
这份教材怎样帮你学会实操
这是给没有编程经验的学习者准备的第一阶段教材。你无需懂数学公式、命令行或英文术语。目标是让你独立完成“接收数据 → 判断与处理 → 保存结果”的小程序,并看懂后续机器学习代码中的常见 Python 写法。它不承诺学完即可训练大模型;模型原理、线性代数、NumPy、PyTorch 是后续阶段。
先下载并完整解压学习包,再双击 index.html。 不要在压缩包的预览窗口里运行脚本。网页、代码和示例数据均可离线使用;第一次安装 Python 需要联网,或提前从官方网站准备适合目标电脑的安装文件。网页本身不需要安装任何程序,也不依赖在线字体、CDN 或账号。运行 Python 代码则需要该电脑已安装 Python。
正文中的代码都是 Python;标注为“终端命令”的内容则写在终端里。网页的“复制”只复制代码,不会自动执行。为保证完整离线、避免额外大型运行环境,本教材不在网页中执行任意 Python 。三个演示器只展示预设语义轨迹,真实练习请在自己的解释器里运行。
文件夹地图
index.html:教材入口,双击打开。
examples/ch02/ex02_01.py:第 02 章第 1 个示例,可以直接运行。
practice/ch02/ex02_01.py:第 02 章第 1 道习题,留给你编写代码。
solutions/ch02/ex02_01.py:该题参考答案;先尝试,再查看。
projects/:四个综合项目,每个有分步任务、示例数据和完整实现。
tools/check_practice.py:练习自检器;tools/verify_examples.py:全部示例核对器。
notes/:错题本和复习记录模板;textbook.md:纯文字备份。
练习请编辑解压包原位置的 practice 文件。 网页文件链接会下载副本;若你修改的是“下载”目录中的副本,自检器仍会读取学习包里的原文件。最稳妥的方法是在编辑器里用 File → Open 打开题目注明的 practice 路径,修改并保存。
示例和习题分别编号,所以同一个 02_01 在 examples 和 practice 中不是同一段程序。编号的前两位是章节号,后两位是本章顺序。
四个工具分别做什么
编辑器 负责修改文本,例如 Python 自带的 IDLE;解释器 负责执行 Python 代码;终端 负责接收启动程序的命令;浏览器 负责显示教材。可以把编辑器想成写菜谱的纸,解释器想成按菜谱做菜的人,终端想成发出“开始做菜”的入口。类比只帮助入门;代码的准确含义以解释器规则为准。
hello.py 是文本文件,.py 告诉你这是 Python 源代码。print("你好") 的括号是调用,双引号包住字符串,print 把内容显示到输出区域。注意使用英文半角括号和引号,不能用中文的 ( ) “ ” 替换。
Windows 安装与首次运行
打开 Python 官方下载页 ,按照 Windows 官方安装说明 安装 Python。官方目前提供 Python install manager;已有正常工作的 Python 3 不必为了本教材重装。安装流程随版本变化,以官方页面为准。
安装完成后,关闭旧终端,再从开始菜单打开 PowerShell 或 Windows Terminal,输入 py --version。若 py 不可用但 python --version 成功,后续把 py 换成 python。
确认输出是 Python 3.10 或更高。本教材使用的语法按 Python 3.10+ 编写,交付前在 3.12 环境实际验证;不是对所有操作系统组合都做过现场测试。
将整个文件夹放在一个容易找到的位置,例如桌面。打开该文件夹,在文件资源管理器地址栏输入 powershell 并回车,通常即可在此目录启动 PowerShell。也可以手动输入 cd "完整文件夹路径"。
输入下面的命令,看到文字即成功。命令前面没有要你复制的 $ 或 >>>。
终端命令 复制代码
py examples/ch01/ex01_01.py
Mac 安装与首次运行
从 Python 官方下载页 获取 macOS 安装包,参考 macOS 官方说明 安装。学习用解释器不要依赖系统工具内部使用的 Python。
用 Spotlight 搜索并打开“终端”,输入 python3 --version,确认 Python 3.10+。
在终端输入 cd (注意末尾有空格),把解压后的学习包文件夹拖入终端,再按回车。拖入操作会帮助你填好带空格的路径。
输入下面的命令:
终端命令 复制代码
python3 examples/ch01/ex01_01.py
之后所有标为 python ... 的通用命令,在尚未使用虚拟环境时,Windows 可改成 py ...,Mac 可改成 python3 ...。第 11 章使用虚拟环境时会给出精确路径。
完全不会编辑文件时怎么做
如果安装中包含 IDLE,在开始菜单或应用程序中打开它。选择 File → Open,打开任意 .py 文件;修改后按 Ctrl+S(Mac 用 Command+S)保存,再选择 Run → Run Module(常见快捷键 F5)。结果会出现在 IDLE Shell 中。练习也可以用任意纯文本代码编辑器完成。
不要用 Word 编辑 .py,也不要保存成 hello.py.txt。Windows 可以在文件资源管理器中开启“显示文件扩展名”;Mac 文本编辑器需要先转换为纯文本。使用 IDLE 通常更省事。
遇到 >>> 说明进入了 Python 交互模式,可以输入 Python 表达式。此时不能直接输入 py hello.py。输入 exit() 回到系统终端,再运行终端命令。... 是交互模式的续行提示,不是要写进程序里的字符。
一次学习的具体流程
每次安排 45—75 分钟,可按自己的时间拆开:5 分钟默写上一节;15 分钟读新知识;20 分钟手敲并改动示例;15 分钟独立做题;5 分钟记录错误。看懂答案不等于能够独立写出。 每个例子先写下预计输出,再运行,然后至少改两个输入。
卡住时按顺序做:复述题目 → 写出输入与输出 → 手算一个小例子 → 用中文列步骤 → 把步骤翻译成代码 → 用 print 看中间值。独立尝试 10—15 分钟后可以展开提示;再卡住才看解析。看过答案的题,第二天关掉答案重写。
换电脑继续学习
复制整个解压后的文件夹,或者复制压缩包后重新解压。已写好的 practice、projects 和 notes 也要一起带走。网页进度保存在当前浏览器本地,它不会自动同步,也可能因移动文件或浏览器清理而丢失:离开前点击“导出进度”,在另一台电脑点击“导入进度”。导出的 JSON 包含章节勾选和笔记,不包含你的 .py 源代码。浏览器禁止本地存储时仍可阅读,应手动导出进度。
第一周不要做的额外工作
不需要显卡,不需要注册模型 API,不需要安装 PyTorch,也不需要一次配置多个编辑器。第 00—16 章和四个项目只使用标准库。先把这条最短路径走通:保存一个文件 → 运行 → 修改 → 再运行。
AI / Agent 穿插内容怎样学
主页的“部分内容为info1110课程”用于标记你的课程关联。基础语法可以配合课堂复习;v1.1 新增的 AI / Agent 场景是自编延伸练习,不据此推定课程的考核范围。
保持原来的顺序:先学本章语法,完成普通示例,再做本章最后的“AI / Agent”示例与练习。遇到暂时不懂的 AI 名词,先关注代码中的字符串、列表、判断和函数;不需要先学神经网络。
本轮的小实验只使用 Python 标准库,完全离线。提示词是给模型的文字要求;大模型(LLM)根据上下文生成内容;工具(tool)是程序可以调用的函数;Agent 则围绕目标组织决策、工具执行和结果反馈。这里用固定数据和规则模拟部分流程,没有调用真实模型、没有网络推理、没有产生 API 费用 。
章节 穿插的场景 这次真正练的 Python
03 / 04 组织提示词、选择处理路径 字符串、f-string、条件顺序
06 / 08 限制执行步数、保存对话 while、break、列表套字典
09 / 12 工具参数与结果、保存会话 函数返回值、JSON 文件
13 / 14 检查结构化输出、独立会话 异常、输入校验、实例属性
15 / 16 模拟流式片段、检索工具循环 yield、状态更新、可追踪的执行过程
每个场景示例都有 .py 文件,新增练习仍可用同一自检器检查。旧题号和进度文件格式保留;在本版导入旧进度后,新题会显示为未完成。
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
下一章 → 本章学会什么 理解执行顺序、表达式和语句 会用 print、注释和缩进 能识别最常见的输入错误 程序就是一系列明确指令
“帮我统计分数”对人足够,对计算机不够。你要写清楚:分数有哪些、怎样相加、人数怎么算、最后显示什么。通常先确定输入、处理、输出 。比如输入两科成绩 80 和 90,处理是相加后除以 2,输出是 85.0。
Python 普通脚本从上到下执行;遇到函数定义会先记住函数,遇到调用才执行函数体(第 09 章);遇到条件和循环会改变执行路径。先把现在的例子当作一行接一行执行。
表达式、语句和函数调用
2 + 3 是能得到一个值的表达式。在文件里单独写 2 + 3 会计算,但通常不会显示结果。print(2 + 3) 才会显示。print 是函数名,括号里的东西是传给它的参数。多个参数用英文逗号分开,默认用空格连接。
print("2 + 3") 输出文字 2 + 3;print(2 + 3) 输出数字 5。引号告诉 Python“里面是文本”,没有引号时则按代码解释。单引号与双引号都可以,开头和结尾必须匹配。
空行、注释、缩进
# 后直到这一行结束是注释,解释器不把它当作指令执行。注释最好解释“为什么”,例如“先换成分钟,方便比较”,而不是只重复代码。字符串中的 # 则是普通文字。
空行帮助人阅读。缩进在 Python 中则参与语法:同一代码块要用一致缩进,建议每层 4 个空格。顶层语句一般从最左边开始。不要混合 Tab 与空格。后面看到 if、for、def 后的冒号,下一层内容通常需要缩进。
先读懂一条报错
故意在练习文件里写 print("你好",运行会看到 SyntaxError,意思是结构不符合语法。补上右括号后再运行。报错指向的位置有时是解释器发现问题的位置,真正漏掉的符号可能在上一行。
NameError 常表示名字没定义或拼错,如写成 pirnt。Print 与 print 不同,Python 区分大小写。每次只改一个问题,然后重新运行;不要一次凭感觉改很多行。
用中文先写算法
以“给两人平均分一笔钱”为例:得到总金额 → 得到人数 → 总金额除以人数 → 显示结果。暂时用固定数字,等第 02 章再接收键盘输入。写程序不等于背语法;先有步骤,再选择语法。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 01_01
第一段程序 运行文件:examples/ch01/ex01_01.py
Python 复制代码
print("你好,Python!" )
print("今天开始学编程。" )预测完后,查看预期输出 怎么执行 第 1 行先输出问候,第 2 行再输出计划。每次 print 默认在末尾换行。程序不会自动猜测你想改变什么。
动手改写: 把两句话改成自己的学习目标。
示例 01_02
文本与计算 运行文件:examples/ch01/ex01_02.py
Python 复制代码
print("2 + 3" )
print(2 + 3 )
print("结果是" , 2 + 3 )预测完后,查看预期输出 怎么执行 第一行的加号是文本字符。第二行先计算表达式得到 5,再交给 print。第三行传入两个参数,用默认空格连接。
动手改写: 预测 print("2" + "3") 会输出什么,再运行。
示例 01_03
控制输出分隔 运行文件:examples/ch01/ex01_03.py
Python 复制代码
print("年" , "月" , "日" , sep="/" )
print("正在学习" , end=":" )
print("Python" )预测完后,查看预期输出 怎么执行 sep 指多个参数之间的分隔符;end 指本次输出结束时追加什么。第二行用中文冒号替代默认换行,所以第三行接在同一行。
动手改写: 把 sep 改成空字符串,把 end 改成换行符。
示例 01_04
注释与执行顺序 运行文件:examples/ch01/ex01_04.py
Python 复制代码
print(80 + 90 )
print((80 + 90 ) / 2 )
print("# 这部分仍会显示" )预测完后,查看预期输出 怎么执行 括号让加法先执行。除法 / 的结果是浮点数,所以显示 85.0。第一行注释不执行,最后一行引号内的 # 不是注释。
动手改写: 删掉平均分表达式内的小括号,观察结果并解释。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 01_01 · 基础 我已独立完成
学习名片 依次输出两行:我叫小林;我每天学习 45 分钟。标点必须与示例一致。
作答文件:practice/ch01/ex01_01.py
预期输出 给我一点提示 每行用一个 print,中文文字放在引号里。
查看参考代码与解析 Python 复制代码
print("我叫小林" )
print("我每天学习 45 分钟" )解题思路 把自然语言中的两句话对应到两次输出。print 自动换行,无需自己写换行符。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 01_01
练习 01_02 · 基础 我已独立完成
先算后显示 输出 12 与 8 的和、差、积,每个结果占一行。
作答文件:practice/ch01/ex01_02.py
预期输出 给我一点提示 引号中的 12 + 8 不会计算。
查看参考代码与解析 Python 复制代码
print(12 + 8 )
print(12 - 8 )
print(12 * 8 )解题思路 加减乘分别使用 +、-、*。数学里的乘号 × 不能直接写进 Python 表达式。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 01_02
练习 01_03 · 纠错 我已独立完成
修复姓名输出 原程序是 Print(小林)。修好后输出 小林。解释两个错误。
作答文件:practice/ch01/ex01_03.py
预期输出 给我一点提示 一个错误是函数名大小写,另一个是文字的表示方法。
查看参考代码与解析 解题思路 Print 不是内置 print;小林 不加引号会被当作变量名查找。中文变量名技术上可以使用,但教材使用英文变量名以便阅读常见代码。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 01_03
练习 01_04 · 场景 我已独立完成
收据一行 用一次 print 输出 苹果|3|12,不要在竖线两侧加入空格。
作答文件:practice/ch01/ex01_04.py
预期输出 给我一点提示 使用 sep 参数。
查看参考代码与解析 Python 复制代码
print("苹果" , 3 , 12 , sep="|" )解题思路 三个参数分别代表品名、数量、金额。sep="|" 替换默认的空格。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 01_04
练习 01_05 · 推理 我已独立完成
平均分与优先级 两次测验是 70 和 90,输出平均分 80.0。说明为什么 70 + 90 / 2 不对。
作答文件:practice/ch01/ex01_05.py
预期输出 给我一点提示 先相加,后除以 2。
查看参考代码与解析 Python 复制代码
print((70 + 90 ) / 2 )解题思路 乘除优先于加减;不加括号得到 70 + 45 = 115。括号明确把总分作为除法的左边。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 01_05
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 会给值命名并更新变量 区分 int、float、str、bool 与 None 会把键盘输入转换成需要的类型 变量是绑定到对象的名字
age = 20 不是数学等式,而是把右边的值交给左边的名字。读成“让 age 指向整数 20”。执行 age = age + 1 时,先读旧值 20,算出 21,再把 age 绑定到 21。初学可把它想成贴标签;第 07 章会说明为什么两个标签可能指向同一个列表。
名字用字母、数字和下划线组合,不能以数字开头;推荐 study_minutes 这种小写下划线风格。class、if 等关键字不能当变量名。不要把变量起名为 list、str、sum,否则会遮住同名内置工具。
类型决定可以做哪些操作
类型 示例 含义与用途
int 3、-8、0 整数,人数、次数、索引
float 3.5、0.01 浮点数,近似表示小数
str "3"、"你好" 文本,即使看起来像数字
bool True、False 真或假,注意首字母大写
NoneType None 暂无结果或缺失值的一个标记
type(x) 查看类型。3 + 2 是数值相加;"3" + "2" 是拼接;"3" + 2 会产生 TypeError。Python 不会替你猜应该拼接还是相加。
算术常用符号
+ - * / 分别是加减乘除,// 是向下取整的除法,% 是余数,** 是幂。正数场景下 125 // 60 得到 2,125 % 60 得到 5,常用来把分钟换成小时和分钟。负数时要小心:-7 // 3 得到 -3,因为向负无穷取整;-7 % 3 得到 2,满足 a == (a // b) * b + a % b。
通常先括号,再乘方,再乘除余,再加减;拿不准时用括号表达意图。-2 ** 2 是 -4,(-2) ** 2 才是 4。total += 5 相当于给当前 total 加 5 后赋回;total 必须先有值。
input 永远先给你字符串
input("年龄:") 会显示提示并等待输入,回车后返回字符串,不包含回车本身。输入 18,得到的是 "18"。要做整数运算,使用 int(...);小数用 float(...)。int("18.5") 会报错,而 int(18.5) 得到 18,是向零截断,不是四舍五入。
str(18) 把数字变成文本。bool("") 是 False,而 bool("False") 是 True,因为它是非空字符串。不要用 bool 把用户输入的“否”自动转换成 False,要明确比较文本。
小数不是总能精确存储
二进制浮点数无法精确表示部分十进制小数。0.1 + 0.2 可能显示 0.30000000000000004。显示两位小数可以用格式化,比较近似数在第 13 章用 math.isclose。学习账本时把金额保存为整数“分”,例如 12.50 元表示为 1250 分。格式化改变显示,不会让底层数值变得精确。
把需求翻译成变量
“买 3 本书,每本 25 元,优惠 10 元”可以拆为 price、quantity、discount,再写 total = price * quantity - discount。变量名说明数据角色,减少神秘数字。先做固定输入,再加 input,是更容易排错的顺序。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 02_01
变量更新 运行文件:examples/ch02/ex02_01.py
Python 复制代码
minutes = 30
minutes = minutes + 15
minutes += 5
print(minutes)预测完后,查看预期输出 怎么执行 先保存 30,第二行用旧值算出 45,第三行再增加 5。变量名不变,绑定的值变了。
动手改写: 增加一行 minutes -= 10,预测新结果。
示例 02_02
检查类型 运行文件:examples/ch02/ex02_02.py
Python 复制代码
print(type(3 ).__name__)
print(type(3.0 ).__name__)
print(type("3" ).__name__)
print(type(True ).__name__)
print(type(None ).__name__)预测完后,查看预期输出 程序输出
int
float
str
bool
NoneType怎么执行 type 返回类型对象,.__name__ 取该类型的名字。现在只需知道这套写法用于观察,不必提前掌握类。
动手改写: 分别对 0、""、False 使用 type。
示例 02_03
分钟拆分 运行文件:examples/ch02/ex02_03.py
Python 复制代码
total_minutes = 125
hours = total_minutes // 60
minutes = total_minutes % 60
print(hours, "小时" , minutes, "分钟" )预测完后,查看预期输出 怎么执行 整除计算装满了几个 60,取余计算不足一个 60 的部分。用 2 * 60 + 5 可以还原总数。
动手改写: 换成 59、60、61,检查边界。
示例 02_04
从输入到数值 运行文件:examples/ch02/ex02_04.py
运行时输入(逐行键入) Python 复制代码
text = input("请输入本周学习天数:" )
days = int(text)
print("总分钟" , days * 45 )预测完后,查看预期输出 怎么执行 示例自动核验会模拟输入 3。真实终端中你键入的 3 也会显示,但那是键盘回显,不属于程序输出。input 不自动给提示末尾加换行。
动手改写: 输入 0 或 7;再试 abc,观察 ValueError。
示例 02_05
购物合计 运行文件:examples/ch02/ex02_05.py
Python 复制代码
price_fen = 1250
quantity = 3
total_fen = price_fen * quantity
print(total_fen)
print(f"应付 {total_fen / 100:.2f} 元" )预测完后,查看预期输出 怎么执行 运算在整数分上完成,显示时除以 100 并保留两位。f 开头的字符串允许在花括号里填表达式,第 03 章展开。
动手改写: 把数量改成 0,说明它是否符合你的业务规则。
示例 02_06
交换变量 运行文件:examples/ch02/ex02_06.py
Python 复制代码
a = 10
b = 20
a, b = b, a
print(a, b)
print(-7 // 3 , -7 % 3 )预测完后,查看预期输出 怎么执行 交换时先求右侧旧的 b 和 a,再给左边两个名字赋值。第二个输出提醒你:负数整除不是简单删掉小数位。
动手改写: 用第三个临时变量完成同样的交换。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 02_01 · 基础 我已独立完成
摄氏转华氏 已有 celsius = 25,按 F = C × 9 / 5 + 32 计算并输出 77.0。
作答文件:practice/ch02/ex02_01.py
预期输出 给我一点提示 乘号写 *,不要写 ×。
查看参考代码与解析 Python 复制代码
celsius = 25
fahrenheit = celsius * 9 / 5 + 32
print(fahrenheit)解题思路 按公式依次运算。用 0 摄氏度手算应为 32,可额外验证。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 02_01
练习 02_02 · 场景 我已独立完成
时间拆分 把 3671 秒拆成小时、分钟、秒,并用空格分隔输出 1 1 11。
作答文件:practice/ch02/ex02_02.py
预期输出 给我一点提示 先整除 3600;剩余秒数再拆成 60。
查看参考代码与解析 Python 复制代码
seconds = 3671
hours = seconds // 3600
minutes = (seconds % 3600 ) // 60
remaining = seconds % 60
print(hours, minutes, remaining)解题思路 先取整小时,余数 71 秒含 1 分 11 秒。验算 1*3600+1*60+11=3671。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 02_02
练习 02_03 · 基础 我已独立完成
两次输入相加 程序读入两行整数,不打印提示。输入 12 和 8 时输出 20。
作答文件:practice/ch02/ex02_03.py
自检器提供的输入 预期输出 给我一点提示 每次 input 后调用 int。
查看参考代码与解析 Python 复制代码
a = int(input())
b = int(input())
print(a + b)解题思路 如果忘记 int,字符串相加会得到 128。先决定需要的运算,再决定数据类型。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 02_03
练习 02_04 · 纠错 我已独立完成
修复记账类型 price = "19.5",count = 2,输出总价 39.0;解释为什么 price * count 得不到总价。
作答文件:practice/ch02/ex02_04.py
预期输出 给我一点提示 字符串乘整数是重复文本。
查看参考代码与解析 Python 复制代码
price = "19.5"
count = 2
print(float(price) * count)解题思路 原式会得到 19.519.5,因为字符串重复两次。转换为 float 后才是数值乘法;正式账本建议整数分。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 02_04
练习 02_05 · 场景 我已独立完成
整箱装书 53 本书,每箱最多 12 本。输出完整箱数 4、剩余本数 5、实际需要箱数 5,每个一行。
作答文件:practice/ch02/ex02_05.py
预期输出 给我一点提示 最后不足一箱也占一箱;可用 (数量 + 容量 - 1) // 容量。
查看参考代码与解析 Python 复制代码
books = 53
capacity = 12
print(books // capacity)
print(books % capacity)
print((books + capacity - 1 ) // capacity)解题思路 向上取整公式适用于非负整数数量和正整数容量。用 0、12、13 验证,得到 0、1、2 箱。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 02_05
练习 02_06 · 迁移 我已独立完成
平均速度 跑了 5 千米,用时 1500 秒。输出平均速度 12.0,单位千米每小时。
作答文件:practice/ch02/ex02_06.py
预期输出 给我一点提示 先把秒换成小时;别把分钟当小时。
查看参考代码与解析 Python 复制代码
distance_km = 5
time_seconds = 1500
time_hours = time_seconds / 3600
print(distance_km / time_hours)解题思路 变量名带单位能减少错误。速度=路程/时间;现实程序还要保证用时大于 0,第 04 章学习判断。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 02_06
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 会索引、切片、清理和拆分文本 能用 f-string 组织清晰输出 理解不可变对象与返回新值 字符串是有顺序的文本
text = "Python" 包含 6 个字符,len(text) 得到 6。索引从 0 开始:text[0] 是 P,text[1] 是 y;负索引从末尾数,text[-1] 是 n。长度为 n 的非空字符串,正索引范围是 0 到 n-1。空字符串 "" 的长度为 0,没有第一个字符。
"你好" 的长度是 2;但有些 emoji 和组合字符由多个 Unicode 码点组成,len 不等于所有场景下肉眼看到的图形数。普通中英文练习先按常见字符理解即可。
切片的左闭右开
text[start:stop:step] 从 start 开始,到 stop 之前停止,按 step 跨步取。text[1:4] 取索引 1、2、3。省略 start 通常从开头,省略 stop 通常到结尾。text[:3] 是前三个,text[3:] 是从索引 3 到末尾,text[::-1] 是倒序。
记忆方法:“开始要,结束不要 ”。先在纸上写索引,再圈出实际取到的位置。普通正向切片中,取几个常可用 stop-start 理解;遇到负步长要重新沿步长方向数。步长不能是 0。单个索引越界报 IndexError,切片超出边界通常会自动截到可用范围。
方法调用读成“对这个对象做什么”
text.strip() 是调用字符串对象的方法,返回去掉两端空白的新字符串;不会删除中间空格。lower() 变小写,upper() 变大写,replace(old, new) 替换文本,split() 按连续空白拆分,split(",") 按逗号拆分,"-".join(parts) 用连字符把多个字符串连接。
" a b ".split() 得到两个词;"a,,b".split(",") 中间会保留空字符串。CSV 含引号或逗号时不要自己 split,第 12 章用 csv 模块。
字符串不能就地修改
name[0] = "L" 会失败,因为 str 不可变。name.upper() 返回新字符串;如果没有保存它,旧变量仍指向原字符串。写 name = name.upper() 才让 name 指向转换结果。不可变不等于变量不能重新赋值。
格式化输出
f"你好,{name}" 会把 name 的值放入文本;f 必须紧贴开头引号。f"{score:.2f}" 显示两位小数,f"{rate:.1%}" 将比例显示为一位小数的百分数。格式化常用于报告,不应把显示后的字符串重新拿来做数值运算。
转义符 \n 表示换行,\t 表示制表符,\\ 表示一个反斜杠。r"C:\notes" 是原始字符串的一种写法,但以单个反斜杠结尾仍有语法限制。处理路径优先用第 12 章的 pathlib,少手写反斜杠。三引号可以书写多行字符串,里面的缩进也可能成为文本的一部分。
一个实用的文本清洗流程
用户输入账号 → 去掉两端空白 → 统一大小写 → 检查是否为空 → 保存。每一步都先打印 repr(text),就能看到看不见的空格和换行。例如 repr(" hi\n") 会显示带引号和转义符的表示。不要为了“清洗”随意删除有意义的内部空格或符号。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 03_01
读字符与切片 运行文件:examples/ch03/ex03_01.py
Python 复制代码
word = "Python"
print(word[0 ], word[-1 ])
print(word[1 :4 ])
print(word[:2 ], word[2 :])
print(word[::-1 ])预测完后,查看预期输出 程序输出
P n
yth
Py thon
nohtyP怎么执行 索引 1 到 3 是 y、t、h。反向切片从最后一个字符向前走。
动手改写: 预测 word[1:5:2] 和 word[99:],再运行。
示例 03_02
清理姓名 运行文件:examples/ch03/ex03_02.py
Python 复制代码
raw = " Alice\n"
clean = raw.strip().lower()
print(repr(raw))
print(clean)
print(len(clean))预测完后,查看预期输出 怎么执行 先 strip 再 lower,链式调用从左向右。repr 明确显示末尾换行,普通 print 不容易看出它。
动手改写: 把输入改成全空格,观察 clean 是否为空。
示例 03_03
分词与拼接 运行文件:examples/ch03/ex03_03.py
Python 复制代码
sentence = " learn python today "
words = sentence.split()
print(words)
print("-" .join(words))预测完后,查看预期输出 程序输出
['learn', 'python', 'today']
learn-python-today怎么执行 split 无参数时把连续空白当分隔。结果是列表,第 07 章详细学习。join 的接收者是分隔字符串,参数是字符串序列。
动手改写: 把 split() 改成 split(" "),比较连续空格的处理。
示例 03_04
报告格式 运行文件:examples/ch03/ex03_04.py
Python 复制代码
name = "小林"
score = 86.456
rate = 0.875
print(f"{name}的分数:{score:.2f}" )
print(f"完成率:{rate:.1%}" )
print(f"编号:{7:03d}" )预测完后,查看预期输出 程序输出
小林的分数:86.46
完成率:87.5%
编号:007怎么执行 .2f 是小数点后两位;.1% 会乘 100 并添加百分号;03d 将整数补齐为至少三位,不足用 0 填充。
动手改写: 将完成率改成 1 与 0,解释输出。
示例 03_05
方法返回新文本 运行文件:examples/ch03/ex03_05.py
Python 复制代码
text = "python is fun"
text.replace("fun" , "useful" )
print(text)
text = text.replace("fun" , "useful" )
print(text)
print("python" in text)预测完后,查看预期输出 程序输出
python is fun
python is useful
True怎么执行 第一次 replace 的返回值被丢弃。第二次赋回 text 才改变名字指向的对象。in 判断子串是否出现,返回布尔值。
动手改写: 检查大小写不同的 "Python" 是否在 text 中。
示例 03_06
AI / Agent:用字符串组织提示词 运行文件:examples/ch03/ex03_06.py
Python 复制代码
topic = " Python 列表 "
level = "零基础"
clean_topic = topic.strip()
prompt = f"请面向{level}读者解释:{clean_topic}。"
print(prompt)
print(f"主题字符数:{len(clean_topic)}" )预测完后,查看预期输出 程序输出
请面向零基础读者解释:Python 列表。
主题字符数:9怎么执行 输入是带首尾空格的 topic。strip 返回去掉首尾空白的新字符串,原 topic 不会被自动修改;赋值后 clean_topic 是 'Python 列表'。f-string 把两个变量的值插入文字,print 才显示结果。len 在这里数 Python 字符串的 Unicode 码位,空格也算一个;它不是模型 token 数。本例只生成提示词,没有模型回复。
动手改写: 先把 topic 改成只有三个空格,预测清理后的字符数。再把模板改成要求输出一个例子,观察引号内文字与变量名各在哪里。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 03_01 · 基础 我已独立完成
邮箱清洗 把 " Learner@Example.COM " 去两端空白并转小写,输出 learner@example.com。
作答文件:practice/ch03/ex03_01.py
预期输出 给我一点提示 链式调用 strip 和 lower。
查看参考代码与解析 Python 复制代码
email = " Learner@Example.COM "
print(email.strip().lower())解题思路 只去两端,不改变内部结构。这里是字符串操作练习,不是完整的邮箱有效性验证。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 03_01
练习 03_02 · 场景 我已独立完成
遮盖号码 给定 phone = "13812345678",保留前 3 位和后 4 位,中间用 4 个星号,输出 138****5678。
作答文件:practice/ch03/ex03_02.py
预期输出 给我一点提示 切片 [:3] 和 [-4:]。
查看参考代码与解析 Python 复制代码
phone = "13812345678"
print(phone[:3 ] + "****" + phone[-4 :])解题思路 先确认本题假定长度为 11。真实输入还需检查长度和内容,不能把遮盖当作完整隐私保护。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 03_02
练习 03_03 · 基础 我已独立完成
提取文件扩展名 给定 filename = "report.final.csv",按点拆分,输出最后一部分 csv。
作答文件:practice/ch03/ex03_03.py
预期输出 给我一点提示 split 返回列表,[-1] 取最后一项。
查看参考代码与解析 Python 复制代码
filename = "report.final.csv"
print(filename.split("." )[-1 ])解题思路 这里练 split 与负索引。处理真实路径更适合 Path.suffix,尤其要注意隐藏文件和没有扩展名的文件。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 03_03
练习 03_04 · 场景 我已独立完成
清理多余空格 把 " I love Python " 转成单个空格分隔的 "I love Python"。
作答文件:practice/ch03/ex03_04.py
预期输出 给我一点提示 split() 后使用一个空格 join。
查看参考代码与解析 Python 复制代码
text = " I love Python "
print(" " .join(text.split()))解题思路 strip 只能去两端,无法合并内部重复空格。先拆成词,再以统一分隔符拼接。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 03_04
练习 03_05 · 推理 我已独立完成
切片推理 给定 s = "abcdef",依次输出 bdf、fedcba、abc,各一行。
作答文件:practice/ch03/ex03_05.py
预期输出 给我一点提示 选择不同起点和步长;反向步长为 -1。
查看参考代码与解析 Python 复制代码
s = "abcdef"
print(s[1 ::2 ])
print(s[::-1 ])
print(s[:3 ])解题思路 第一个索引序列是 1、3、5。第二个是 5、4、3、2、1、0。第三个是 0、1、2。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 03_05
练习 03_06 · 迁移 我已独立完成
学习进度百分比 完成 7 节,共 20 节。输出 已完成 35.0%。
作答文件:practice/ch03/ex03_06.py
预期输出 给我一点提示 先算比例,再用 .1% 格式。
查看参考代码与解析 Python 复制代码
completed = 7
total = 20
print(f"已完成 {completed / total:.1%}" )解题思路 比例是 0.35;百分号格式自己乘以 100。不要先乘 100 再用 %,否则会显示 3500.0%。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 03_06
练习 03_07 · 纠错 我已独立完成
确认是否已保存清洗结果 text=" Python "。有人调用 text.strip() 后直接打印 text。请修复并输出 Python,再输出长度 6。
作答文件:practice/ch03/ex03_07.py
预期输出 给我一点提示 方法返回新字符串,要保存返回值。
查看参考代码与解析 Python 复制代码
text = " Python "
text = text.strip()
print(text)
print(len(text))解题思路 字符串不可变,strip 不会就地改原对象。长度检查帮助你确认两端空格确实被去掉。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 03_07
练习 03_08 · 迁移 我已独立完成
AI / Agent:整理检索问题 给定 question = " What is Python? ",先去掉首尾空白,再变成小写,保存为 cleaned。输出 cleaned,下一行输出它的字符数。只练字符串处理,不做真实搜索。
作答文件:practice/ch03/ex03_08.py
预期输出 给我一点提示 先写 cleaned = question.strip().lower()。方法从左到右执行;len 统计的是清理后字符串。
查看参考代码与解析 Python 复制代码
question = " What is Python? "
cleaned = question.strip().lower()
print(cleaned)
print(len(cleaned))解题思路 原始文字 → strip 去边缘空白 → lower 统一英文大小写 → len 计数。中间的两个空格和问号都保留。常见错误是对原 question 求长度,或忘了接住方法返回的新字符串。边界试验:空字符串清理后仍为空,长度为 0;中文不因为 lower 而变成英文。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 03_08
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 使用比较和逻辑运算 正确组织 if、elif、else 检查边界、空值和短路行为 从比较得到真假
== 判断相等,!= 判断不等,>、<、>=、<= 比较大小。赋值 = 与比较 == 完全不同。18 <= age < 60 是合法链式比较,表示年龄同时落在这个区间。
and 要两边条件都成立,or 至少一边成立,not 取反。判断是否周末应写 day == "周六" or day == "周日",不能写 day == "周六" or "周日";后一项非空字符串总为真。
if 是选一条路径
基本结构是 if 条件:,下一行缩进写条件为真时做的事。elif 是前面没有选中时继续检查,else 是前面全部没有选中时兜底。一条 if/elif/else 链只会执行最多一个分支。多个独立 if 则会分别检查,有可能都执行。
成绩分级如果先判断 score >= 60,90 分也会先进入这个分支;高等级通常从高到低写。缩进结束代表这个分支结束。程序随后继续执行分支后面的语句。
真值不是只有 True 和 False
0、0.0、空字符串、空列表、空字典、空集合和 None 常被当作假;非空容器通常为真。if items: 常用来表示“有元素”。"0" 和 "False" 都是非空字符串,是真。
None 经常表示缺失值,判断它用 is None 或 is not None。值相等通常用 ==;is 判断是否为同一个对象,不要用它比较普通字符串或整数的数值相等。
短路能避免无意义运算
and 左边为假就不看右边;or 左边为真就不看右边。例如 count > 0 and total / count > 60,count 为 0 时不会执行除法,避免除零。技术上 and/or 返回某个操作数,而不保证一定返回 bool;刚开始尽量让两边都是明确条件。
用边界检查自己的判断
判断“满 100 减 10”,最有价值的测试不是只测 200,而是测 99、100、101。判断合法成绩,至少测 -1、0、60、100、101。先验证输入范围,再做业务分级,能避免把不可能的值分成正常等级。
把复杂判断拆成有意义的名字
不要把所有逻辑塞进一个长 if。可以先算 has_ticket、is_open,再判断 if has_ticket and is_open:。表达式很长时加括号,明确逻辑关系。条件中的布尔变量不必写 == True。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 04_01
判断是否通过 运行文件:examples/ch04/ex04_01.py
Python 复制代码
score = 59
if score >= 60 :
print("通过" )
else :
print("继续练习" )
print("记录完成" )预测完后,查看预期输出 怎么执行 59 >= 60 为假,走 else。最后一行没有缩进,因此不论哪条路径都会执行。
动手改写: 把 score 换成 60、100、-1;最后一个提醒你还没验证范围。
示例 04_02
成绩等级 运行文件:examples/ch04/ex04_02.py
Python 复制代码
score = 85
if not 0 <= score <= 100 :
print("成绩无效" )
elif score >= 90 :
print("优秀" )
elif score >= 60 :
print("通过" )
else :
print("待加强" )预测完后,查看预期输出 怎么执行 先挡住不合法数据,然后从最高阈值往下判断。85 不满足 90,但满足 60。
动手改写: 验证 -1、0、59、60、89、90、100、101。
示例 04_03
同时满足两个条件 运行文件:examples/ch04/ex04_03.py
Python 复制代码
age = 20
has_ticket = True
if age >= 18 and has_ticket:
print("允许进入" )
else :
print("条件未满足" )预测完后,查看预期输出 怎么执行 and 要求年龄和票据条件都成立。用四种真假组合做真值表,可以检查逻辑。
动手改写: 把 has_ticket 改为 False,再把 age 改为 17。
示例 04_04
短路避免除零 运行文件:examples/ch04/ex04_04.py
Python 复制代码
count = 0
total = 80
if count > 0 and total / count >= 60 :
print("平均分通过" )
else :
print("人数不足或平均分未通过" )预测完后,查看预期输出 怎么执行 第一个条件为假,第二个除法不执行。短路可以用来保护操作,但别让重要的副作用藏在条件右边。
动手改写: 改成 count = 2,手算第二个条件。
示例 04_05
缺失与零分 运行文件:examples/ch04/ex04_05.py
Python 复制代码
score = 0
if score is None :
print("尚未提交" )
else :
print(f"已提交,成绩 {score}" )
print(bool("False" ), bool("" ))预测完后,查看预期输出 怎么执行 零分仍是已提交的有效成绩。若使用 if not score,会把 0 和缺失混在一起。
动手改写: 把 score 改成 None,观察分支改变。
示例 04_06
AI / Agent:按明确规则选择处理路径 运行文件:examples/ch04/ex04_06.py
Python 复制代码
request = " 请解释列表 "
cleaned = request.strip()
if cleaned == "" :
route = "请先输入问题"
elif "计算" in cleaned:
route = "交给计算工具"
else :
route = "进入文字讲解"
print(route)预测完后,查看预期输出 怎么执行 先清理 request,再从上到下尝试条件。当前内容非空且不含“计算”,所以进入 else。这叫规则路由:由你写的条件决定走哪条路径,不包含模型理解。后面的章节会把这些文字路径换成函数调用。包含“不要计算”的句子也会命中关键词,说明关键词判断有明确局限。
动手改写: 依次替换 request 为三个空格、“请计算 2 + 3”、“不要计算,只解释”。记录实际路线,再写一句话说明这种规则为什么不能代表理解了用户意图。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 04_01 · 基础 我已独立完成
奇偶判断 给定 n = 17,如果能被 2 整除输出 偶数,否则输出 奇数。
作答文件:practice/ch04/ex04_01.py
预期输出 给我一点提示 余数等于 0 才是整除。
查看参考代码与解析 Python 复制代码
n = 17
if n % 2 == 0 :
print("偶数" )
else :
print("奇数" )解题思路 17 除以 2 余 1。补测 0 和负数,0 也属于偶数。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 04_01
练习 04_02 · 场景 我已独立完成
运费规则 订单金额 99 元,满 100 元免运费,否则运费 8 元。输出应付总额 107。
作答文件:practice/ch04/ex04_02.py
预期输出 给我一点提示 临界值 100 属于免运费。
查看参考代码与解析 Python 复制代码
amount = 99
if amount >= 100 :
shipping = 0
else :
shipping = 8
print(amount + shipping)解题思路 先决定运费,再统一计算合计,避免在每个分支重复相同输出。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 04_02
练习 04_03 · 纠错 我已独立完成
合法分数 score = 105。若不在 0 到 100 内输出 无效,否则达到 60 输出 通过,未达到输出 未通过。
作答文件:practice/ch04/ex04_03.py
预期输出 给我一点提示 合法性判断放在等级判断之前。
查看参考代码与解析 Python 复制代码
score = 105
if not 0 <= score <= 100 :
print("无效" )
elif score >= 60 :
print("通过" )
else :
print("未通过" )解题思路 错误写法先判断 score >= 60,会把 105 判通过。必须先限定问题域。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 04_03
练习 04_04 · 纠错 我已独立完成
周末判断修复 day = "周一"。修复 if day == "周六" or "周日" 的错误,输出 工作日。
作答文件:practice/ch04/ex04_04.py
预期输出 给我一点提示 or 两边都要完整比较,也可使用 in。
查看参考代码与解析 Python 复制代码
day = "周一"
if day == "周六" or day == "周日" :
print("周末" )
else :
print("工作日" )解题思路 "周日" 作为独立非空字符串是真;修复后左右两项均为 False。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 04_04
练习 04_05 · 迁移 我已独立完成
找三个数最大值 a=7,b=12,c=9,不用 max,输出最大值 12。
作答文件:practice/ch04/ex04_05.py
预期输出 给我一点提示 先把 a 当作当前最大值,再依次比较。
查看参考代码与解析 Python 复制代码
a, b, c = 7 , 12 , 9
largest = a
if b > largest:
largest = b
if c > largest:
largest = c
print(largest)解题思路 这里用两个独立 if,因为两次比较都需要执行。相等时无需更新,结果仍正确。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 04_05
练习 04_06 · 场景 我已独立完成
区分未填和零 value = 0,None 表示未填,其他数值包括 0 都算已填。输出 已填。
作答文件:practice/ch04/ex04_06.py
预期输出 给我一点提示 用 is None,而不是 if not value。
查看参考代码与解析 Python 复制代码
value = 0
if value is None :
print("未填" )
else :
print("已填" )解题思路 业务含义决定判断方式。零值和没有值是两个不同状态。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 04_06
练习 04_07 · 场景 我已独立完成
实验启动条件 has_data=True,has_config=True,is_running=False。只有有数据、有配置且当前未运行时输出 可以启动,否则输出 暂不能启动。
作答文件:practice/ch04/ex04_07.py
预期输出 给我一点提示 三个条件用 and,其中一个要取 not。
查看参考代码与解析 Python 复制代码
has_data = True
has_config = True
is_running = False
if has_data and has_config and not is_running:
print("可以启动" )
else :
print("暂不能启动" )解题思路 布尔名字直接放入条件,无需 == True。逐个翻转三个值,应该每次都阻止启动。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 04_07
练习 04_08 · 迁移 我已独立完成
AI / Agent:输出长度约定 给定 reply = " hello "、limit = 5。清理首尾空白后:空内容输出 empty;字符数超过 limit 输出 too_long;否则输出 ok。条件按此顺序组织。
作答文件:practice/ch04/ex04_08.py
预期输出 给我一点提示 长度恰好等于 5 应通过,比较符号应是 >。先判断空内容,可以把两种错误明确区分。
查看参考代码与解析 Python 复制代码
reply = " hello "
limit = 5
cleaned = reply.strip()
if cleaned == "" :
print("empty" )
elif len(cleaned) > limit:
print("too_long" )
else :
print("ok" )解题思路 清理后 hello 长度为 5,没有超过上限,因此输出 ok。请分别用空白、四字符、五字符、六字符测试。这是我们自己制定的字符长度规则,不是实际模型的上下文长度限制;模型上下文一般需要按对应 tokenizer 的 token 计算。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 04_08
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 理解遍历、循环变量和 range 会累计、计数、筛选与查找 能手工追踪每一次迭代 为什么需要循环
三个分数可以手写三次相加,三千个就不行。for score in scores: 的含义是从 scores 中依次取一个元素,把它绑定给 score,然后执行缩进块。块执行完再取下一个,取完就结束。每一次重复称为一次“迭代”。
本章先把 [70, 85, 60] 理解成一串有顺序的数据,叫列表。第 07 章再系统学习增删改查。循环变量可以任意起名,但 score 比 x 更容易理解。
range 生成整数序列
range(5) 对应 0、1、2、3、4;range(1, 6) 对应 1、2、3、4、5;range(2, 10, 2) 对应 2、4、6、8。与切片一样,终点不包含。range(5, 0, -1) 是 5、4、3、2、1。步长必须非零,方向与边界不匹配时可能一个数都没有。
range 本身不是列表,一般不用为了遍历把它转成 list。要观察全部内容时可以 print(list(range(5)))。range 使用整数边界,不接受 0.5 作为步长。
累计与计数模板
累计先设 total = 0,每次执行 total += value。计数先设 count = 0,满足条件时执行 count += 1。初始值写在循环前;如果写在循环体里,每次都会清零。
“及格人数”累加的是 1;“及格分数总和”累加的是 score。两个题目只差一句话,代码里的更新对象却不同。每次写循环都要问:本轮取到什么?保留什么状态?状态怎么变?什么时候结束?
手工追踪表
对 [2, 4, 1] 求和:初始 total=0;第 1 轮 value=2,更新为 2;第 2 轮 value=4,更新为 6;第 3 轮 value=1,更新为 7。不要只看最后输出,要能解释中间过程。网页的循环演示器可以逐步显示同样的状态变化。
空数据也要考虑
空列表的 for 一次也不执行,因此 total 保留 0。这是合理的空和,但平均值不能直接写 total/len(values),因为人数为 0。先检查长度再除。求最大值时也要明确空数据怎么处理;第 09 章用函数返回 None 或抛异常表达约定。
循环之后变量还在吗
for 不会创建独立作用域。非空循环结束后,循环变量通常仍绑定最后一个值;如果循环一次未执行,这个名字可能根本没定义。不要依赖“最后的循环变量”作为结果;建立明确的结果变量更可靠。
动手看执行过程
每轮 total 怎样变化 固定程序:对 [2, 4, 1] 执行 total += value。先预测下一轮 total,再点下一步。
上一步 下一步 重新开始
这是预设步骤演示;真实 Python 请运行示例 05_02。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 05_01
遍历分数 运行文件:examples/ch05/ex05_01.py
Python 复制代码
scores = [70 , 85 , 60 ]
for score in scores:
print("分数" , score)
print("遍历结束" )预测完后,查看预期输出 程序输出
分数 70
分数 85
分数 60
遍历结束怎么执行 循环每次只取一个分数。最后的 print 在循环外,因此只执行一次。
动手改写: 把最后的 print 缩进到循环体,预测区别。
示例 05_02
累计过程 运行文件:examples/ch05/ex05_02.py
Python 复制代码
total = 0
for value in [2 , 4 , 1 ]:
total += value
print(value, total)
print("总和" , total)预测完后,查看预期输出 怎么执行 total 跨越各轮保存累计状态。输出中的左列是本轮输入,右列是更新后的累计值。
动手改写: 把 total = 0 移入循环,解释为什么结果变成 1。
示例 05_03
统计通过人数 运行文件:examples/ch05/ex05_03.py
Python 复制代码
count = 0
for score in [59 , 60 , 80 , 40 ]:
if score >= 60 :
count += 1
print(count)预测完后,查看预期输出 怎么执行 for 里面嵌套 if,因此 if 内再多缩进一层。只在 60 和 80 两轮更新 count。
动手改写: 再添加 total,只累计通过者分数。
示例 05_04
范围与倒计时 运行文件:examples/ch05/ex05_04.py
Python 复制代码
for number in range(1 , 4 ):
print(number)
print("倒数" )
for number in range(3 , 0 , -1 ):
print(number)预测完后,查看预期输出 怎么执行 range 的第二个参数是停止边界,不是最后一个必含元素。负步长使数值递减。
动手改写: 改成 range(3, 0),解释为什么没有输出。
示例 05_05
带序号遍历 运行文件:examples/ch05/ex05_05.py
Python 复制代码
names = ["小林" , "小陈" , "小王" ]
for number, name in enumerate(names, start=1 ):
print(number, name)预测完后,查看预期输出 怎么执行 enumerate 把每项配上序号,左边两个名字分别接收序号与内容。start=1 改的是显示序号,不是原列表索引规则。
动手改写: 删除 start=1,观察序号从哪里开始。
示例 05_06
空列表平均值 运行文件:examples/ch05/ex05_06.py
Python 复制代码
scores = []
total = 0
for score in scores:
total += score
if len(scores) == 0 :
print("没有成绩" )
else :
print(total / len(scores))预测完后,查看预期输出 怎么执行 空列表跳过循环,再由 if 避免除零。程序要为“什么都没有”设计明确输出。
动手改写: 填入两个分数,再测试只有一个分数。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 05_01 · 基础 我已独立完成
一到十求和 用 for 循环计算 1 到 10(包含 10)的总和,输出 55。
作答文件:practice/ch05/ex05_01.py
预期输出 给我一点提示 range 的终点要写 11。
查看参考代码与解析 Python 复制代码
total = 0
for number in range(1 , 11 ):
total += number
print(total)解题思路 10 轮分别累加 1 至 10。不要在循环内部重新初始化 total。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 05_01
练习 05_02 · 基础 我已独立完成
偶数总和 计算 1 到 20 之间所有偶数的和,输出 110。
作答文件:practice/ch05/ex05_02.py
预期输出 给我一点提示 可用步长 2,也可用 if 判断。
查看参考代码与解析 Python 复制代码
total = 0
for number in range(2 , 21 , 2 ):
total += number
print(total)解题思路 从 2 开始每次加 2,包含 20。另一种写法遍历全部整数后检查 % 2 == 0。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 05_02
练习 05_03 · 场景 我已独立完成
学习打卡统计 学习分钟数 [0, 30, 45, 0, 60],分别输出学习过的天数 3 和总分钟 135。
作答文件:practice/ch05/ex05_03.py
预期输出 给我一点提示 计数累加 1,总量累加分钟。
查看参考代码与解析 Python 复制代码
days = 0
total = 0
for minutes in [0 , 30 , 45 , 0 , 60 ]:
if minutes > 0 :
days += 1
total += minutes
print(days)
print(total)解题思路 总和可以对所有项累加,因为 0 不影响结果;有效天数只在大于 0 时增加。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 05_03
练习 05_04 · 场景 我已独立完成
单词长度报告 对 ["cat", "python", "AI"] 输出 cat:3、python:6、AI:2,各占一行。
作答文件:practice/ch05/ex05_04.py
预期输出 给我一点提示 每次处理一个词,len 得到长度。
查看参考代码与解析 Python 复制代码
for word in ["cat" , "python" , "AI" ]:
print(f"{word}:{len(word)}" )解题思路 把“单个词怎样处理”写好后,再用 for 应用到整个列表,这正是批量处理的基本模式。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 05_04
练习 05_05 · 迁移 我已独立完成
找最大值 不用 max,求非空列表 [-5, -2, -9] 的最大值,输出 -2。
作答文件:practice/ch05/ex05_05.py
预期输出 给我一点提示 初始最大值不能设为 0,全部为负会出错。
查看参考代码与解析 Python 复制代码
numbers = [-5 , -2 , -9 ]
largest = numbers[0 ]
for number in numbers[1 :]:
if number > largest:
largest = number
print(largest)解题思路 用第一个真实元素初始化,之后逐项更新。此实现约定列表非空,空列表需另行处理。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 05_05
练习 05_06 · 迁移 我已独立完成
统计元音 统计 "Education" 中英文字母元音 a e i o u 的数量,忽略大小写,输出 5。
作答文件:practice/ch05/ex05_06.py
预期输出 给我一点提示 先 lower,再用 in 判断字符是否属于元音字符串。
查看参考代码与解析 Python 复制代码
count = 0
for char in "Education" .lower():
if char in "aeiou" :
count += 1
print(count)解题思路 依次命中 e、u、a、i、o。这里只统计指定英文字母,不是一般语言的语音分析。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 05_06
练习 05_07 · 迁移 我已独立完成
最长单词 在 ["I","study","python","daily"] 中找第一个最长词,输出 python;不用 max。
作答文件:practice/ch05/ex05_07.py
预期输出 给我一点提示 用当前最长词作状态,只在严格更长时更新。
查看参考代码与解析 Python 复制代码
words = ["I" , "study" , "python" , "daily" ]
longest = ""
for word in words:
if len(word) > len(longest):
longest = word
print(longest)解题思路 使用 > 而不是 >=,同长度时保留先出现的词。本题空列表时结果为空字符串,应在实际需求中明确是否接受。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 05_07
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 根据条件重复执行 区分 break、continue 与 return 理解嵌套循环和循环边界 for 与 while 怎样选
有一串现成数据需要逐项处理,优先 for;需要“持续做,直到某个条件改变”,常用 while。例如反复请求输入直到合法、持续显示菜单直到用户选择退出。不要为了用 while 把简单遍历写得更复杂。
while 条件: 每次进入循环前都会重新检查条件。先检查,再执行,所以条件一开始为假时执行零次。构成一个可控 while 的三个部分是:初始状态、继续条件、状态更新。遗漏更新可能产生无限循环。
手工走一遍计数器
n = 3;检查 n>0 为真,输出 3,然后 n-=1;再检查 2>0,输出 2;再检查 1>0,输出 1;最后 n=0,条件为假,退出。条件不是进入时检查一次就永远不变,每轮都会重新计算。
break 与 continue
break 立即结束它所在的最内层循环;continue 跳过本轮剩余语句,开始下一轮。嵌套循环里的 break 通常只退出内层。函数里的 return 则结束整个当前函数,第 09 章会讲。
while 中如果把计数器更新放在 continue 之后,某些路径可能永远跳过更新。解决方法是确保每条继续执行的路径都推进状态,或者直接用 for 遍历。
哨兵输入与菜单
哨兵是约定的结束标记,例如输入 q 就结束录入。先检查是不是 q,再把其他输入转换成数字,否则 int("q") 会直接报错。数据值可能本身为 0 时,别把 0 随意当结束信号;选不会与有效输入冲突的标记。
嵌套循环怎样执行
外层走一步,内层通常完整走一遍。两组实验学习率 × 三个批大小,会有 2×3=6 种组合。把外层看成行、内层看成列有助于理解,但它不必真是二维表。不要把“两个顺序排列的循环”误认为“两个嵌套循环”。
从错误中退出
终端里的长时间循环通常可以用 Ctrl+C 中断;IDLE 可用 Shell 中断或 Restart Shell。Ctrl+C 是恢复控制的方法,不是正确终止条件的替代品。调试时先限制最大次数,例如最多 5 轮,再观察状态。
Python 还有 for/while 的 else:仅在循环正常耗尽或条件变假、没有由 break 退出时执行。初学阶段用 found 变量更直白;阅读别人代码遇到循环 else 时记住“没有 break 才执行”,它不属于内部 if。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 06_01
有限倒计时 运行文件:examples/ch06/ex06_01.py
Python 复制代码
remaining = 3
while remaining > 0 :
print(remaining)
remaining -= 1
print("开始" )预测完后,查看预期输出 怎么执行 remaining 每轮减少,最终使条件变假。最后的输出在循环之外。
动手改写: 把初值改成 0,观察循环体是否执行。
示例 06_02
遇到目标就停止 运行文件:examples/ch06/ex06_02.py
Python 复制代码
for value in [4 , 8 , 15 , 16 ]:
if value > 10 :
print("首个大于 10 的数" , value)
break
print("检查过" , value)预测完后,查看预期输出 程序输出
检查过 4
检查过 8
首个大于 10 的数 15怎么执行 15 命中后 break,连本轮后面的输出也跳过,16 不再访问。
动手改写: 把条件改成 > 100,观察没有命中的路径。
示例 06_03
跳过无效项 运行文件:examples/ch06/ex06_03.py
Python 复制代码
total = 0
for value in [10 , -1 , 20 , -5 , 30 ]:
if value < 0 :
continue
total += value
print(total)预测完后,查看预期输出 怎么执行 负数轮次直接进入下一轮,正数被累计。continue 不结束整个遍历。
动手改写: 改成 break,解释为什么只得到 10。
示例 06_04
读取直到 q 运行文件:examples/ch06/ex06_04.py
运行时输入(逐行键入) Python 复制代码
total = 0
while True :
text = input().strip()
if text == "q" :
break
total += int(text)
print(total)预测完后,查看预期输出 怎么执行 模拟输入 10、20、q。while True 靠显式 break 终止;先判 q,再转整数。非数字输入的恢复在第 13 章学习。
动手改写: 增加一行数字 0,确认它是有效数据而不是退出标记。
示例 06_05
嵌套组合 运行文件:examples/ch06/ex06_05.py
Python 复制代码
for model in ["A" , "B" ]:
for batch_size in [2 , 4 ]:
print(model, batch_size)预测完后,查看预期输出 怎么执行 外层 A 固定时,内层先完成 2 和 4;再换 B 并重新遍历内层。
动手改写: 增加一个批大小 8,先预测输出行数。
示例 06_06
无命中的处理 运行文件:examples/ch06/ex06_06.py
Python 复制代码
found = False
for value in [1 , 3 , 5 ]:
if value % 2 == 0 :
found = True
print(value)
break
if not found:
print("没有偶数" )预测完后,查看预期输出 怎么执行 found 记录有没有找到,循环结束后统一检查。把初始化放在循环外,才能跨轮保留结果。
动手改写: 在中间加入 4,确认只打印第一个偶数。
示例 06_07
AI / Agent:最多执行三轮的任务循环 运行文件:examples/ch06/ex06_07.py
Python 复制代码
step = 0
max_steps = 3
finished = False
while step < max_steps:
step += 1
print(f"第 {step} 轮" )
if step == 2 :
finished = True
break
print(f"已完成:{finished}" )
print(f"实际轮数:{step}" )预测完后,查看预期输出 程序输出
第 1 轮
第 2 轮
已完成:True
实际轮数:2怎么执行 step 从 0 开始,每轮先加 1;第 1 轮未完成,重新判断 while;第 2 轮把 finished 改为 True 后 break,跳到循环后。max_steps 限制最多启动几轮;finished 单独记录是否完成。运行了两轮和任务完成是两个不同事实。本例用 step == 2 模拟完成条件,真实 Agent 需要根据执行结果判断。
动手改写: 把完成条件改成 step == 5,max_steps 保持 3。应执行三轮且 finished 为 False。再把 max_steps 改成 0,说明为什么循环体一次也不执行。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 06_01 · 基础 我已独立完成
while 求和 用 while 求 1 到 5 的和,输出 15。
作答文件:practice/ch06/ex06_01.py
预期输出 给我一点提示 计数器从 1 开始,每轮递增。
查看参考代码与解析 Python 复制代码
number = 1
total = 0
while number <= 5 :
total += number
number += 1
print(total)解题思路 条件用 <= 包含 5;循环后 number 为 6。不递增会一直累加同一个数。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 06_01
练习 06_02 · 场景 我已独立完成
首次及格 在 [40, 55, 72, 90] 中找到第一个及格成绩,输出 72 后停止。
作答文件:practice/ch06/ex06_02.py
预期输出 给我一点提示 条件成立后 print,再 break。
查看参考代码与解析 Python 复制代码
for score in [40 , 55 , 72 , 90 ]:
if score >= 60 :
print(score)
break 解题思路 “第一个”意味着后面的 90 不应再输出。若题目改成“所有”,就不该 break。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 06_02
练习 06_03 · 场景 我已独立完成
排除缺测值 [-1, 30, 0, 45, -1] 中 -1 表示缺测,0 有效。输出有效条数 3 与总量 75,各一行。
作答文件:practice/ch06/ex06_03.py
预期输出 给我一点提示 只跳过 == -1 的项,不要写 if not value。
查看参考代码与解析 Python 复制代码
count = 0
total = 0
for value in [-1 , 30 , 0 , 45 , -1 ]:
if value == -1 :
continue
count += 1
total += value
print(count)
print(total)解题思路 缺失标记与 0 的业务含义不同。统计有效条数时必须保留 0。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 06_03
练习 06_04 · 推理 我已独立完成
限次查找 给定 guesses = [2, 4, 7, 9],目标是 7,输出第几次猜中:3。
作答文件:practice/ch06/ex06_04.py
预期输出 给我一点提示 enumerate 从 1 开始,命中后 break。
查看参考代码与解析 Python 复制代码
for attempt, guess in enumerate([2 , 4 , 7 , 9 ], start=1 ):
if guess == 7 :
print(attempt)
break 解题思路 序号表示尝试次数,元素表示猜测值。这两个概念不要混淆。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 06_04
练习 06_05 · 迁移 我已独立完成
二维坐标 输出两行三列的所有坐标,行与列都从 0 开始,顺序为 0 0 到 1 2,每对一行。
作答文件:practice/ch06/ex06_05.py
预期输出 程序输出
0 0
0 1
0 2
1 0
1 1
1 2给我一点提示 外层行号 range(2),内层列号 range(3)。
查看参考代码与解析 Python 复制代码
for row in range(2 ):
for column in range(3 ):
print(row, column)解题思路 外层 2 次,每次内层 3 次,一共 6 对。日后张量维度也需要先辨认每一轴表示什么。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 06_05
练习 06_06 · 纠错 我已独立完成
修复 continue 死循环 写 while 输出 1 到 5 中的奇数,每个一行;确保遇到偶数也会推进计数器。
作答文件:practice/ch06/ex06_06.py
预期输出 给我一点提示 一种简单方法是先递增,再决定是否 continue。
查看参考代码与解析 Python 复制代码
n = 0
while n < 5 :
n += 1
if n % 2 == 0 :
continue
print(n)解题思路 更新放在 continue 前,所有轮次都能推进。若从 n=1 开始并把更新写在末尾,n=2 时可能永远跳过更新。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 06_06
练习 06_07 · 挑战 我已独立完成
整数各位求和 对非负整数 2048,用 while、// 和 % 求各位数字之和,输出 14。
作答文件:practice/ch06/ex06_07.py
预期输出 给我一点提示 %10 取末位,//10 去末位。
查看参考代码与解析 Python 复制代码
number = 2048
total = 0
while number > 0 :
total += number % 10
number //= 10
print(total)解题思路 逐轮末位为 8、4、0、2;number 变为 204、20、2、0。输入 0 时循环零次,总和仍为 0,符合约定。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 06_07
练习 06_08 · 迁移 我已独立完成
AI / Agent:完成前先扣减工作量 给定 remaining = 5、steps = 0。每轮处理一项,remaining 减 1,steps 加 1;只要有剩余且 steps 小于 3 就继续。最后分别输出 steps、remaining。不要写无限循环。
作答文件:practice/ch06/ex06_08.py
预期输出 给我一点提示 while 条件同时检查 remaining > 0 和 steps < 3。两个计数器都在循环体中更新。
查看参考代码与解析 Python 复制代码
remaining = 5
steps = 0
while remaining > 0 and steps < 3 :
remaining -= 1
steps += 1
print(steps)
print(remaining)解题思路 执行前为 (steps, remaining) = (0, 5),三轮后依次为 (1, 4)、(2, 3)、(3, 2)。下一次条件检查因 steps < 3 为 False 停止。没有完成的两项仍保留,不能把达到轮数上限写成“任务完成”。改 remaining 为 0 和 1,分别应得到 0/0 与 1/0。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 06_08
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 会对列表增删改查和切片 区分修改原对象与返回新对象 理解别名、浅拷贝与嵌套列表 列表解决一组数据的问题
values = [10, 20, 30] 把多个对象放在有顺序的容器中,可按索引取元素,也可以遍历。列表可以混合类型,但同一业务列表尽量保持结构一致,便于处理。len(values) 是元素个数;嵌套列表的长度只数外层元素,不自动数所有叶子。
单项赋值 values[1] = 99 改变原列表。append(x) 在末尾加入一个对象;extend(xs) 逐项加入另一个可迭代对象中的内容;insert(i, x) 插入;pop() 删除并返回末尾元素;pop(i) 按索引删除并返回;remove(x) 删除第一个相等的值,找不到会报 ValueError。del values[i] 也能按索引删除。
append 与 extend 的区别
对 [1, 2] 执行 append([3,4]) 得到 [1,2,[3,4]],因为加入的是一个列表对象;extend([3,4]) 得到 [1,2,3,4],因为逐项加入。初学可记:“append 装一个,extend 逐个装”。append("ab") 装入一个字符串,extend("ab") 会装入 a 和 b 两个字符。
sort 与 sorted 的区别
values.sort() 修改原列表,并返回 None;sorted(values) 返回新的排序列表,原对象不变。不要写 values = values.sort(),否则 values 会变成 None。reverse() 也是就地修改并返回 None。降序可用 sorted(values, reverse=True)。
两个名字可能指向同一个对象
b = a 没有复制列表,只是让 b 也指向 a 指向的对象。因此 b.append(3) 后,通过 a 也会看到变化。与之不同,b = [9] 只是让 b 改绑到另一个列表,不会修改 a 指向的旧列表。
读代码时问两个问题:“这是在修改对象,还是在改变名字的绑定?” 列表方法、元素赋值经常修改对象;普通 = 给名字重新绑定。列表上的 += 通常会就地扩展,不能简单把所有类型上的 += 都理解为创建新对象。
浅拷贝复制外壳
b = a.copy() 或 b = a[:] 创建新外层列表。如果元素是整数等不可变对象,通常足够;若元素本身是列表,内层仍可能共享。copy.deepcopy(a) 会递归复制常见嵌套结构,但不是所有对象都适合或需要深拷贝。
grid = [[0] * 3] * 2 的两行指向同一个内层列表,改一行会影响另一行。用循环每次新建 [0] * 3,或第 10 章的 [[0] * 3 for _ in range(2)],让每行独立。
不要一边遍历一边删同一列表
删除会让后面的元素向前移动,迭代位置却继续推进,容易跳过元素。更直白的方法是建立新列表,只把要保留的元素 append 进去。第 10 章可写成列表推导式。
元组是不可变的序列
point = (3, 4) 常表示一组固定结构的数据。x, y = point 是解包,元素数量要匹配。单元素元组写 (3,),逗号不能省;(3) 只是整数外加括号。元组的槽位不能重新赋值,但若里面放了可变列表,那个列表仍能改变,所以“元组不可变”并非内部所有东西都永远不变。
精讲:列表复制,到底复制到哪一层
先把名字想成指向对象的箭头。b = a 多画了一根箭头,并没有新建列表;b = a.copy() 新建一个外层列表,但外层里面装着的元素仍指向原来的对象。
以 a = [[1], [2]]、b = a.copy() 为例,先在纸上画出两个外层方框,再画两个内层方框。两个外层各自独立,但它们都指向那两个内层方框。
操作(各行都从上面的初始状态重新开始) a b 原因
b.append([3]) [[1], [2]] [[1], [2], [3]] 只改 b 的外层
b[0].append(9) [[1, 9], [2]] [[1, 9], [2]] 改了共享的内层
b[0] = [9] [[1], [2]] [[9], [2]] 只换 b 外层第 0 格的箭头
回到示例 07_05,逐行运行,再分别替换成表中的操作。不要把三个操作连着执行后拿结果与表比较:表中每一行都假设你重新初始化了 a 和 b。
迁移到会话记录: messages 常是“列表里面装字典”。外层 .copy() 不能隔离每条字典里的修改。如果只是给复制后的列表追加新消息,外层复制常已够用;如果还要修改旧消息里的字段,就需要明确哪些层应独立。第 08 章先学会读写字典,第 14 章再处理不同会话的独立状态。
动手看对象关系
赋值与浅拷贝为什么不同 选择写法 b = a(共享) b = a.copy()(复制外层)
上一步 下一步 重新开始
本演示的列表元素是整数;嵌套列表的浅拷贝请继续看示例 07_05。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 07_01
基本增删改查 运行文件:examples/ch07/ex07_01.py
Python 复制代码
tasks = ["阅读" , "练习" ]
tasks.append("复习" )
tasks[0 ] = "读第 7 章"
last = tasks.pop()
print(tasks)
print(last)预测完后,查看预期输出 怎么执行 append 加末尾,索引赋值改首项,pop 同时执行删除并返回被删内容。
动手改写: 用 insert 在最前面插入 "预习"。
示例 07_02
append 和 extend 运行文件:examples/ch07/ex07_02.py
Python 复制代码
a = [1 , 2 ]
a.append([3 , 4 ])
b = [1 , 2 ]
b.extend([3 , 4 ])
print(a)
print(b)预测完后,查看预期输出 程序输出
[1, 2, [3, 4]]
[1, 2, 3, 4]怎么执行 a 外层长度为 3,b 长度为 4。嵌套结构不同,后面的遍历方式也不同。
动手改写: 分别打印 len(a)、len(b)、a[-1][0]。
示例 07_03
排序与返回值 运行文件:examples/ch07/ex07_03.py
Python 复制代码
scores = [80 , 60 , 90 ]
ordered = sorted(scores)
print(scores)
print(ordered)
result = scores.sort()
print(scores)
print(result)预测完后,查看预期输出 程序输出
[80, 60, 90]
[60, 80, 90]
[60, 80, 90]
None怎么执行 sorted 没改 scores。sort 随后改变 scores,但它的返回值是 None。不要把“执行效果”与“返回值”混在一起。
动手改写: 用 reverse=True 做降序,并确保原数据保留。
示例 07_04
共享与重新绑定 运行文件:examples/ch07/ex07_04.py
Python 复制代码
a = [1 , 2 ]
b = a
b.append(3 )
print(a)
b = [9 ]
print(a)
print(b)预测完后,查看预期输出 程序输出
[1, 2, 3]
[1, 2, 3]
[9]怎么执行 append 改共享对象;b = [9] 创建并改绑到另一个对象。重新绑定 b 不会沿着旧关系去修改 a。
动手改写: 把 b = a 换成 b = a.copy(),比较第一条输出。
示例 07_05
浅拷贝的边界 运行文件:examples/ch07/ex07_05.py
Python 复制代码
import copy
a = [[1 ], [2 ]]
b = a.copy()
c = copy.deepcopy(a)
b[0 ].append(9 )
print(a)
print(b)
print(c)预测完后,查看预期输出 程序输出
[[1, 9], [2]]
[[1, 9], [2]]
[[1], [2]]怎么执行 b 外层独立,b[0] 却和 a[0] 共享内层列表;c 的常见嵌套列表被递归复制。import 的语法第 11 章系统学习。
动手改写: 尝试 b.append([3]),观察为什么不会给 a 增加第三项。
示例 07_06
二维列表共享陷阱 运行文件:examples/ch07/ex07_06.py
Python 复制代码
bad = [[0 ] * 2 ] * 2
bad[0 ][0 ] = 9
print(bad)
good = []
for _ in range(2 ):
good.append([0 ] * 2 )
good[0 ][0 ] = 9
print(good)预测完后,查看预期输出 程序输出
[[9, 0], [9, 0]]
[[9, 0], [0, 0]]怎么执行 重复的是内层对象的引用,不是重新创建两行。循环每轮产生新列表,因此 good 的两行彼此独立。下划线表示这个循环值本身不重要。
动手改写: 把网格变成 3 行 4 列,只修改最后一格。
示例 07_07
元组与解包 运行文件:examples/ch07/ex07_07.py
Python 复制代码
point = (3 , 4 )
x, y = point
print(x, y)
print(type((3 ,)).__name__)
print(type((3 )).__name__)预测完后,查看预期输出 怎么执行 逗号建立元组结构。解包按位置赋值,变量个数与元素个数不匹配会报错。
动手改写: 尝试 a, b, c = point,读懂错误后恢复。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 07_01 · 场景 我已独立完成
保留合格成绩 从 [30, 60, 85, 59] 中生成新列表 [60, 85],并输出。不要修改遍历中的原列表。
作答文件:practice/ch07/ex07_01.py
预期输出 给我一点提示 建立空列表,满足条件就 append。
查看参考代码与解析 Python 复制代码
passed = []
for score in [30 , 60 , 85 , 59 ]:
if score >= 60 :
passed.append(score)
print(passed)解题思路 筛选模式保留原顺序,也避开边遍历边删除导致的跳项。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 07_01
练习 07_02 · 场景 我已独立完成
最高三个分数 scores = [70, 95, 80, 60, 90],输出降序前三名 [95, 90, 80],再输出原列表。
作答文件:practice/ch07/ex07_02.py
预期输出 程序输出
[95, 90, 80]
[70, 95, 80, 60, 90]给我一点提示 sorted 返回新列表,然后 [:3]。
查看参考代码与解析 Python 复制代码
scores = [70 , 95 , 80 , 60 , 90 ]
print(sorted(scores, reverse=True )[:3 ])
print(scores)解题思路 两步分别做排序和取前缀。若不足三项,切片返回全部已有项,不会越界。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 07_02
练习 07_03 · 纠错 我已独立完成
修复复制错误 a = [1,2],复制给 b 后给 b 加入 3。要求输出 a 为 [1, 2],b 为 [1, 2, 3]。
作答文件:practice/ch07/ex07_03.py
预期输出 给我一点提示 用 copy,而不是仅赋值。
查看参考代码与解析 Python 复制代码
a = [1 , 2 ]
b = a.copy()
b.append(3 )
print(a)
print(b)解题思路 此题元素是不可变整数,浅拷贝足够。嵌套可变元素需要进一步考虑共享关系。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 07_03
练习 07_04 · 迁移 我已独立完成
移动末尾任务 tasks = ["A", "B", "C"],把最后一个移动到最前,输出 ["C", "A", "B"] 对应的 Python 列表表示。
作答文件:practice/ch07/ex07_04.py
预期输出 给我一点提示 pop 返回被删项,再 insert 到索引 0。
查看参考代码与解析 Python 复制代码
tasks = ["A" , "B" , "C" ]
last = tasks.pop()
tasks.insert(0 , last)
print(tasks)解题思路 题目默认列表非空。空列表调用 pop 会失败,真实函数可先判断 tasks。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 07_04
练习 07_05 · 迁移 我已独立完成
二维求和 grid = [[1,2,3],[4,5,6]],输出所有数字的总和 21。
作答文件:practice/ch07/ex07_05.py
预期输出 给我一点提示 外层取一行,内层取该行的一个数。
查看参考代码与解析 Python 复制代码
grid = [[1 , 2 , 3 ], [4 , 5 , 6 ]]
total = 0
for row in grid:
for value in row:
total += value
print(total)解题思路 len(grid) 是 2,不是 6。两层循环逐个访问叶子数值。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 07_05
练习 07_06 · 挑战 我已独立完成
按顺序去重 对 ["a","b","a","c","b"] 去重并保留首次出现顺序,输出 ["a","b","c"] 的列表表示。
作答文件:practice/ch07/ex07_06.py
预期输出 给我一点提示 若元素不在结果列表中,再 append。
查看参考代码与解析 Python 复制代码
unique = []
for item in ["a" , "b" , "a" , "c" , "b" ]:
if item not in unique:
unique.append(item)
print(unique)解题思路 本章用列表即可表达算法。大数据中反复做列表成员判断会较慢,第 08 章可增加 seen 集合提升通常的查找效率。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 07_06
练习 07_07 · 纠错 我已独立完成
修复 sort 赋值 values=[3,1,2]。修复 values=values.sort() 导致 None 的问题;要求保留 values 的列表并输出升序。
作答文件:practice/ch07/ex07_07.py
预期输出 给我一点提示 sort 自己修改原列表,不用接收返回值。
查看参考代码与解析 Python 复制代码
values = [3 , 1 , 2 ]
values.sort()
print(values)解题思路 另一种正确方法是 values=sorted(values)。关键是辨认方法的副作用与返回值。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 07_07
练习 07_08 · 挑战 我已独立完成
复制二维数表 original=[[1,2],[3,4]],用逐行 copy 建立新二维列表,修改新表左上角为 9。依次输出原表、新表。
作答文件:practice/ch07/ex07_08.py
预期输出 程序输出
[[1, 2], [3, 4]]
[[9, 2], [3, 4]]给我一点提示 外层新建,每行也新建;本题叶子为整数,复制两层足够。
查看参考代码与解析 Python 复制代码
original = [[1 , 2 ], [3 , 4 ]]
copied = []
for row in original:
copied.append(row.copy())
copied[0 ][0 ] = 9
print(original)
print(copied)解题思路 只 original.copy() 仍共享内层行。本题逐行 copy 解决两层结构,任意深层结构应重新分析或使用适当深拷贝。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 07_08
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 用键组织有名字的字段 用字典做计数和分组 选择合适容器并处理嵌套结构 字典是键到值的映射
student = {"name": "小林", "score": 85} 用字段名取值:student["score"]。键必须可哈希,常见字符串和整数可以作为键,列表不行。相同键只能保存一个当前值,再赋值会覆盖旧值。
student["city"] 在键不存在时抛 KeyError;student.get("city", "未知") 返回默认值但不把它写入字典。判断键是否存在可以用 "city" in student,字典上的 in 默认检查键,不检查值。
遍历方式要明确
for key in data 遍历键,data.values() 提供值,data.items() 提供键值对。常用 for key, value in data.items():。现代 Python 字典保留插入顺序,但这不代表按键的大小排序;需要排序就显式调用 sorted。
不要遍历字典时添加或删除键。可以先生成要删除的键列表,再进行删除;或创建新字典。只更新已有键的值是另一回事,但初学仍建议保持流程清楚。
计数的核心公式
counts[word] = counts.get(word, 0) + 1 的意思:先读旧次数,没有就视作 0,再加 1,最后写回。get 本身不负责更新。这种“读旧值 → 计算 → 写回”的模式与变量累计相同,只是状态按键分开保存。
一条记录与多条记录
字典表示一个学生,字典列表表示多个学生:[{"name":"A","score":80}, ...]。先用 for 取一个 record,再用 record["score"] 取该记录字段。不要对整个 records 直接写 records["score"],外层是列表,索引需要整数或切片。
面对复杂结构,先逐层问 type 和 len,再用一个很小的数据例子。很多所谓“AI 数据问题”本质是把列表、字典、字符串层次搞错。
集合用于去重和成员判断
set([1,1,2]) 得到只含 1 与 2 的集合。空集合写 set(),{} 是空字典。集合元素也需要可哈希;集合不提供索引,迭代顺序不作为程序结果约定。想稳定展示可排序元素,用 sorted。
交集 a & b 是两边都有;并集 a | b 是任一边有;差集 a - b 是只在 a;对称差 a ^ b 是只在一边。set 去重不保留你想要的原列表顺序;保序去重可组合 seen 集合与结果列表。
怎样选容器
需求 优先考虑 原因
有顺序、可增删的一组数据 list 按位置遍历和修改
固定结构的一组值 tuple 明确槽位结构且槽位不可改
字段名到字段值、词到次数 dict 通过键访问
唯一元素、快速成员判断 set 去重与集合运算
不要为了显得高级强行选择复杂结构。先画出一条数据,再决定“一条用什么、多条用什么”。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 08_01
读取与默认值 运行文件:examples/ch08/ex08_01.py
Python 复制代码
student = {"name" : "小林" , "score" : 85 }
student["score" ] = 90
print(student["name" ], student["score" ])
print(student.get("city" , "未知" ))
print("city" in student)预测完后,查看预期输出 怎么执行 get 返回默认值没有修改字典,所以最后仍为 False。
动手改写: 使用 student["city"] 读取,观察 KeyError。
示例 08_02
词频统计 运行文件:examples/ch08/ex08_02.py
Python 复制代码
counts = {}
for word in "python is fun python is useful" .split():
counts[word] = counts.get(word, 0 ) + 1
for word, count in counts.items():
print(word, count)预测完后,查看预期输出 程序输出
python 2
is 2
fun 1
useful 1怎么执行 每个词有独立计数器。插入顺序是首次见到这些词的顺序。
动手改写: 加上大小写和逗号,分析当前清洗规则还缺什么。
示例 08_03
记录列表 运行文件:examples/ch08/ex08_03.py
Python 复制代码
students = [
{"name" : "A" , "score" : 55 },
{"name" : "B" , "score" : 80 },
]
for student in students:
if student["score" ] >= 60 :
print(student["name" ])预测完后,查看预期输出 怎么执行 外层先取字典,内层再按键取字段。列表与字典各自承担一层组织任务。
动手改写: 给每条记录添加 city,再筛选城市与分数双条件。
示例 08_04
集合关系 运行文件:examples/ch08/ex08_04.py
Python 复制代码
train_ids = {1 , 2 , 3 }
test_ids = {3 , 4 }
print(sorted(train_ids & test_ids))
print(sorted(train_ids | test_ids))
print(sorted(train_ids - test_ids))预测完后,查看预期输出 程序输出
[3]
[1, 2, 3, 4]
[1, 2]怎么执行 交集发现 ID 3 重复出现在两组数据中。用于训练测试分离时,ID 检查只是第一层,还需考虑同源内容或同一用户泄漏。
动手改写: 让两组完全无交集,观察空交集的输出。
示例 08_05
按类别累计 运行文件:examples/ch08/ex08_05.py
Python 复制代码
records = [("书" , 30 ), ("餐饮" , 20 ), ("书" , 50 )]
totals = {}
for category, amount in records:
totals[category] = totals.get(category, 0 ) + amount
print(totals)预测完后,查看预期输出 怎么执行 先按类别找到对应累计值,再加本次金额。计数和金额累计只在“加 1 还是加 amount”上不同。
动手改写: 改成按类别统计笔数。
示例 08_06
保序去重与 seen 运行文件:examples/ch08/ex08_06.py
Python 复制代码
seen = set()
unique = []
for word in ["a" , "b" , "a" , "c" ]:
if word not in seen:
seen.add(word)
unique.append(word)
print(unique)预测完后,查看预期输出 怎么执行 seen 用于判断见没见过,unique 用于保存顺序。两个结构服务不同需求;不要直接输出集合当成有序结果。
动手改写: 解释为什么 seen.add 后仍需要 unique.append。
示例 08_07
AI / Agent:列表保存顺序,字典保存消息字段 运行文件:examples/ch08/ex08_07.py
Python 复制代码
messages = [
{"role" : "user" , "content" : "什么是列表?" },
{"role" : "assistant" , "content" : "列表保存有顺序的元素。" },
]
messages.append({"role" : "user" , "content" : "给我一个例子。" })
for message in messages:
print(f"{message['role']}: {message['content']}" )
print(len(messages))预测完后,查看预期输出 程序输出
user: 什么是列表?
assistant: 列表保存有顺序的元素。
user: 给我一个例子。
3怎么执行 messages 外层列表保存时间顺序;每个内层字典保存一条消息。messages[0] 取得第一条字典,再用 ['content'] 取得正文;for 每次让 message 指向一条字典。这里 role 只是我们约定的字段,assistant 文字是预先写好的,没有模型参与。真实服务的消息格式和角色语义要查对应接口。
动手改写: 只输出 role 为 user 的消息,再统计用户消息的条数。不要删除遍历中的元素;先用 if 选择要处理的记录。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 08_01 · 基础 我已独立完成
库存更新 stock = {"apple":3,"pear":2},苹果补货 5 个,新增 banana 4 个,输出 stock 的 Python 表示。
作答文件:practice/ch08/ex08_01.py
预期输出 程序输出
{'apple': 8, 'pear': 2, 'banana': 4}给我一点提示 已有键重新赋值,新键直接赋值。
查看参考代码与解析 Python 复制代码
stock = {"apple" : 3 , "pear" : 2 }
stock["apple" ] += 5
stock["banana" ] = 4
print(stock)解题思路 += 在这里先取出整数再加回;字典的键顺序由首次插入决定。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 08_01
练习 08_02 · 场景 我已独立完成
字母次数 统计 banana 每个字母出现次数,输出字典 {"b":1,"a":3,"n":2} 对应的 Python 表示。
作答文件:practice/ch08/ex08_02.py
预期输出 程序输出
{'b': 1, 'a': 3, 'n': 2}给我一点提示 遍历字符串,每个字符作为键。
查看参考代码与解析 Python 复制代码
counts = {}
for char in "banana" :
counts[char] = counts.get(char, 0 ) + 1
print(counts)解题思路 首次出现按 0 起步,后续读取已有次数。插入顺序 b、a、n 与出现次数大小无关。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 08_02
练习 08_03 · 基础 我已独立完成
读取可选配置 config = {"batch_size":16}。读取缺失的 epochs,默认 3,输出 3;再输出字典,证明没有新增键。
作答文件:practice/ch08/ex08_03.py
预期输出 给我一点提示 get 的第二个参数是默认值。
查看参考代码与解析 Python 复制代码
config = {"batch_size" : 16 }
print(config.get("epochs" , 3 ))
print(config)解题思路 默认值只是本次读取结果。如果要存入字典,需要显式赋值或学习 setdefault。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 08_03
练习 08_04 · 场景 我已独立完成
检测重复 ID ids = ["s1","s2","s1","s3"]。若有重复输出 True,否则 False。
作答文件:practice/ch08/ex08_04.py
预期输出 给我一点提示 比较原列表长度与集合长度。
查看参考代码与解析 Python 复制代码
ids = ["s1" , "s2" , "s1" , "s3" ]
print(len(ids) != len(set(ids)))解题思路 集合合并重复元素,长度变小意味着至少有一个重复。此方法只回答有没有,不能列出具体重复项及次数。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 08_04
练习 08_05 · 场景 我已独立完成
两班共同选课 a={"python","math"},b={"python","english"},输出共同课程的排序列表 ["python"]。
作答文件:practice/ch08/ex08_05.py
预期输出 给我一点提示 求交集后 sorted。
查看参考代码与解析 Python 复制代码
a = {"python" , "math" }
b = {"python" , "english" }
print(sorted(a & b))解题思路 排序是为了稳定展示,不是集合本身有顺序。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 08_05
练习 08_06 · 挑战 我已独立完成
按城市分组 记录 [{"name":"A","city":"台北"},{"name":"B","city":"高雄"},{"name":"C","city":"台北"}],构建城市到姓名列表的字典并输出。
作答文件:practice/ch08/ex08_06.py
预期输出 程序输出
{'台北': ['A', 'C'], '高雄': ['B']}给我一点提示 遇到新城市先创建空列表,再 append。
查看参考代码与解析 Python 复制代码
records = [
{"name" : "A" , "city" : "台北" },
{"name" : "B" , "city" : "高雄" },
{"name" : "C" , "city" : "台北" },
]
groups = {}
for record in records:
city = record["city" ]
if city not in groups:
groups[city] = []
groups[city].append(record["name" ])
print(groups)解题思路 每个新城市得到自己独立的列表。不要把同一个预建列表反复赋给多个城市,否则它们会共享内容。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 08_06
练习 08_07 · 场景 我已独立完成
按标签统计 records=[{"label":1},{"label":0},{"label":1}],输出标签到数量的字典 {1:2,0:1}。
作答文件:practice/ch08/ex08_07.py
预期输出 给我一点提示 先取 record["label"],再对该键累计。
查看参考代码与解析 Python 复制代码
records = [{"label" : 1 }, {"label" : 0 }, {"label" : 1 }]
counts = {}
for record in records:
label = record["label" ]
counts[label] = counts.get(label, 0 ) + 1
print(counts)解题思路 这是类别分布的最小统计,有助于发现标签不平衡。JSON 保存这种字典时整数键会变成字符串,读回时不要假定类型不变。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 08_07
练习 08_08 · 迁移 我已独立完成
读取嵌套配置 config={"training":{"batch_size":8,"epochs":3}},输出 batch_size 与 epochs 的乘积 24。
作答文件:practice/ch08/ex08_08.py
预期输出 给我一点提示 先取 training 得到内层字典。
查看参考代码与解析 Python 复制代码
config = {"training" : {"batch_size" : 8 , "epochs" : 3 }}
training = config["training" ]
print(training["batch_size" ] * training["epochs" ])解题思路 逐层命名帮助观察结构。这里乘积只作语法练习,不代表训练步数,真实步数还取决于数据量和批数。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 08_08
练习 08_09 · 迁移 我已独立完成
AI / Agent:统计用户消息 messages 为 [{"role": "user", "content": "hi"}, {"role": "assistant", "content": "hello"}, {"role": "user", "content": "bye"}]。只输出用户消息正文,并在最后输出用户消息数。所有记录都保证有这两个键。
作答文件:practice/ch08/ex08_09.py
预期输出 给我一点提示 count 从 0 开始;只有 role 等于 user 时才打印正文并加 1。
查看参考代码与解析 Python 复制代码
messages = [
{"role" : "user" , "content" : "hi" },
{"role" : "assistant" , "content" : "hello" },
{"role" : "user" , "content" : "bye" },
]
count = 0
for message in messages:
if message["role" ] == "user" :
print(message["content" ])
count += 1
print(count)解题思路 先取 role 决定是否进入分支,再取 content。count 不应在分支外增加,否则会把 assistant 也算进去。空列表没有任何正文输出,最后应输出 0;将所有 role 改为 assistant 也应得到 0。字段缺失的处理留到第 13 章,不能把本题的输入保证推广到任何外部数据。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 08_09
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 定义并调用函数,写清输入输出约定 区分 print、return 和 None 理解作用域、参数传递与默认参数陷阱 追踪连续调用中的旧值、返回值与接收位置 为什么需要函数
同一段“清洗文本、计算平均分、检查范围”的代码如果复制三次,改规则时就要改三处。函数把一个明确任务装进有名字的代码块,调用时传入数据,再得到结果。好函数名通常是动词,例如 clean_text、calculate_total。
基本形式是 def 函数名(参数):,缩进部分叫函数体。def 执行时创建函数,并不立即跑函数体;写 函数名(实际值) 才是调用。形参是定义中的名字,实参是调用时传入的值。例如 def double(number) 里的 number 是形参,double(5) 里的 5 是实参。
从调用到返回的四步
看到 answer = add(2, 3):先求实参得到 2 和 3;进入函数,把它们分别绑定给 a 和 b;执行函数体并求出 5;return 把 5 交回调用位置,最后 answer 绑定 5。把函数调用想成表达式中的一个可求值步骤,而不只是“跳到另一段代码”。
print 与 return 必须分清
print 把内容显示到屏幕,便于人看;return 把对象交还给调用者,便于后续程序继续使用。一个只 print 没有 return 的函数,调用结果默认是 None。print(add(2, 3)) 先由 add 返回值,再让 print 显示它。
执行 return 后,当前函数立即结束,后面的函数体语句不执行。所有需要结果的路径都应有明确返回,否则有些输入会意外得到 None。返回多个值时常用 return total, count,实际返回的是一个元组,调用者可以解包。
参数的几种写法
位置参数按顺序对应;关键字参数按名字对应。power(3, exponent=2) 更容易看出第二项含义。def power(base, exponent=2) 给了默认值,调用者可以省略 exponent。普通调用中位置参数放在关键字参数前面,避免给同一个参数赋值两次。
默认值在函数定义时求值,不是每次调用重新创建。不要使用 items=[] 作为想要“每次一个新列表”的默认值,改为 items=None,进入后 if items is None: items=[]。这个检查必须用 is None,不能简单 if not items,否则调用者传入的空列表也会被替换。
作用域决定去哪里找名字
函数内部赋值创建的名字通常是局部变量,外面不能直接使用。函数可以读取外层可见名字,但把所有输入写成参数更容易理解和测试。不要一开始就依赖 global 修改全局变量。
如果函数里给 x 赋值,Python 通常把该函数中的 x 当作局部名字,即使你在赋值之前试图读外部 x,也可能得到 UnboundLocalError。改为传参和返回值,比到处加 global 更清楚。
传入列表会不会被修改
函数参数获得的是对象引用的绑定。函数内执行 items.append(x) 会修改调用者共享的列表;执行 items = [] 只改变局部参数的绑定,不会替换调用者的名字。不能简单说“Python 都是值传递”或“所有赋值都修改外部”。要看是否修改了同一个对象。
若希望函数不改输入,可先建立新列表再处理。题目应声明是否允许修改输入,测试也应检查这一点。
写函数前的输入输出约定
先用一句话描述职责;列出输入类型、合法范围、返回值;决定空输入怎么办;给出至少三个样例。平均分函数可约定空列表返回 None,也可约定抛 ValueError,两种都可能合理,关键在于调用者知道并处理。不要让约定靠读函数内部猜。
本章开始的部分习题附带额外函数测试。你除了打印展示结果,还要按指定函数名与参数实现功能;自检器会用其他输入调用函数,检查边界和是否修改了输入。
精讲:把一次函数调用拆成五个时刻
读示例 09_01 时,按“实参 → 形参 → 函数体 → 返回值 → 接收位置”追踪。对于 answer = add(2, 3):
时刻 正在发生什么 目前能知道什么
1 计算调用括号里的 2 和 3 准备两个实参
2 进入 add,建立局部名字 a 和 b a 是 2,b 是 3
3 计算 a + b 得到 5,但外面的赋值尚未完成
4 执行 return 5 本次调用结束,add(2, 3) 的值是 5
5 调用位置继续赋值 answer 现在是 5
def 出现时先定义函数,函数体要等调用才运行。形参是函数定义里的名字,实参是调用时提供的值。第二次调用会有新的一次局部执行环境。
精讲:print 与 return 分别通向哪里
可以把 print 看作显示窗口,把 return 看作把结果交回调用位置。函数可以一边打印、一边返回,两件事互不替代。没有执行到带值的 return 时,正常结束的函数返回 None;None 是一个值,表示这里没有提供具体结果,不是字符串 "None"。
先运行示例 09_02,只把函数体中的 print(x * 2) 改成 return x * 2,预测输出会从几行变为几行。外面的 print(answer) 保持原样,这样才能单独观察返回值的变化。
为什么 AI 工具要 return? 后面的程序需要拿到结果继续处理。若工具只打印“找到 3 条记录”,调用者拿到的可能是 None,不能把它当成列表遍历。调试日志可以 print;供下游使用的数据要设计明确的返回结构。
精讲:局部名字变化与共享对象变化
示例 09_05 中 value = value + 10 让局部名字 value 指向新整数,外面的 score 仍指向旧整数。示例 09_06 中 items.append(...) 修改双方指向的同一个列表。两者都符合“参数先绑定到传入的对象”,不需要死记“列表传引用、整数传值”这种容易误导的口诀。
默认列表陷阱的检查方法:连续调用两次,再观察第二次是否带上第一次的记录。修复时用 items is None 判断有没有提供列表;不要用 if not items,否则调用者明确传入的空列表也会被替换。这在需要修改调用者列表时会改变约定。
闭卷小问: 函数中有三个 return,一次调用最多执行几个?答案是一个;执行到第一个 return 后,本次调用就结束。不同输入可以走到不同的 return。
精讲:为什么函数调用了三次,外面的计数仍是 0
假设 next_round(current) 返回 current + 1。写 next_round(rounds) 只计算出新值,没有把它存回 rounds;写 rounds = next_round(rounds) 才会更新调用位置的名字。记住这条完整路线:旧值作为实参进入 → 函数算出新值 → return 交回 → 调用者赋值保存 。
写法 三次调用各自收到的 current 三次返回值 最后的 rounds
只写 next_round(rounds) 0、0、0 1、1、1,但没保存 0
写 rounds = next_round(rounds) 0、1、2 1、2、3,逐次保存 3
这两行写法可以放进相同的 for 循环,分别运行验证。每次普通函数调用都有自己的局部名字;整数值不会因为函数里给形参重新赋值,就自动改变调用者的名字。第 09 章新增示例 09_10 让你同时观察两个调用方式。
实操:把任务预算写成明确的输入与返回值
想象一个离线 Agent 练习,每做一步扣掉一部分预算。先定义 remaining_budget(remaining, used) 只负责计算“还剩多少”,再由外面决定是否保存新预算、打印或继续执行。这样可以分别手算单步结果和连续多步的结果。
给自己画三列:调用前余额、传入的 used、返回的新余额。先从 60 扣 15,再从新余额扣 50;按本题约定余额最低为 0,应得到 45、0。若两次都拿原始 60 作为输入,第二次会得到 10,那是调用方的数据流写错了,未必是函数公式写错。
闭卷检查: return 是否等于“修改调用者变量”?不等于。修改列表对象、重新绑定局部名字、返回一个值,是三个需要分别观察的动作。
作用域报错的下一站
在函数里直接写 rounds += 1,却没有把 rounds 设成参数或先在本次调用中赋值,会涉及 UnboundLocalError。第 13 章的示例 13_07 提供可运行的错误与修复对照;它会用该章学过的 try/except 捕获错误,便于继续观察修复结果。此处先练好参数与返回值,不必提前记异常处理语法。
动手看调用与返回
answer = add(2, 3) 经历了什么
上一步 下一步 重新开始
观察参数如何进入函数,返回值如何交回调用位置。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 09_01
定义与调用 运行文件:examples/ch09/ex09_01.py
Python 复制代码
def add(a, b):
return a + b
result = add(2 , 3 )
print(result)
print(add(10 , 20 ) * 2 )预测完后,查看预期输出 怎么执行 第一处先计算 2+3 再给 result。第二处先得到 30,再在调用处乘 2。函数体不依赖特定数字,可用于不同输入。
动手改写: 改写成 subtract(a,b),说明参数顺序为何重要。
示例 09_02
打印不等于返回 运行文件:examples/ch09/ex09_02.py
Python 复制代码
def show_double(number):
print(number * 2 )
result = show_double(3 )
print(result)预测完后,查看预期输出 怎么执行 函数先显示 6;没有 return,所以调用表达式的值是 None,第二个 print 显示它。
动手改写: 改成 return number * 2,再由外部显示结果。
示例 09_03
提前返回处理空输入 运行文件:examples/ch09/ex09_03.py
Python 复制代码
def mean(values):
if not values:
return None
return sum(values) / len(values)
print(mean([80 , 100 ]))
print(mean([]))预测完后,查看预期输出 怎么执行 sum 是内置求和函数。空输入路径提前返回,后面的除法不会执行;调用者必须理解 None 表示没有平均值。
动手改写: 将空输入约定改成抛 ValueError,并在第 13 章练习捕获。
示例 09_04
默认参数与关键字 运行文件:examples/ch09/ex09_04.py
Python 复制代码
def greet(name, prefix="你好" ):
return f"{prefix},{name}"
print(greet("小林" ))
print(greet(prefix="晚上好" , name="小陈" ))预测完后,查看预期输出 怎么执行 第一调用省略 prefix,用默认值。第二调用按名字对应,书写次序不影响参数绑定。
动手改写: 调用 greet("小林", name="小陈"),观察重复赋值错误。
示例 09_05
作用域与返回 运行文件:examples/ch09/ex09_05.py
Python 复制代码
score = 60
def improve(value):
value = value + 10
return value
new_score = improve(score)
print(score, new_score)预测完后,查看预期输出 怎么执行 局部 value 重新绑定到 70,不会把外部 score 改成 70。想更新外部名字可显式写 score = improve(score)。
动手改写: 删除 return,观察 new_score。
示例 09_06
修改对象与改绑参数 运行文件:examples/ch09/ex09_06.py
Python 复制代码
def add_item(items):
items.append("B" )
def replace_items(items):
items = ["C" ]
tasks = ["A" ]
add_item(tasks)
replace_items(tasks)
print(tasks)预测完后,查看预期输出 怎么执行 add_item 对共享列表追加 B;replace_items 只把局部 items 改绑到新列表。函数返回后这个局部绑定消失。
动手改写: 让 replace_items 返回新列表,并由外面 tasks = replace_items(tasks)。
示例 09_07
默认列表陷阱与修复 运行文件:examples/ch09/ex09_07.py
Python 复制代码
def bad_add(value, items=[]):
items.append(value)
return items
print(bad_add("A" ))
print(bad_add("B" ))
def good_add(value, items=None ):
if items is None :
items = []
items.append(value)
return items
print(good_add("A" ))
print(good_add("B" ))预测完后,查看预期输出 程序输出
['A']
['A', 'B']
['A']
['B']怎么执行 bad_add 的默认列表在定义时创建一次,多次调用共享。good_add 在每次省略 items 时创建新列表;若显式传入列表,仍会修改那个列表。
动手改写: 显式传入 items=[],验证 good_add 保留对该列表的修改。
示例 09_08
多个结果与文档字符串 运行文件:examples/ch09/ex09_08.py
Python 复制代码
def summarize(values):
"""返回数值列表的总和与项数。"""
return sum(values), len(values)
total, count = summarize([2 , 4 , 6 ])
print(total, count)预测完后,查看预期输出 怎么执行 函数体第一条字符串常作为文档字符串,可用 help(summarize) 查看。return 的逗号把两个值组成元组。
动手改写: 再返回最大值,并为无元素情况写约定。
示例 09_09
AI / Agent:工具函数把结果交回调用者 运行文件:examples/ch09/ex09_09.py
Python 复制代码
def count_words(text):
words = text.split()
return {"count" : len(words), "empty" : len(words) == 0 }
result = count_words(" learn Python today " )
print(result["count" ])
print(result["empty" ])
print(count_words("" )["count" ])预测完后,查看预期输出 怎么执行 输入字符串进入 text;split 得到三个按空白分开的词;函数构造字典并 return;外层 result 接收字典后取字段。函数体不打印,所以可以重复用于统计、测试或保存文件。它要求 text 是字符串;第 13 章再学习怎样验证来自外部的数据。按空白分词只适合此处演示,既不是通用中文分词器,也不是模型 tokenizer。
动手改写: 把 return 改成 print 字典,再运行,观察 result 的值为什么变成 None。修复后传入纯空白字符串,预测 count 和 empty。
示例 09_10
AI / Agent:返回的新轮数要由调用者接住 运行文件:examples/ch09/ex09_10.py
Python 复制代码
def next_round(current):
return current + 1
rounds = 0
for _ in range(3 ):
next_round(rounds)
print(f"未接收:{rounds}" )
for _ in range(3 ):
rounds = next_round(rounds)
print(f"保存后:{rounds}" )预测完后,查看预期输出 程序输出
未接收:0
保存后:1
保存后:2
保存后:3怎么执行 第一个循环每次都传入 0,并得到 1,但没有变量接收这三个返回值。第二个循环的右边先计算 next_round(rounds),然后左边 rounds 才绑定新值;所以下一轮能接着上次的结果计算。下划线只是一个变量名,表示本例不需要循环索引。这是离线计数实验,没有执行真实 Agent 或模型调用。
动手改写: 把初始 rounds 改为 5,先写出全部四行输出再运行。随后只删掉第二个循环中的赋值,解释为什么三次打印都不再增长。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 09_01 · 基础 我已独立完成
温度转换函数 实现 to_fahrenheit(celsius),返回摄氏转华氏的值。展示 print(to_fahrenheit(25)) 输出 77.0。
作答文件:practice/ch09/ex09_01.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 函数负责返回,外面负责显示。
查看参考代码与解析 Python 复制代码
def to_fahrenheit(celsius):
return celsius * 9 / 5 + 32
print(to_fahrenheit(25 ))解题思路 把第 02 章公式放进函数,调用者可以多次使用不同输入。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 09_01
练习 09_02 · 场景 我已独立完成
安全平均值 实现 mean(values),非空返回平均值,空列表返回 None,不修改输入。依次打印 mean([60,90]) 与 mean([])。
作答文件:practice/ch09/ex09_02.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 先检查空,再除以长度。
查看参考代码与解析 Python 复制代码
def mean(values):
if not values:
return None
return sum(values) / len(values)
print(mean([60 , 90 ]))
print(mean([]))解题思路 空平均没有本题定义下的数值。用 None 表达缺失,而非用 0 冒充。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 09_02
练习 09_03 · 迁移 我已独立完成
限制范围 实现 clamp(value, low, high),假定 low<=high:低于 low 返回 low,高于 high 返回 high,否则原样返回。打印 clamp(120,0,100)。
作答文件:practice/ch09/ex09_03.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 依次判断上下界。
查看参考代码与解析 Python 复制代码
def clamp(value, low, high):
if value < low:
return low
if value > high:
return high
return value
print(clamp(120 , 0 , 100 ))解题思路 最后 return 覆盖区间内部与两个端点。别漏掉“都不越界”这条路径。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 09_03
练习 09_04 · 场景 我已独立完成
清洗一组姓名 实现 clean_names(names),去每项两端空白,丢弃空结果,保留其余顺序且不修改输入。打印对 [" A "," ","B"] 的结果。
作答文件:practice/ch09/ex09_04.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 每轮先 strip,再判断 clean 是否非空。
查看参考代码与解析 Python 复制代码
def clean_names(names):
result = []
for name in names:
clean = name.strip()
if clean:
result.append(clean)
return result
print(clean_names([" A " , " " , "B" ]))解题思路 结果建立在新列表中。空白字符串经 strip 变空后再过滤,否则会保留无内容项。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 09_04
练习 09_05 · 挑战 我已独立完成
统计通过率 实现 pass_rate(scores, threshold=60),返回通过人数/总人数,空列表返回 None。打印 pass_rate([50,60,90]),用 .1% 显示为 66.7%。
作答文件:practice/ch09/ex09_05.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 把计数和平均逻辑结合;结果是 0 到 1 的比例。
查看参考代码与解析 Python 复制代码
def pass_rate(scores, threshold=60 ):
if not scores:
return None
count = 0
for score in scores:
if score >= threshold:
count += 1
return count / len(scores)
print(f"{pass_rate([50, 60, 90]):.1%}" )解题思路 threshold 可以通过关键字修改。显示百分比放在外部,函数返回数值便于后续运算。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 09_05
练习 09_06 · 纠错 我已独立完成
无共享默认值 实现 append_new(value, items=None),未传 items 时每次建立独立列表;传入列表时追加到该列表。依次打印 append_new("A") 与 append_new("B")。
作答文件:practice/ch09/ex09_06.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 必须检查 is None。
查看参考代码与解析 Python 复制代码
def append_new(value, items=None ):
if items is None :
items = []
items.append(value)
return items
print(append_new("A" ))
print(append_new("B" ))解题思路 默认值只用不可变的 None 作标记。显式提供空列表时仍保留该对象,因此测试也检查对象身份。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 09_06
练习 09_07 · 迁移 我已独立完成
是否回文 实现 is_palindrome(text),按 lower 后 split/join 去除全部空白,再比较正反文本。打印对 "Never odd or even" 的结果。标点本题不删除。
作答文件:practice/ch09/ex09_07.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 用 "".join(...) 去空白,再与 [::-1] 比较。
查看参考代码与解析 Python 复制代码
def is_palindrome(text):
cleaned = "" .join(text.lower().split())
return cleaned == cleaned[::-1 ]
print(is_palindrome("Never odd or even" ))解题思路 先写清规范化规则。按本题定义空字符串也满足与逆序相等,不同需求可以另作约定。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 09_07
练习 09_08 · 场景 我已独立完成
优惠后的新列表 实现 discounted(prices, rate=0.9),返回每个价格乘 rate 的新列表,不改输入。打印 [100,200] 的默认结果。
作答文件:practice/ch09/ex09_08.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 用一个新列表收集各价格的折后值。
查看参考代码与解析 Python 复制代码
def discounted(prices, rate=0.9 ):
result = []
for price in prices:
result.append(price * rate)
return result
print(discounted([100 , 200 ]))解题思路 rate=0.9 表示付原价的 90%,不是减去 90%。此题使用浮点练习,精确金额仍建议整数分或 Decimal。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 09_08
练习 09_09 · 场景 我已独立完成
AI / Agent:先约定工具的输入和返回结构 实现 summarize_text(text),输入保证是字符串。先 strip;返回字典,字段 chars 为清理后的长度、preview 为前 4 个字符、empty 表示是否为空。函数不能打印。用 " Python " 调用,依次打印三个字段。
作答文件:practice/ch09/ex09_09.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 先保存 cleaned,再构造一个字典 return。切片 [:4] 遇到不足 4 个字符时不会越界报错。
查看参考代码与解析 Python 复制代码
def summarize_text(text):
cleaned = text.strip()
return {
"chars" : len(cleaned),
"preview" : cleaned[:4 ],
"empty" : cleaned == "" ,
}
result = summarize_text(" Python " )
print(result["chars" ])
print(result["preview" ])
print(result["empty" ])解题思路 清理后的 Python 长度为 6,前四字符是 Pyth。把计算与输出分开后,调用者可以只显示 preview,或把整个结果写成 JSON。边界:空白返回 chars=0、preview=''、empty=True;短字符串原样作为预览。本题额外检查空白、短字符串和函数没有打印的约定。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 09_09
练习 09_10 · 迁移 我已独立完成
AI / Agent:把剩余预算逐步传下去 实现 remaining_budget(remaining, used),两个输入保证为非负整数,返回扣减后的余额,最低为 0;函数不得打印或依赖全局余额。先设 budget=60,接收扣 15 的结果并打印;再接收扣 50 的结果并打印。
作答文件:practice/ch09/ex09_10.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 函数先计算 remaining - used,负数返回 0,否则返回计算值。调用时要把返回值赋给 budget,再进行下一次调用。
查看参考代码与解析 Python 复制代码
def remaining_budget(remaining, used):
result = remaining - used
if result < 0 :
return 0
return result
budget = 60
budget = remaining_budget(budget, 15 )
print(budget)
budget = remaining_budget(budget, 50 )
print(budget)解题思路 第一步 60-15 得到 45,第二步把 45 传回函数,45-50 小于 0,按约定返回 0。若没有保存第一次的返回值,就会错误地从 60 再扣 50。本题的预算是人为设定的整数额度,不是 API 账单或模型 token 数。边界:扣 0 不变、恰好扣完为 0、超过余额也为 0;重复输入应得到相同结果。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 09_10
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 将简单循环转成推导式 用 key 指定排序依据 使用 enumerate、zip、any 与 all 列表推导式按“结果 遍历 条件”读
[x * x for x in numbers if x > 0]:遍历 numbers;只保留大于 0 的 x;对每个保留项计算 x*x;收集到新列表。写法里结果在前,执行时仍先从遍历中拿到 x。
初学先展开成四行:result=[];for;if;append。展开后理解了再缩写。列表推导式会一次生成完整列表;并非更省内存。多个嵌套 for 可以表达展开矩阵,但可读性差时普通循环更好。
两种 if 写法的区别
[x for x in nums if x > 0] 是筛选,会减少元素数量;[x if x > 0 else 0 for x in nums] 是逐项选择结果,每个输入都有一个输出。前者的 if 在 for 后;后者的 if/else 属于前面的结果表达式。
字典推导式如 {word: len(word) for word in words};集合推导式如 {word.lower() for word in words}。字典若产生重复键,后面的值覆盖前面。
排序的 key 是“拿什么来比”
sorted(records, key=...) 默认会用 key 返回的值排序。可以先定义普通函数 def get_score(record): return record["score"]。lambda record: record["score"] 是只含一个表达式的小函数,不是必须掌握的炫技写法。
多条件排序可返回元组,例如 key=lambda r: (-r["score"], r["name"]) 表示分数降序,同分姓名升序。先比较第一项,相同再看第二项。Python 排序是稳定的,键相等时保留输入相对顺序。
zip 把多个序列并排配对
zip(names, scores) 每次给一个姓名和一个分数。默认遇到较短序列结束,较长序列余下元素会被忽略。需要一一对应时,先检查长度,或在本教材支持的 Python 3.10+ 中使用 zip(..., strict=True),不等长会报 ValueError。
enumerate 给单个序列配索引;zip 把多个序列配成行。zip 和 enumerate 返回可迭代工具对象,要观察所有结果可 list(...),但遍历时不必先转列表。
any 与 all
any(条件序列) 问是否至少一个为真;all(条件序列) 问是否全部为真。空输入时 any 返回 False,all 返回 True。这是逻辑约定,不能用 all([]) 推断“有数据而且合格”,业务上还要检查非空。
先保证正确,再追求简洁
一行代码不一定更容易维护。清洗文本、解析数字、记录错误同时发生时,普通 for 更清楚。函数职责明确、变量名直白、测试覆盖边界,通常比把所有代码缩成一行重要。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 10_01
循环与推导式等价 运行文件:examples/ch10/ex10_01.py
Python 复制代码
numbers = [-2 , 0 , 3 , 4 ]
result = []
for number in numbers:
if number > 0 :
result.append(number ** 2 )
print(result)
print([number ** 2 for number in numbers if number > 0 ])预测完后,查看预期输出 怎么执行 两种写法都先筛正数,再平方。不是对所有数平方后再筛正数,否则 -2 也会留下。
动手改写: 写一个只保留长度至少 3 的单词的推导式。
示例 10_02
过滤与替换 运行文件:examples/ch10/ex10_02.py
Python 复制代码
numbers = [-2 , 0 , 3 ]
print([x for x in numbers if x > 0 ])
print([x if x > 0 else 0 for x in numbers])预测完后,查看预期输出 怎么执行 第一行长度变为 1;第二行长度仍为 3,负数与零都按规则映射到 0。
动手改写: 把替换值改成 None,说明缺失标记和数值零的区别。
示例 10_03
按记录字段排序 运行文件:examples/ch10/ex10_03.py
Python 复制代码
records = [
{"name" : "B" , "score" : 90 },
{"name" : "A" , "score" : 90 },
{"name" : "C" , "score" : 80 },
]
ordered = sorted(records, key=lambda row: (-row["score" ], row["name" ]))
print([row["name" ] for row in ordered])预测完后,查看预期输出 怎么执行 分数取负实现第一项降序,姓名保持升序。同为 90 时 A 排在 B 前。
动手改写: 改成分数升序、同分姓名升序。
示例 10_04
配对与序号 运行文件:examples/ch10/ex10_04.py
Python 复制代码
names = ["A" , "B" ]
scores = [80 , 90 ]
for index, (name, score) in enumerate(zip(names, scores, strict=True ), start=1 ):
print(index, name, score)预测完后,查看预期输出 怎么执行 zip 先产生 (name,score) 对,enumerate 再在外面配序号。左侧嵌套解包与右侧结构匹配。
动手改写: 给 scores 多加一个元素,观察 strict 的错误。
示例 10_05
全部与至少一个 运行文件:examples/ch10/ex10_05.py
Python 复制代码
scores = [50 , 80 , 90 ]
print(any(score >= 60 for score in scores))
print(all(score >= 60 for score in scores))
print(any([]), all([]))预测完后,查看预期输出 程序输出
True
False
False True怎么执行 这里括号内是生成器表达式,第 15 章详细解释。any 找到真就可停止,all 找到假就可停止。
动手改写: 把 scores 换成空列表,补上非空判断表达“所有已有学生均通过且确实有学生”。
示例 10_06
字典推导式 运行文件:examples/ch10/ex10_06.py
Python 复制代码
words = ["cat" , "python" , "AI" ]
lengths = {word: len(word) for word in words}
print(lengths)预测完后,查看预期输出 程序输出
{'cat': 3, 'python': 6, 'AI': 2}怎么执行 每个输入生成一个键值对。重复单词会覆盖同一键,因此结果不一定与输入长度相等。
动手改写: 用 lower 生成集合,给重复大小写单词去重。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 10_01 · 基础 我已独立完成
平方正数 实现 positive_squares(values),返回正数的平方列表,保持顺序;打印对 [-2,0,3] 的结果。
作答文件:practice/ch10/ex10_01.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 只保留 >0 的元素。
查看参考代码与解析 Python 复制代码
def positive_squares(values):
return [value ** 2 for value in values if value > 0 ]
print(positive_squares([-2 , 0 , 3 ]))解题思路 先筛再算;负数的平方虽为正,但原输入不符合条件。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 10_01
练习 10_02 · 场景 我已独立完成
单词按长度排序 实现 sort_words(words),长度升序,同长度字母升序,不改输入。打印对 ["dog","a","cat","python"] 的结果。
作答文件:practice/ch10/ex10_02.py
预期输出 程序输出
['a', 'cat', 'dog', 'python']本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 key 返回 (len(word), word)。
查看参考代码与解析 Python 复制代码
def sort_words(words):
return sorted(words, key=lambda word: (len(word), word))
print(sort_words(["dog" , "a" , "cat" , "python" ]))解题思路 长度先决定分组,字母顺序决定同长词的先后。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 10_02
练习 10_03 · 迁移 我已独立完成
负值截为零 实现 replace_negative(values),负数变 0,其他数不变。打印对 [-2,0,5] 的结果。
作答文件:practice/ch10/ex10_03.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 这里是替换,不是过滤。
查看参考代码与解析 Python 复制代码
def replace_negative(values):
return [0 if value < 0 else value for value in values]
print(replace_negative([-2 , 0 , 5 ]))解题思路 输入输出长度相等。筛掉负数会改变位置对应关系,可能损坏样本与标签的配对。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 10_03
练习 10_04 · 场景 我已独立完成
建立姓名成绩映射 实现 make_scores(names, scores),同长度且姓名唯一时返回字典;长度不同抛 ValueError。打印 ["A","B"] 与 [80,90] 的结果。
作答文件:practice/ch10/ex10_04.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 zip 使用 strict=True。
查看参考代码与解析 Python 复制代码
def make_scores(names, scores):
return dict(zip(names, scores, strict=True ))
print(make_scores(["A" , "B" ], [80 , 90 ]))解题思路 strict 捕获长度不一致。姓名唯一是本题前提;若重复需要另行确定覆盖、报错或分组的业务规则。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 10_04
练习 10_05 · 推理 我已独立完成
所有项均有效 实现 all_positive(values),必须非空且每个数字大于 0 才返回 True。依次打印对 [1,2] 和 [] 的结果。
作答文件:practice/ch10/ex10_05.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 bool(values) 与 all 组合。
查看参考代码与解析 Python 复制代码
def all_positive(values):
return bool(values) and all(value > 0 for value in values)
print(all_positive([1 , 2 ]))
print(all_positive([]))解题思路 单独 all([]) 是 True,不符合本题业务约定。显式的非空条件解决这一差异。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 10_05
练习 10_06 · 挑战 我已独立完成
展开二维列表 实现 flatten(rows),按行展开一层二维列表。打印 [[1,2],[],[3]] 的结果 [1,2,3]。
作答文件:practice/ch10/ex10_06.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 先 for row,再 for value in row。
查看参考代码与解析 Python 复制代码
def flatten(rows):
return [value for row in rows for value in row]
print(flatten([[1 , 2 ], [], [3 ]]))解题思路 多个 for 的顺序与普通嵌套循环一致。此函数只展开一层,不递归展开任意深度结构。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 10_06
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 理解 import 和模块边界 会使用常见标准库 知道解释器、pip 与虚拟环境的对应关系 模块就是组织代码的文件
一个 .py 文件可以作为模块。import math 导入模块,然后 math.sqrt(9) 使用其中的函数。from math import sqrt 直接导入名字,调用时写 sqrt(9)。前者来源清晰,后者更短;避免 from module import *,它会让名字来源难以追踪。
Python 首次导入模块时会执行模块顶层代码,然后通常缓存模块。因此不要在工具模块顶层直接开启交互输入、训练过程或大量文件写入。函数定义放顶层可以,真正运行的入口放在 main 保护下。
入口保护是什么意思
if __name__ == "__main__": 判断这个文件是否作为入口直接运行。如果直接 python app.py,条件为真;如果另一个文件 import app,条件通常为假。常见结构是定义 main(),然后在这条 if 中调用 main()。
这让同一个文件既可直接运行,也可被测试或其他文件导入复用。入口保护不是防止别人使用代码的安全机制,它只是运行方式判断。
两个文件怎样合作
建立同一文件夹中的 helpers.py,定义 def double(x): return x*2;另一个 main.py 写 from helpers import double,再调用它。从该文件夹启动 main.py。不要把你的文件命名为 random.py、json.py、csv.py 或 math.py,否则可能遮住标准库并造成看起来奇怪的导入错误。
模块搜索与解释器路径有关。发生 ModuleNotFoundError 时,先检查名称、当前使用的解释器和项目结构,不要反复随机安装。import sys; print(sys.executable) 可显示当前解释器路径。
标准库与第三方包
标准库随正常的 Python 安装提供,例如 math、random、pathlib、json、csv、collections、datetime、statistics、unittest。第三方包需要额外安装,例如 NumPy 和 PyTorch。本教材第一阶段只用标准库,你无需联网安装这些库才能完成作业。
pip 是安装 Python 包的工具。优先 python -m pip ...,意思是让“这个 Python”运行 pip,减少包装到另一个解释器里的问题。包名和导入名不一定相同,要看包的官方文档。
虚拟环境为什么有用
不同项目可能需要不同版本依赖。venv 为每个项目建立独立的解释器入口和包目录。它不是虚拟机,也不需要给每个练习建一个环境;整个学习包用一个即可。第一阶段没有第三方包,虚拟环境是操作练习,不是运行基础示例的硬性条件。
在学习包根目录打开终端,Windows 执行:
终端命令 复制代码
py -m venv .venv
.\.venv\Scripts\python.exe --version
.\.venv\Scripts\python.exe examples/ch01/ex01_01.py
.\.venv\Scripts\python.exe -m pip --version
Mac 执行:
终端命令 复制代码
python3 -m venv .venv
./.venv/bin/python --version
./.venv/bin/python examples/ch01/ex01_01.py
./.venv/bin/python -m pip --version
上面使用精确路径,无需先激活。可选激活方式:Windows PowerShell 为 .\.venv\Scripts\Activate.ps1,Mac 为 source .venv/bin/activate,激活后可用 python,退出用 deactivate。若 PowerShell 阻止激活脚本,直接使用上面的解释器完整路径即可,不必为了课程放宽系统执行策略。
换电脑时复制源代码和依赖清单,重新创建 .venv,不要搬运旧 .venv 。不同操作系统路径和二进制依赖不通用。后续有第三方依赖时,可在旧环境用 python -m pip freeze > requirements.txt 保存安装快照,在新环境用 python -m pip install -r requirements.txt 安装;跨系统或新版本仍可能需要调整。当前教材无需安装第三方依赖。
随机数与实验记录
random.Random(42) 建立局部随机数生成器,可帮助在相同环境与调用顺序下复现实验。不要把固定 seed 理解为跨所有库、硬件、版本的绝对一致保证。后续学习 PyTorch 时还需要记录版本、硬件和确定性设置。
环境操作依据 Python Packaging 官方指南 ;具体安装选项变化时请以官方页面为准。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 11_01
数学工具 运行文件:examples/ch11/ex11_01.py
Python 复制代码
import math
print(math.sqrt(81 ))
print(math.ceil(2.1 ))
print(math.floor(-2.1 ))
print(math.isclose(0.1 + 0.2 , 0.3 ))预测完后,查看预期输出 怎么执行 ceil 向上取整,floor 向下取整,isclose 判断浮点近似相等。金融等精确十进制需求还需更合适的数据表示。
动手改写: 预测 floor(2.9) 与 ceil(-2.9)。
示例 11_02
Counter 计数 运行文件:examples/ch11/ex11_02.py
Python 复制代码
from collections import Counter
counts = Counter("a b a c b a" .split())
print(counts["a" ])
print(counts["missing" ])
print(counts.most_common(2 ))预测完后,查看预期输出 程序输出
3
0
[('a', 3), ('b', 2)]怎么执行 Counter 封装了第 08 章的常见计数逻辑。先理解手写版本,再用标准库减少重复。
动手改写: 用 Counter 统计一段英文文本,先统一大小写。
示例 11_03
日期间隔 运行文件:examples/ch11/ex11_03.py
Python 复制代码
from datetime import date
start = date(2026 , 1 , 1 )
end = date(2026 , 1 , 8 )
print((end - start).days)预测完后,查看预期输出 怎么执行 这里使用固定日期而非“今天”,确保你在不同日期运行也得到相同输出。结果表示间隔天数,不把两端各算一次。
动手改写: 计算同一天之间的间隔。
示例 11_04
局部随机性 运行文件:examples/ch11/ex11_04.py
Python 复制代码
import random
first = random.Random(42 )
second = random.Random(42 )
a = [first.random() for _ in range(3 )]
b = [second.random() for _ in range(3 )]
print(a == b)
print(all(0 <= value < 1 for value in a))预测完后,查看预期输出 怎么执行 两个生成器在当前环境里使用相同 seed 和调用序列,得到一致序列。random() 返回 [0,1) 的值。
动手改写: 只让一个生成器先多调用一次,观察序列是否仍相同。
示例 11_05
可导入的入口 运行文件:examples/ch11/ex11_05.py
Python 复制代码
def double(value):
return value * 2
def main():
print(double(5 ))
if __name__ == "__main__" :
main()预测完后,查看预期输出 怎么执行 直接运行时 main 被调用;导入时只定义函数,不执行 main。综合项目都会使用这个结构。
动手改写: 在同目录新建文件导入这个示例模块并调用 double。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 11_01 · 场景 我已独立完成
均值与中位数 用 statistics 模块计算 [1,2,100] 的 mean 和 median,分别输出并把均值格式化为两位小数。
作答文件:practice/ch11/ex11_01.py
预期输出 给我一点提示 中位数是排序后的中间值,不等于平均值。
查看参考代码与解析 Python 复制代码
import statistics
values = [1 , 2 , 100 ]
print(f"{statistics.mean(values):.2f}" )
print(statistics.median(values))解题思路 异常大值显著影响均值,中位数在这里仍为 2。这里只练库函数,不作完整统计推断。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 11_01
练习 11_02 · 基础 我已独立完成
词频最多者 用 Counter 统计 "red blue red green red blue",输出 most_common(1) 的结果。
作答文件:practice/ch11/ex11_02.py
预期输出 给我一点提示 先 split。
查看参考代码与解析 Python 复制代码
from collections import Counter
counts = Counter("red blue red green red blue" .split())
print(counts.most_common(1 ))解题思路 most_common 返回列表,其中元素是词和次数组成的元组,即使只取一个也是列表。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 11_02
练习 11_03 · 迁移 我已独立完成
向上取整批次数 23 条数据,每批最多 8 条,用 math.ceil 输出需要几批:3。
作答文件:practice/ch11/ex11_03.py
预期输出 给我一点提示 先做除法再向上取整。
查看参考代码与解析 Python 复制代码
import math
print(math.ceil(23 / 8 ))解题思路 前两批各 8 条,最后 7 条。对大整数计数可优先使用整数公式避免浮点精度问题。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 11_03
练习 11_04 · 拓展 我已独立完成
十进制精确相加 用 decimal.Decimal 从字符串构造 0.1 和 0.2,相加并输出 0.3。
作答文件:practice/ch11/ex11_04.py
预期输出 给我一点提示 从字符串构造,而非先构造二进制 float。
查看参考代码与解析 Python 复制代码
from decimal import Decimal
print(Decimal("0.1" ) + Decimal("0.2" ))解题思路 Decimal(0.1) 会带入浮点近似值。先从精确文本表示构造,才符合此例目的。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 11_04
练习 11_05 · 场景 我已独立完成
复现实验打乱 列表 range(5),分别复制两份并用两个 random.Random(7) 打乱,输出两份是否相同 True,再输出原列表。
作答文件:practice/ch11/ex11_05.py
预期输出 给我一点提示 shuffle 原地修改,所以先 copy。
查看参考代码与解析 Python 复制代码
import random
original = list(range(5 ))
a = original.copy()
b = original.copy()
random.Random(7 ).shuffle(a)
random.Random(7 ).shuffle(b)
print(a == b)
print(original)解题思路 不要写 a = rng.shuffle(a),shuffle 返回 None。复制保护原始顺序。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 11_05
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 理解工作目录与脚本目录 用 with 安全读写 UTF-8 文本 选择 CSV、JSON、JSONL 表示结构化数据 相对路径相对于哪里
Path("data.txt") 默认相对于当前工作目录 ,不一定是代码文件所在目录。你从不同目录启动脚本,可能读到不同地方。终端先 cd 到学习包根目录可以减少混乱。
需要固定相对脚本位置时用 BASE = Path(__file__).resolve().parent,再写 BASE / "data" / "scores.csv"。__file__ 通常在脚本中存在,但交互窗口和某些 notebook 环境中没有。Path 的 / 在这里是拼接路径,不是数值除法;它会按操作系统处理分隔符。
文本读写与覆盖
path.read_text(encoding="utf-8") 返回整个文件字符串;path.write_text(text, encoding="utf-8") 写入文本,已有同名文件会被覆盖 。父目录不存在时先 mkdir(parents=True, exist_ok=True)。本章示例将演示文件写入当前工作目录下的 outputs/,可反复运行并覆盖这些专用示例文件;不要替换成你自己的重要文件路径。
用 with path.open("r", encoding="utf-8") as file: 打开文件,离开 with 块后自动关闭。"r" 读取,"w" 覆盖写,"a" 追加,"x" 只在文件不存在时新建。二进制模式如 rb 面向字节,本阶段先专注文本。
一行一行处理
for line in file 可以逐行处理,避免一次把大文件读进内存。line 通常保留行末换行符。若只想去掉行末换行,用 rstrip("\n");strip() 会连两端其他空白一起删除,是否合适取决于数据。splitlines() 把完整字符串拆成行,适合小文件。
CSV 是表格,不是随便按逗号拆
CSV 一行一条记录,通常第一行是列名。字段里可能包含逗号、引号甚至换行,所以使用 csv.DictReader / DictWriter。打开 CSV 时加 newline="",让 csv 模块正确处理换行。
CSV 读取的字段默认是字符串,"80" 要 int 后才能计算。不要把表头拼错或空字符串当成正常分数;第 13 章学习识别与记录错误。Excel 导出的文件可能含 BOM,可尝试 encoding="utf-8-sig" 读取 UTF-8 BOM 文件,不要对所有编码问题盲目使用 errors="ignore"。
JSON 保存嵌套结构
JSON 能表示对象、数组、字符串、数字、布尔值和 null。读入 Python 后对应 dict、list、str、int/float、bool、None。JSON 文本里是 true/false/null,Python 源码里是 True/False/None。JSON 要求对象键为字符串;集合等类型不能直接序列化。
json.dumps(obj) 返回 JSON 字符串,json.loads(text) 把字符串解析为对象。json.dump(obj,file) 和 json.load(file) 面向文件对象。ensure_ascii=False 保留中文便于阅读,indent=2 让缩进清晰。数据不合法时会报 JSONDecodeError;不要使用 eval 读取 JSON。
JSONL 是每行一个 JSON 对象
每行独立解析,适合逐条流式处理记录。空行怎么处理、坏行是跳过还是终止,都要制定约定。AI 数据集常见这种格式;文本中的换行应由 JSON 转义,而不是直接破坏一行一条记录的结构。
跨电脑可靠性
使用 UTF-8、pathlib、明确的相对脚本路径,避免硬编码 C:\Users\你的名字 或 /Users/你的名字。把数据和代码一起打包。文件写出后读回来核对条数和关键字段,比“没有报错”更有说服力。
精讲:路径正确,文件才会出现在你预期的位置
Path("data/input.json") 从当前工作目录开始寻找;当前工作目录由你怎样启动程序决定。Path(__file__).resolve().parent 从脚本文件的位置定位。把项目放到另一台电脑后,后者更适合寻找随项目一起复制的数据。__file__ 用于脚本;在交互窗口或某些笔记本里不要假设它存在。
请从学习包根目录运行示例 12_06,再换一个目录用完整路径运行同一个文件,观察它打印的路径字段。该示例只解析路径,不会创建文件。真正需要写文件时,先确认目标目录;mkdir(parents=True, exist_ok=True) 可以创建缺失目录。
模式 目标文件不存在时 目标文件已存在时 常见用途
r 报 FileNotFoundError 读取,不截断 读课件或已有记录
w 创建文件 一打开就截断原内容 重写完整快照
a 创建文件 在末尾追加 每行一条的日志
“先读再改再写完整 JSON”与“每行追加一条 JSONL”是两种不同设计。不要在 JSON 数组末尾直接追加另一个 JSON 对象;拼在一起通常不再是一个合法的 JSON 文档。JSONL 则约定一行一个 JSON 值,读取时逐行解析。本教材日志只使用一行一个对象。
精讲:字符串看起来像数字,不等于已经是数字
CSV 读回的 "80" 是字符串;先用 int 转换再计算。JSON 读回的值则取决于原 JSON 的类型:80 读成整数,"80" 仍是字符串,null 读成 None。成功解析文件只说明格式可以读取,不能证明字段类型和业务范围正确。
编码统一使用 UTF-8。CSV 文件使用 newline="" 交给 csv 模块处理换行;来自部分表格软件的 UTF-8 BOM 文件可以按明确需求用 utf-8-sig 读取。不要靠到处删首字符来修复表头。
自查顺序: 找到文件 → 确认编码 → 解析格式 → 检查字段 → 转换类型 → 执行业务。后面 JSON 会话示例先解决保存与读回;第 13 章补上输入校验。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 12_01
写入与读回文本 运行文件:examples/ch12/ex12_01.py
Python 复制代码
from pathlib import Path
folder = Path("outputs" )
folder.mkdir(exist_ok=True )
path = folder / "example12_01.txt"
path.write_text("第一天:变量\n第二天:循环\n" , encoding="utf-8" )
text = path.read_text(encoding="utf-8" )
print(text, end="" )预测完后,查看预期输出 怎么执行 目录先建立,再写固定演示文件。读取结果末尾已有换行,所以 print 用 end="" 避免多一行。
动手改写: 追加第三天内容,再读回确认没有覆盖前两天。
示例 12_02
逐行读取与清理 运行文件:examples/ch12/ex12_02.py
Python 复制代码
from pathlib import Path
folder = Path("outputs" )
folder.mkdir(exist_ok=True )
path = folder / "example12_02.txt"
path.write_text(" Python \n\n AI \n" , encoding="utf-8" )
with path.open(encoding="utf-8" ) as file:
for line in file:
clean = line.strip()
if clean:
print(clean)预测完后,查看预期输出 怎么执行 with 管理文件关闭;for 提供一行;strip 和非空检查完成简单清洗。这是第 16 章文本管道的基础。
动手改写: 保留行内空格,确认 strip 不会删掉中间空格。
示例 12_03
CSV 写读转换 运行文件:examples/ch12/ex12_03.py
Python 复制代码
import csv
from pathlib import Path
folder = Path("outputs" )
folder.mkdir(exist_ok=True )
path = folder / "example12_03.csv"
with path.open("w" , encoding="utf-8" , newline="" ) as file:
writer = csv.DictWriter(file, fieldnames=["name" , "score" ])
writer.writeheader()
writer.writerows([{"name" : "小林" , "score" : 80 }, {"name" : "小陈" , "score" : 90 }])
with path.open(encoding="utf-8" , newline="" ) as file:
rows = list(csv.DictReader(file))
print(type(rows[0 ]["score" ]).__name__)
print(sum(int(row["score" ]) for row in rows))预测完后,查看预期输出 怎么执行 写时是整数,CSV 文本再读回来是字符串。列名由表头决定。数值转换放在明确处理步骤里。
动手改写: 把名字改成带英文逗号的文本,确认 csv 模块能正确读写。
示例 12_04
JSON 往返 运行文件:examples/ch12/ex12_04.py
Python 复制代码
import json
config = {"name" : "练习" , "batch_size" : 8 , "enabled" : True }
text = json.dumps(config, ensure_ascii=False )
restored = json.loads(text)
print(text)
print(restored == config)预测完后,查看预期输出 程序输出
{"name": "练习", "batch_size": 8, "enabled": true}
True怎么执行 JSON true 与 Python True 的拼写不同。dumps/loads 在内存字符串间转换,配合 Path 就可以持久化。
动手改写: 加一个 None 字段,观察 JSON 中变成什么。
示例 12_05
逐行 JSONL 运行文件:examples/ch12/ex12_05.py
Python 复制代码
import json
from pathlib import Path
folder = Path("outputs" )
folder.mkdir(exist_ok=True )
path = folder / "example12_05.jsonl"
records = [{"id" : 1 , "text" : "你好" }, {"id" : 2 , "text" : "Python" }]
with path.open("w" , encoding="utf-8" ) as file:
for record in records:
file.write(json.dumps(record, ensure_ascii=False ) + "\n" )
with path.open(encoding="utf-8" ) as file:
for line in file:
record = json.loads(line)
print(record["id" ], record["text" ])预测完后,查看预期输出 怎么执行 每次 dumps 生成一个 JSON 文本,再追加真实换行。读取时每行都是独立 JSON。
动手改写: 让 text 含换行,查看文件中的转义后再读回。
示例 12_06
仅解析路径 运行文件:examples/ch12/ex12_06.py
Python 复制代码
from pathlib import Path
path = Path("data" ) / "train.jsonl"
print(path.name)
print(path.stem)
print(path.suffix)预测完后,查看预期输出 程序输出
train.jsonl
train
.jsonl怎么执行 name 是最后一部分文件名,stem 去最后一个扩展名,suffix 带点。构造 Path 不会自动创建文件。
动手改写: 尝试 report.final.csv,观察 stem。
示例 12_07
AI / Agent:把会话快照写入 JSON 再读回 运行文件:examples/ch12/ex12_07.py
Python 复制代码
import json
from pathlib import Path
messages = [{"role" : "user" , "content" : "学习函数" }]
path = Path("outputs/ai_chat_demo.json" )
path.parent.mkdir(parents=True , exist_ok=True )
text = json.dumps(messages, ensure_ascii=False , indent=2 )
path.write_text(text, encoding="utf-8" )
restored = json.loads(path.read_text(encoding="utf-8" ))
print(type(restored).__name__)
print(restored[0 ]["content" ])
print(restored == messages)预测完后,查看预期输出 怎么执行 messages 是内存列表;dumps 把它变成 JSON 字符串;write_text 把字符串写到当前工作目录下的 outputs/ai_chat_demo.json;read_text 读回字符串;loads 再得到列表。两次转换方向相反,值相等不代表还是同一个内存对象。重复运行会重写这个演示文件;在学习包根目录运行时,可到 outputs 查看内容。这里叫持久化,因为文件在程序退出后仍在;不是训练模型或修改模型权重。
动手改写: 给列表追加一条 assistant 消息再运行,确认文件里出现两条。随后只修改内存列表而不写文件,再读文件,说明为什么文件不会自动跟着改变。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 12_01 · 场景 我已独立完成
提取非空行 实现 nonempty_lines(text),去各行两端空白,丢弃空行。打印对 " A \n\n B " 的结果。
作答文件:practice/ch12/ex12_01.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 先 splitlines,再 strip。
查看参考代码与解析 Python 复制代码
def nonempty_lines(text):
result = []
for line in text.splitlines():
clean = line.strip()
if clean:
result.append(clean)
return result
print(nonempty_lines(" A \n\n B " ))解题思路 函数先只负责文本转换,文件读写留给外部,这样容易独立验证。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 12_01
练习 12_02 · 基础 我已独立完成
配置解析 实现 read_batch_size(text),解析 JSON 字符串并读取 batch_size,缺失默认 8。打印对 {"name":"demo"} 的结果。假定输入 JSON 对象合法。
作答文件:practice/ch12/ex12_02.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 json.loads 后 get。
查看参考代码与解析 Python 复制代码
import json
def read_batch_size(text):
config = json.loads(text)
return config.get("batch_size" , 8 )
print(read_batch_size('{"name":"demo"}' ))解题思路 此题聚焦解析和默认值。真实应用还需要验证 batch_size 是正整数,不能接受任意字段类型。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 12_02
练习 12_03 · 场景 我已独立完成
CSV 字符串求和 实现 csv_total(text),CSV 含 amount 列,内容为整数字符串;返回合计。打印对 "amount\n10\n20\n" 的结果 30。
作答文件:practice/ch12/ex12_03.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 io.StringIO 可以把内存字符串当文本文件使用。
查看参考代码与解析 Python 复制代码
import csv
import io
def csv_total(text):
reader = csv.DictReader(io.StringIO(text))
return sum(int(row["amount" ]) for row in reader)
print(csv_total("amount\n10\n20\n" ))解题思路 StringIO 免去测试中创建实体文件,但 csv 解析规则相同。空数据只有表头时 sum 得到 0。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 12_03
练习 12_04 · 迁移 我已独立完成
JSONL 有效记录 实现 parse_jsonl(text),跳过空行,解析其余每行 JSON,返回列表。假定非空行都是合法 JSON。打印两条 {"id":1}、{"id":2} 的结果。
作答文件:practice/ch12/ex12_04.py
预期输出 程序输出
[{'id': 1}, {'id': 2}]本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 逐行 strip,非空才 loads。
查看参考代码与解析 Python 复制代码
import json
def parse_jsonl(text):
records = []
for line in text.splitlines():
if line.strip():
records.append(json.loads(line))
return records
print(parse_jsonl('{"id":1}\n\n{"id":2}\n' ))解题思路 空行不是合法 JSON,所以应先按约定跳过。坏行处理会在下一章增加。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 12_04
练习 12_05 · 基础 我已独立完成
扩展名清单 实现 suffixes(names),用 Path.suffix 返回各文件扩展名的列表;打印 ["a.txt","b.tar.gz","README"] 的结果。
作答文件:practice/ch12/ex12_05.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 suffix 只取最后一个扩展名,没有则为空字符串。
查看参考代码与解析 Python 复制代码
from pathlib import Path
def suffixes(names):
return [Path(name).suffix for name in names]
print(suffixes(["a.txt" , "b.tar.gz" , "README" ]))解题思路 要所有扩展名可查看 suffixes 属性;本题只要最后一个。此函数不要求文件实际存在。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 12_05
练习 12_06 · 场景 我已独立完成
读取数字文件 实现 read_numbers(path),path 为 pathlib.Path,读 UTF-8 文本,每个非空行是合法整数,返回整数列表。演示时在临时目录建文件,内容 10、20,输出 [10,20]。
作答文件:practice/ch12/ex12_06.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 复用 splitlines、strip 与 int;临时目录离开 with 后自动清理。
查看参考代码与解析 Python 复制代码
from pathlib import Path
from tempfile import TemporaryDirectory
def read_numbers(path):
return [int(line) for line in path.read_text(encoding="utf-8" ).splitlines() if line.strip()]
with TemporaryDirectory() as folder:
path = Path(folder) / "numbers.txt"
path.write_text("10\n20\n" , encoding="utf-8" )
print(read_numbers(path))解题思路 临时目录只用于演示和检查,不适合保存你长期需要的学习记录。函数不负责跳过非法数字,本题约定输入合法。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 12_06
练习 12_07 · 场景 我已独立完成
AI / Agent:读回合法的 JSONL 消息 实现 parse_messages(text):输入为多行文本,每个非空行保证是含 role、content 的合法 JSON 对象;跳过空行,返回解析后的字典列表。调用文本为两行 {"role":"user","content":"hi"} 与 {"role":"assistant","content":"hello"},中间有一空行。输出列表长度和第二条正文。
作答文件:practice/ch12/ex12_07.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 用 splitlines 遍历行,strip 后为空就 continue;否则 json.loads 后 append。此题只处理合法记录,坏 JSON 留到下一章。
查看参考代码与解析 Python 复制代码
import json
def parse_messages(text):
messages = []
for line in text.splitlines():
if not line.strip():
continue
messages.append(json.loads(line))
return messages
text = '{"role":"user","content":"hi"}\n\n{"role":"assistant","content":"hello"}'
messages = parse_messages(text)
print(len(messages))
print(messages[1 ]["content" ])解题思路 splitlines 把文本拆成行;空行被跳过;每个非空行独立 loads,而不是把整段 JSONL 当一个 JSON 文档。最后返回列表供调用者使用。尝试末尾换行、Windows 的 CRLF 换行和全空输入;这些都不应多产生一条消息。下一章再区分格式错误和字段错误。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 12_07
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 按顺序阅读 traceback 只捕获预期异常并保留诊断信息 用正常、边界和错误输入验证函数 逐行检查 JSONL,记录原始行号并保留后续合法数据 将 CSV 字段分步转换,保留坏记录的结束行号 三种不同的问题
语法错误让代码无法按语法解析,例如少冒号;运行时错误发生在执行到某一步时,例如 int("abc");逻辑错误可能完全不报错,却给出错误结果,例如平均分除以固定 3 而不是实际人数。第三种尤其需要测试发现。
读 traceback 的步骤
先看最后一行的异常类型和消息,再往上找到你自己文件的行号,打开那一行,检查参与操作的变量类型和值。若这个值来自上一步,再追到它的来源。长报错栈不意味着有很多独立错误;它经常是在描述同一个错误的调用路径。
异常 常见含义 首先检查
SyntaxError / IndentationError 语法或缩进不对 冒号、括号、引号、缩进
NameError 名字不存在 拼写、是否先赋值
TypeError 类型或调用方式不匹配 type、参数数目
ValueError 类型可用但内容不合法 int 的输入、范围
IndexError / KeyError 位置或键不存在 长度、键集合
FileNotFoundError 找不到文件 当前目录、路径、是否解压
ModuleNotFoundError 找不到模块 解释器、文件名、环境
ZeroDivisionError 除数为零 空数据、计数初始化
最小化问题
把大程序缩成还能复现错误的最小例子。打印 repr(value) 看不可见字符,打印 type(value) 看字符串还是数字,打印 len(values) 看是否为空。用小数据手算结果,避免一开始在上万条数据里找错。
try 与 except
把“确实可能失败的一小段”放入 try。except ValueError as error: 处理特定错误并获取消息。不要用裸 except 吞掉所有异常,不然 Ctrl+C 等中断也可能被拦截;也不要 except Exception: pass 把错误静默丢掉。
多个错误可以分别处理,例如文件不存在与 JSON 格式错误。else 可放 try 成功后执行的代码,finally 常用于不论成功失败都要执行的清理。文件关闭优先使用 with,减少手写 finally。
什么时候主动 raise
输入不符合函数约定时,可以 raise ValueError("batch_size 必须大于 0")。清晰消息比后来意外除零好。交互层可以捕获并让用户重试;核心处理函数通常报告错误,让调用者决定如何处理。
assert 与正式输入校验
assert actual == expected 常用在开发和测试中核对内部预期。它失败会抛 AssertionError。不要用 assert 替代处理外部输入的验证,因为 Python 优化模式可能移除 assert;用户输入校验应使用 if 和 raise。
浮点测试用 math.isclose(actual, expected, rel_tol=..., abs_tol=...),容差应由问题精度决定。接近 0 时绝对容差尤其有用。不要不加思考地对所有问题套同一个容差。
为一个函数设计测试
以 mean 为例:正常输入 [2,4] →3;单元素 [5] →5;空输入 [] →None;负数 [-2,2] →0;输入不被修改。不要只选“好看的输入”。测试通过说明满足这些检查,不证明对所有可能输入绝对正确。
怎样用本教材自检器
先编辑 practice/ch09/ex09_02.py 并保存。在学习包根目录执行:
终端命令 复制代码
python tools/check_practice.py 09_02
python tools/check_practice.py --chapter 09
Windows 无虚拟环境时把 python 换成 py;Mac 换成 python3。自检先比较示例输出,再对部分函数题使用其他输入。它只运行学习包中的指定题目文件,不会替你保存编辑器里尚未保存的内容。未完成题显示未通过是正常的。不要用 --solutions 代替作答;该选项仅用于核验参考答案。
失败时按报告找到:运行报错、输出不同,或函数额外测试失败。先看题目函数名是否一致、return 是否遗漏、空数据是否处理,再看格式。若靠硬编码示例输出过关,并没有达到学习目的;至少自己再换两个输入。
精讲:Traceback 从哪里开始读
遇到报错先保留完整输出,再读最后一行的异常类型和原因。往上找你自己代码对应的文件名、行号和那一行表达式。最靠近末尾的调用帧通常是异常出现的地方;更上面的调用帧解释程序怎样走到那里。
练习时建立一个临时文件,只写 value = int("abc") 并运行。你应看到 ValueError。现在把输入改成 "12" 再运行:异常消失,但没有 print 就不会显示 12。这说明“运行成功”和“输出符合需求”要分别检查。不要为了看到内容而把所有函数都改成 print。
现象 首先检查 下一步实验
NameError 名字拼写、定义是否执行 在调用前确认名字已定义
UnboundLocalError 函数内是否给同名变量赋值后又提前读取 把依赖改成显式参数并 return 新值
TypeError 当前操作两边实际是什么类型 临时打印 type(value)
IndexError / KeyError 索引、键、输入是否为空或缺字段 手工构造最小的空输入
输出不对但无异常 条件边界、更新顺序、数据约定 列输入 → 中间值 → 预期输出表
精讲:解析成功只是第一道检查
json.loads 可以读出列表、数字、字符串或字典。因此不能在解析后直接假设结果有 result["answer"]。结构化模型输出也需要检查:最外层类型是什么、必需字段是否存在、字段类型是否符合约定、值是否在范围内。
Python 中 bool 是 int 的子类。如果约定“次数必须是整数,不能是 True”,仅 isinstance(value, int) 会把 True 放进来。可以使用 type(value) is int,或在 isinstance 检查之外明确排除 bool。请理解约定后选择,不要机械地把所有 isinstance 改掉。
只有可恢复的错误才适合重试。格式错误可以要求重新生成;重复执行一个已经成功写入的工具可能造成重复记录。后面的离线示例只重试读取预设回复,最多三次;它不代表网络重试或有副作用工具的完整策略。
精讲:自检失败时怎样缩小问题
输出比较失败时,自检器会显示第一处不同的行,用引号让行首行尾空格可见。先比这一行,再检查多余的提示语和换行。函数测试失败时,Traceback 会显示 assert 对应的条件;把那组输入单独送进自己的函数,逐步打印中间值。
assert 适合本教材的开发检查。运行 Python 时若使用 -O,断言可能被移除;真正需要始终执行的外部输入校验,应写 if 和 raise。
修复实验:函数里为什么读不到外面的计数
先看新增示例 13_07 的 broken_round。函数体中出现 rounds += 1,Python 会把这里的 rounds 视为这个函数的局部名字;而 += 又要先读取旧值,所以在局部值尚未建立时抛出 UnboundLocalError。即使外面已有 rounds=0,也不能在这处读取中自动补上局部值。
示例专门捕获这一个异常并打印它的类型,随后显示外面的 rounds 仍是 0。修复版显式接收 current 并返回 current+1,再由外面赋值保存。这样每一次调用的输入和结果都能单独测试。
想看完整 Traceback,可以在临时副本里只保留 rounds、broken_round 的定义及 broken_round() 调用,去掉捕获部分再运行。此时出现异常并退出是预期现象。不同 Python 版本的错误措辞可能有差别;先看类型、文件行号和出错表达式,不必背整句英文。
实操:JSONL 的一行数据要经过三道检查
下面训练记录的约定由本教材自行制定:每行是一个对象;text 是字符串,去首尾空白后非空;label 是整数 0 或 1,不能是 True/False。它只是小型练习格式,不代表任何模型厂商的训练接口。
检查层 当前拿到什么 失败例子 本例错误类别
JSON 解析 原始的一行字符串 缺少右花括号 json
结构与字段类型 loads 得到的 Python 值 最外层是列表,或 text 是数字 schema
业务取值 类型已正确的字段 text 仅空白,或 label 为 2 value
先确保最外层是字典,才能按字段读取;先确保 text 是字符串,才能 strip;先确保 label 是本题允许的整数类型,才能检查值域。格式能解析、字段类型正确、符合使用约定,是三件事。
为什么原始行号必须在跳过空行之前取得
用 enumerate(text.splitlines(), start=1) 先得到物理行号,再跳过空行。如果先删掉空行再编号,第 3 行的错误就可能被写成第 2 行,回原文件时会找错位置。
新增示例 13_08 有 6 行:第 1 行合法、第 2 行为空、第 3 行 JSON 损坏、第 4 行字段类型错误、第 5 行 label 超范围、第 6 行合法。先手算:保留 2 条、跳过 1 行、记录 3 个错误,错误行号应为 3、4、5。运行后逐个比对。
坏行之后还能继续,是因为明确选择了 continue
解析失败后先保存行号和错误类别,再 continue 到下一行;合法行才加入 records。try 只包住 json.loads,并只捕获 JSONDecodeError。若把整个处理函数都包进宽泛的 except 后默默跳过,拼错变量名等程序缺陷也可能被隐藏。
这个例子处理内存字符串,所以每次运行不改任何数据文件。接入第 12 章文件读取时,可以把 Path(...).read_text(encoding="utf-8") 的结果交给函数。文件不存在或编码不对属于读取层错误,不能冒充“某一行 JSON 不合法”;大文件逐行流式读取留作后续改写。
CSV 精修:从文本到可以计算的数字,中间发生了什么
前置:第 07 章列表、第 09 章函数、第 12 章 csv 和 StringIO,以及本章的 ValueError。先完成 12_03 的 CSV 字符串求和,再做下面的坏数据练习。StringIO 把内存字符串包装成可读取的文本流,不会创建文件。
本例约定表头严格为 name,score,顺序也相同;每条记录两列,姓名去掉首尾空白后非空,成绩是 int 能转换的整数字符串,范围 0 到 100。先检查列数,再检查姓名,再转换成绩,最后检查范围。第一处失败就记下原因并 continue,不能把同一条坏记录加进 valid。
当前步骤 当前值 类型或含义 下一步
csv.reader 解析一条记录 ['小何', '0'] 两个字符串组成的列表 检查 len(row)
row[1].strip() '0' 字符串,不是数字 int 转换
int 得到结果 0 整数,转换成功 检查 0 <= score <= 100
加入 valid {'name': '小何', 'score': 0} 可以交给统计函数的字典 继续下一条
0 是有效成绩。 不要用 if not score 判断“没有填成绩”,因为整数 0 的真假值为 False。应在转换前看字符串是否为空,或让 int('') 抛出 ValueError。int('80.5') 也会失败;int(' 80 ') 和 int('+80') 可以成功,本题接受 int 的这些常规写法。
为什么不直接 split(',') 或按记录数冒充行号
"林,一",80 的姓名中含逗号,csv.reader 能把它读成两列;split(',') 会错误拆成三部分。CSV 的引号内还可以合法换行,所以“第几条记录”和“原文件第几行”并不总相同。
示例 13_09 使用 reader.line_num,表示解析完当前记录时已经读到的物理行号,即记录结束行号 。完全空的物理行(没有任何字符)解析成 [] 后跳过;小李 是一列,不是空行;小李, 是两列,第二列是空字符串,属于整数转换失败。坏记录只报告第一处错误。
示例原始行 数据情况 处理结果
1 name,score 表头,只检查一次
2 姓名含英文逗号,成绩 80 保留
3 完全空的物理行 跳过
4 成绩 bad integer
5 成绩 101 range
6 只有姓名,没有成绩列 columns
7 成绩 0 保留
三层实操:每次只增加一种困难
读懂: 先预测示例 13_09 保留几条、错误几条,以及错误行号。运行后逐项对照上表。两个列表不是重复数据:valid 给统计层,errors 给修复原始数据的人。
改写: 将 bad 改为 90,预测合法记录变为 3 条;将 101 改为 100,预测它通过;将 0 改为 -1,预测 range 错误;把“小李”改为“小李,”,预测错误从 columns 变为 integer。每次只改一项,保留其他输入。
迁移: 完成习题 13_10 的离线工具耗时汇总。沿用检查顺序,但独立写出函数和边界;不看成绩例子的代码也能说明每层输入与输出,再接第 17 章成绩表清洗项目。
本例会继续处理字段错误,但错误表头会直接抛出 ValueError。严重引号错误可能让 csv.reader 抛出 csv.Error;该异常向调用者报告并停止,因为无法确定下一条记录从哪里开始。不要将所有 CSV 错误都理解成“可以安全跳过一行”。读取真实文件时使用 open(path, encoding="utf-8", newline=""),让 csv 模块处理换行。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 13_01
具体捕获转换错误 运行文件:examples/ch13/ex13_01.py
Python 复制代码
texts = ["10" , "bad" , "20" ]
values = []
for text in texts:
try :
value = int(text)
except ValueError:
print("跳过无效值" , repr(text))
else :
values.append(value)
print(sum(values))预测完后,查看预期输出 怎么执行 只捕获 int 可能产生的 ValueError,失败记录原因;成功才加入数值列表。不要把 int 后所有业务逻辑都塞进一个宽泛 try。
动手改写: 加上空字符串和 "2.5",观察 int 的规则。
示例 13_02
主动验证约定 运行文件:examples/ch13/ex13_02.py
Python 复制代码
def divide(total, count):
if count <= 0 :
raise ValueError("count 必须为正数" )
return total / count
try :
divide(10 , 0 )
except ValueError as error:
print(error)预测完后,查看预期输出 怎么执行 在执行除法之前验证数据。调用处决定怎样显示错误,核心函数不强行 print。
动手改写: 用 count=-1 与 count=2 测试。
示例 13_03
测试浮点结果 运行文件:examples/ch13/ex13_03.py
Python 复制代码
import math
result = 0.1 + 0.2
print(result == 0.3 )
print(math.isclose(result, 0.3 , rel_tol=1e-9 , abs_tol=1e-12 ))预测完后,查看预期输出 怎么执行 直接相等对二进制浮点近似可能不合适。给出容差后比较数值接近程度。
动手改写: 对 0 与 1e-13 比较,观察 abs_tol 的作用。
示例 13_04
小型回归检查 运行文件:examples/ch13/ex13_04.py
Python 复制代码
def clamp(value, low, high):
return min(max(value, low), high)
assert clamp(-1 , 0 , 10 ) == 0
assert clamp(5 , 0 , 10 ) == 5
assert clamp(11 , 0 , 10 ) == 10
print("3 个检查通过" )预测完后,查看预期输出 怎么执行 每个断言对应区间的一种区域。修改函数后再运行这些检查,可以发现旧行为是否被破坏。
动手改写: 补上恰好等于 0 和 10 的检查。
示例 13_05
带行号记录坏数据 运行文件:examples/ch13/ex13_05.py
Python 复制代码
lines = ["80" , "oops" , "90" ]
for line_number, text in enumerate(lines, start=1 ):
try :
score = int(text)
except ValueError:
print(f"第 {line_number} 行不是整数:{text}" )
continue
if not 0 <= score <= 100 :
print(f"第 {line_number} 行超出范围" )
continue
print("保留" , score)预测完后,查看预期输出 程序输出
保留 80
第 2 行不是整数:oops
保留 90怎么执行 语法可解析与业务合法是两层检查。能 int 转换不代表分数一定在 0—100 内。
动手改写: 加一条 120,验证范围检查。
示例 13_06
AI / Agent:解析、校验、有限次重试 运行文件:examples/ch13/ex13_06.py
Python 复制代码
import json
def read_answer(text):
data = json.loads(text)
if not isinstance(data, dict):
raise ValueError("最外层必须是对象" )
answer = data.get("answer" )
if not isinstance(answer, str) or not answer.strip():
raise ValueError("answer 必须是非空字符串" )
return answer.strip()
replies = ["not-json" , '{"answer": 12}' , '{"answer": "先学函数"}' ]
for attempt, reply in enumerate(replies, start=1 ):
try :
answer = read_answer(reply)
except ValueError:
print(f"第 {attempt} 次:格式或内容不合格" )
continue
print(f"第 {attempt} 次:{answer}" )
break
else :
print("已用完预设回复,停止" )预测完后,查看预期输出 程序输出
第 1 次:格式或内容不合格
第 2 次:格式或内容不合格
第 3 次:先学函数怎么执行 第 1 条连 JSON 语法都不满足;第 2 条能解析,但 answer 是整数;第 3 条才满足本题结构约定。json.JSONDecodeError 属于 ValueError,因此此处一起捕获;正式调试时可以分别记录原因。try 只包住解析函数,成功后打印并 break。for 的 else 仅在没有 break、正常遍历结束时执行。本例读取三条预设字符串,不会请求模型重试;通过校验也只说明结构合格,不保证答案事实正确。
动手改写: 把第三条改成空字符串字段,确认出现“停止”。再把第二条改为合法答案,确认第三条不会读取。
示例 13_07
作用域排错:先复现,再用参数和返回值修复 运行文件:examples/ch13/ex13_07.py
Python 复制代码
rounds = 0
def broken_round():
rounds += 1
return rounds
try :
broken_round()
except UnboundLocalError as error:
print(type(error).__name__)
print(f"外部轮数:{rounds}" )
def fixed_round(current):
return current + 1
rounds = fixed_round(rounds)
rounds = fixed_round(rounds)
print(f"修复后:{rounds}" )预测完后,查看预期输出 程序输出
UnboundLocalError
外部轮数:0
修复后:2怎么执行 broken_round 的 += 要读取还不存在的局部 rounds,赋值未能完成就出错。捕获异常后,外部 rounds 仍为 0。fixed_round 两次分别收到 0 和 1,返回 1 和 2,调用者保存返回值后得到 2。捕获错误本身没有修复逻辑;修复来自明确的参数和返回值。
动手改写: 把第二次修复调用改成单独的 fixed_round(rounds),不接收返回值,预测最后输出。再对照第 09 章 09_10,解释“报错”和“能运行但没保存结果”的区别。
示例 13_08
AI 数据排错:保留坏行位置,继续处理下一行 运行文件:examples/ch13/ex13_08.py
Python 复制代码
import json
def read_training_lines(text):
records, errors = [], []
blank_lines = 0
for line_number, line in enumerate(text.splitlines(), start=1 ):
if not line.strip():
blank_lines += 1
continue
try :
item = json.loads(line)
except json.JSONDecodeError:
errors.append({"line" : line_number, "kind" : "json" })
continue
if not isinstance(item, dict):
errors.append({"line" : line_number, "kind" : "schema" })
continue
content = item.get("text" )
label = item.get("label" )
if not isinstance(content, str) or type(label) is not int:
errors.append({"line" : line_number, "kind" : "schema" })
continue
if not content.strip() or label not in (0 , 1 ):
errors.append({"line" : line_number, "kind" : "value" })
continue
records.append({"text" : content.strip(), "label" : label})
return records, errors, blank_lines
text = '\n' .join([
'{"text": " Python ", "label": 1}' ,
'' ,
'{"text": "broken", "label": 0' ,
'{"text": 42, "label": 0}' ,
'{"text": "hello", "label": 2}' ,
'{"text": "Agent", "label": 0}' ,
])
records, errors, blank_lines = read_training_lines(text)
print(f"保留 {len(records)} 条,跳过 {blank_lines} 个空行" )
for error in errors:
print(f"第 {error['line']} 行:{error['kind']}" )
print(records)预测完后,查看预期输出 程序输出
保留 2 条,跳过 1 个空行
第 3 行:json
第 4 行:schema
第 5 行:value
[{'text': 'Python', 'label': 1}, {'text': 'Agent', 'label': 0}]怎么执行 输入文本先分行并编号;空行跳过但保留原行号;每行依次经过解析、类型和取值检查;只有合格记录进入 records。坏行的类别与行号进入 errors,不影响第 6 行被保留。多个字段同时出错时,只报告本程序最先检查到的类别,不代表已经列出所有问题。这里只检查数据格式,没有训练模型。
动手改写: 把第 4 行的 label 改为 true、text 改为 "ok",预测它仍属于 schema;再把第 5 行 text 改为空白且 label 改为 0,预测属于 value。最后在首行前加一个空行,核对所有原错误行号加 1。
示例 13_09
CSV 清洗:空行、缺列与零分分别处理 运行文件:examples/ch13/ex13_09.py
Python 复制代码
import csv
import io
def clean_scores_csv(text):
reader = csv.reader(io.StringIO(text, newline="" ), strict=True )
if next(reader, None ) != ["name" , "score" ]:
raise ValueError("表头必须为 name,score" )
valid, errors = [], []
for row in reader:
end_line = reader.line_num
if not row:
continue
if len(row) != 2 :
errors.append((end_line, "columns" ))
continue
name = row[0 ].strip()
if not name:
errors.append((end_line, "name" ))
continue
try :
score = int(row[1 ].strip())
except ValueError:
errors.append((end_line, "integer" ))
continue
if not 0 <= score <= 100 :
errors.append((end_line, "range" ))
continue
valid.append({"name" : name, "score" : score})
return valid, errors
text = 'name,score\n"林,一",80\n\n小陈,bad\n小周,101\n小李\n小何,0\n'
valid, errors = clean_scores_csv(text)
print(valid)
for end_line, reason in errors:
print(f"结束行 {end_line}:{reason}" )预测完后,查看预期输出 程序输出
[{'name': '林,一', 'score': 80}, {'name': '小何', 'score': 0}]
结束行 4:integer
结束行 5:range
结束行 6:columns怎么执行 reader 先把文本解析成字段列表。列数与姓名合格后才尝试 int,只有转换成功才判断分数范围。第 3 行空行不应挤掉后面的物理行号,所以从 reader.line_num 取得结束行。第 7 行的 0 转换成功且在范围内,必须保留。所有错误分支都 continue,因此错误记录不会落入最后的 valid.append。
动手改写: 先做正文的四次单项改写,再试只有表头的输入,确认返回两个空列表;改错表头时应得到 ValueError。最后用带引号换行的姓名,观察结束行号为什么可能跳两个数。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 13_01 · 基础 我已独立完成
容错转整数 实现 parse_int(text),转换成功返回整数,ValueError 时返回 None。依次打印对 "12" 和 "abc" 的结果。输入约定为字符串。
作答文件:practice/ch13/ex13_01.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 只捕获 ValueError。
查看参考代码与解析 Python 复制代码
def parse_int(text):
try :
return int(text)
except ValueError:
return None
print(parse_int("12" ))
print(parse_int("abc" ))解题思路 默认 None 表示解析失败,但 0 是有效返回值;调用者应使用 is None 判断。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 13_01
练习 13_02 · 场景 我已独立完成
分数合法性 实现 valid_score(text),字符串可转整数且在 0—100 内返回该整数,否则返回 None。打印对 "101" 和 "60" 的结果。
作答文件:practice/ch13/ex13_02.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 先转换,再检查范围。
查看参考代码与解析 Python 复制代码
def valid_score(text):
try :
value = int(text)
except ValueError:
return None
if not 0 <= value <= 100 :
return None
return value
print(valid_score("101" ))
print(valid_score("60" ))解题思路 两阶段防止可解析但业务无效的数据进入后续统计。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 13_02
练习 13_03 · 迁移 我已独立完成
正批大小 实现 batch_count(total, size),假定 total 为非负整数,size 为整数;size<=0 抛 ValueError,否则向上取整计算批次数。打印 batch_count(17,8)。
作答文件:practice/ch13/ex13_03.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 先验证 size,再使用整数公式。
查看参考代码与解析 Python 复制代码
def batch_count(total, size):
if size <= 0 :
raise ValueError("size 必须为正数" )
return (total + size - 1 ) // size
print(batch_count(17 , 8 ))解题思路 明确 total 的输入前提,不把本题测试范围误当作任意类型都能自动处理。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 13_03
练习 13_04 · 场景 我已独立完成
保留坏行位置 实现 parse_numbers(lines),返回 (values, bad_lines),有效整数字符串进 values,解析失败的 1 起始行号进 bad_lines。打印对 ["1","x","3"] 的结果。
作答文件:practice/ch13/ex13_04.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 用 enumerate,并分别累计两个列表。
查看参考代码与解析 Python 复制代码
def parse_numbers(lines):
values = []
bad_lines = []
for number, line in enumerate(lines, start=1 ):
try :
values.append(int(line))
except ValueError:
bad_lines.append(number)
return values, bad_lines
print(parse_numbers(["1" , "x" , "3" ]))解题思路 错误位置也是输出的一部分,用户才能回到原数据修复;不要只说“有错误”。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 13_04
练习 13_05 · 纠错 我已独立完成
防止空字符串输入 实现 require_name(text),strip 后为空则抛 ValueError,否则返回清理后的姓名。打印对 " A " 的结果。
作答文件:practice/ch13/ex13_05.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 检查的是清理后的字符串。
查看参考代码与解析 Python 复制代码
def require_name(text):
name = text.strip()
if not name:
raise ValueError("姓名不能为空" )
return name
print(require_name(" A " ))解题思路 仅检查原字符串是否非空会放过全空格;先规范化再验证。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 13_05
练习 13_06 · 挑战 我已独立完成
只捕获预期解析错误 实现 json_object(text),JSON 解析失败或解析结果不是字典时返回 None,否则返回字典。分别打印对 {"a":1}、[]、bad 的结果。
作答文件:practice/ch13/ex13_06.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 json.loads 后检查 isinstance(data, dict)。
查看参考代码与解析 Python 复制代码
import json
def json_object(text):
try :
data = json.loads(text)
except json.JSONDecodeError:
return None
if not isinstance(data, dict):
return None
return data
print(json_object('{"a":1}' ))
print(json_object('[]' ))
print(json_object('bad' ))解题思路 语法合法的 JSON 不一定具有你要求的结构。解析层与结构验证层分别检查,后续才能安全按键访问。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 13_06
练习 13_07 · 场景 我已独立完成
AI / Agent:验证工具调用数据 实现 read_call(text):解析 JSON,要求最外层为字典、name 恰好为 "char_count"、arguments 为字典且其中 text 为字符串。成功时返回 arguments["text"];不满足任一条件时 raise ValueError。空字符串允许。调用 {"name":"char_count","arguments":{"text":"Python"}} 并打印返回文本。
作答文件:practice/ch13/ex13_07.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 分层检查,不要连写 data['arguments']['text'] 后才判断。get 取不到键会得到 None,正好可以进入错误分支。JSON 语法错误本身也是 ValueError 的一种。
查看参考代码与解析 Python 复制代码
import json
def read_call(text):
data = json.loads(text)
if not isinstance(data, dict):
raise ValueError("调用必须是对象" )
if data.get("name" ) != "char_count" :
raise ValueError("不支持的工具名" )
arguments = data.get("arguments" )
if not isinstance(arguments, dict):
raise ValueError("arguments 必须是对象" )
content = arguments.get("text" )
if not isinstance(content, str):
raise ValueError("text 必须是字符串" )
return content
request = '{"name":"char_count","arguments":{"text":"Python"}}'
print(read_call(request))解题思路 先解析,再按层检查类型和字段,最后才返回允许的数据。本题只负责验证,不执行任何工具。name 的允许值只有 char_count;其他名字应明确拒绝,不能 eval 数据中的文本。空文本是字符串,符合本题约定;数字、null 和缺失字段不符合。额外测试会检查这些分支,并要求错误类型为 ValueError。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 13_07
练习 13_08 · 场景 我已独立完成
作用域修复:统计工具执行成功次数 坏思路是在函数中直接给外部 total += 1。请改为实现 count_successes(results, start=0):results 保证是布尔列表,start 保证为非负整数。以 start 为起点,只为 True 增加 1,返回新计数,不修改 results,不依赖全局计数,不打印。设 total=10,用 [True, False, True] 调用后保存到 total 并打印。
作答文件:practice/ch13/ex13_08.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 进入函数先用 count=start 建立局部计数;循环中按结果更新 count,最后 return。外面用 total = count_successes(..., start=total)。
查看参考代码与解析 Python 复制代码
def count_successes(results, start=0 ):
count = start
for succeeded in results:
if succeeded:
count += 1
return count
total = 10
total = count_successes([True , False , True ], start=total)
print(total)解题思路 局部 count 有明确初始值,所以 += 有旧值可读。三次判断后依次是 11、11、12;调用者再把返回的 12 存为 total。空列表应原样返回 start;全 False 不增加;重复调用默认 start=0 时不能记住上一次结果。这里的 True/False 是固定输入,只表示练习中的状态标记,不证明真实模型回答质量。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 13_08
练习 13_09 · 综合 我已独立完成
AI / Agent:逐行检查工具日志并记录错误 实现 read_tool_log(text),输入保证是字符串。每个非空行应为 JSON 对象,tool 为非空字符串,ok 为 bool。跳过空行;合法记录将 tool.strip() 后保存为只含 tool、ok 的新字典。返回 (records, errors),errors 每项为 {"line":原始行号,"kind":类别};坏 JSON 类别为 json,非字典/缺字段/类型错误为 schema,tool 仅空白为 value。每行最多报一个错误,按上述顺序检查。五行依次为 {"tool":" search ","ok":true}、空行、bad-json、{"tool":"count","ok":1}、{"tool":"save","ok":false}。最后打印 records 和 errors,各占一行。
作答文件:practice/ch13/ex13_09.py
预期输出 程序输出
[{'tool': 'search', 'ok': True}, {'tool': 'save', 'ok': False}]
[{'line': 3, 'kind': 'json'}, {'line': 4, 'kind': 'schema'}]本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 沿用示例的 enumerate → 跳空行 → try loads → 检查类型 → 检查取值顺序;ok=1 不是本题允许的 bool。不要在编号之前删除空行。
查看参考代码与解析 Python 复制代码
import json
def read_tool_log(text):
records, errors = [], []
for line_number, line in enumerate(text.splitlines(), start=1 ):
if not line.strip():
continue
try :
item = json.loads(line)
except json.JSONDecodeError:
errors.append({"line" : line_number, "kind" : "json" })
continue
if not isinstance(item, dict):
errors.append({"line" : line_number, "kind" : "schema" })
continue
tool = item.get("tool" )
ok = item.get("ok" )
if not isinstance(tool, str) or not isinstance(ok, bool):
errors.append({"line" : line_number, "kind" : "schema" })
continue
if not tool.strip():
errors.append({"line" : line_number, "kind" : "value" })
continue
records.append({"tool" : tool.strip(), "ok" : ok})
return records, errors
text = '\n' .join([
'{"tool":" search ","ok":true}' ,
'' ,
'bad-json' ,
'{"tool":"count","ok":1}' ,
'{"tool":"save","ok":false}' ,
])
records, errors = read_tool_log(text)
print(records)
print(errors)解题思路 第 2 行为空,但第 3 行仍编号为 3。第 4 行 ok 是整数 1,不能当作 bool。第 5 行 ok=False 表示日志记录的工具状态失败,但这条日志本身格式合法,所以应保留。日志的格式合格与记录的操作成功是两件事;额外字段按题目约定不进入新记录。本题既不重新执行工具,也不证明日志内容真实。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 13_09
练习 13_10 · 迁移 我已独立完成
AI / Agent:汇总本地工具耗时 CSV 实现 tool_time_total(text),严格要求表头为 tool,ms,否则抛出 ValueError(包括空文本)。用 csv.reader 解析,忽略完全空行;逐条按顺序检查:恰好两列、工具名 strip 后非空、ms 能经 int 转换、ms 不小于 0。失败只记录第一项原因,代码依次为 columns、tool、integer、range,位置为该记录结束物理行号。返回 (合法耗时之和, 错误列表),错误项为 (结束行号, 原因)。不打印,不写文件。打印对 'tool,ms\n"lookup,local",12\n\nfetch,bad\ncache,-1\nsave\nfinish,0\n' 的返回值。严重 CSV 语法错误交给 csv 模块抛出。
作答文件:practice/ch13/ex13_10.py
预期输出 程序输出
(12, [(4, 'integer'), (5, 'range'), (6, 'columns')])本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 先读表头;total=0、errors=[]。每次 next(reader) 由 for 自动完成后,reader.line_num 已指向记录结束行。所有检查通过才 total += ms。不要把合法的 0 当作缺失。
查看参考代码与解析 Python 复制代码
import csv
import io
def tool_time_total(text):
reader = csv.reader(io.StringIO(text, newline="" ), strict=True )
if next(reader, None ) != ["tool" , "ms" ]:
raise ValueError("表头必须为 tool,ms" )
total, errors = 0 , []
for row in reader:
end_line = reader.line_num
if not row:
continue
if len(row) != 2 :
errors.append((end_line, "columns" ))
continue
if not row[0 ].strip():
errors.append((end_line, "tool" ))
continue
try :
ms = int(row[1 ].strip())
except ValueError:
errors.append((end_line, "integer" ))
continue
if ms < 0 :
errors.append((end_line, "range" ))
continue
total += ms
return total, errors
text = 'tool,ms\n"lookup,local",12\n\nfetch,bad\ncache,-1\nsave\nfinish,0\n'
print(tool_time_total(text))解题思路 两条合法记录的耗时为 12 与 0,所以合计 12。空行不参与求和,错误仍对应第 4、5、6 物理行。缺失第二列属于 columns,存在第二列但为空属于 integer。额外列也必须拒绝。只有表头返回 (0, []);空文本连表头都没有,应抛出 ValueError。这是人为填写的离线日志,毫秒不是 token;记录格式合法和耗时非负,不代表工具执行成功,也不代表最终任务正确。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 13_10
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 区分类与实例 理解 __init__、self 和实例属性 知道何时简单函数已经足够 先从熟悉对象出发
你已经用过 names.append(...) 和 text.strip():数据与可执行操作放在对象上。类定义一类对象的结构与行为,实例是按这个类创建出的具体对象。例如 StudyRecord 是类,“小林今天学了 45 分钟”的记录是一个实例。
不要因为学了类就把所有函数都改写成类。没有持续状态、只做一次输入到输出转换时,普通函数通常足够。多个操作围绕同一份状态协作时,类比较自然。
__init__ 与 self
class Counter: 定义类。def __init__(self, start=0): self.value = start 在实例初始化时设置状态。counter = Counter(3) 创建实例并初始化为 3;你不手动传 self,Python 的方法调用机制会提供当前实例。
counter.add(2) 可理解为让 Counter.add 接收 counter 作为 self,再接收 2。self 是约定俗成的参数名,应保持这个惯例。self.value 是实例上的属性;局部变量 value 与 self.value 不是同一个名字。
各实例应有各自的可变状态
在 __init__ 里写 self.items = [],每次创建对象都会新建列表。若直接在 class 缩进下写 items = [],它是类属性,可能被多个实例共享。这个陷阱与函数可变默认参数类似:你以为每个对象新建了一份,实际上只创建了一次。
类属性适合共享常量等状态,例如课程名称;实例属性适合个人进度、账户余额、单个数据集记录。
方法也要有明确约定
add(minutes) 可以检查分钟数是否为非负整数,合法才更新状态;average() 可以在没有记录时返回 None。类内部也会遇到空数据、输入验证与副作用问题,不能因为包了一层 class 就省略这些思考。
阅读特殊方法
__len__ 支持 len(obj),__repr__ 返回开发者友好的字符串表示,__getitem__ 支持 obj[index]。后续 PyTorch 数据集常使用 __len__ 和 __getitem__;这一章只学习对应的 Python 机制,不需要安装深度学习库。
继承先会读就够
class Dog(Animal): 表示 Dog 从 Animal 继承可用行为,子类也可以定义同名方法覆盖它。super() 常用于调用父类相关实现。第一阶段重心是对象状态、方法调用和组合,复杂继承、多重继承与元类暂时不用展开。
组合是“一个对象持有另一个对象或数据”,例如一个 StudyTracker 持有 records 列表。真实程序中它通常比构造很深的继承树更直白。
精讲:self 是哪一个对象
对本章普通实例方法,调用 a.add("hello") 时,方法中的 self 就是 a;调用 b.add("hello") 时,self 就是 b。self 是惯用的形参名,不是“所有实例共用的总对象”。
self.messages = [] 放在 __init__ 中,每新建一个实例就执行一次,创建各自的列表。如果把 messages = [] 直接放在 class 缩进内、方法外,这个类属性通常会被尚未设置同名实例属性的实例共同访问。聊天程序中,这会造成不同会话混用记录。
检查这种错误时不要只建一个对象。建立 a、b 两个对象,只向 a 添加一条记录,然后分别检查两者长度。a 应为 1,b 应为 0。再考虑返回值:若方法直接 return 内部列表,调用者就可以从外部修改内部状态。后面的练习要求返回列表长度,使这个约定简单而明确。
第 07 章对象关系、第 09 章参数、第 14 章 self 讲的是同一个基础:名字指向对象,修改对象与重新绑定名字要分清。先在纸上画箭头,再看代码。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 14_01
最小计数器 运行文件:examples/ch14/ex14_01.py
Python 复制代码
class Counter:
def __init__(self, start=0 ):
self.value = start
def add(self, amount=1 ):
self.value += amount
counter = Counter(3 )
counter.add()
counter.add(2 )
print(counter.value)预测完后,查看预期输出 怎么执行 初始化 3,默认加 1,再加 2。value 是这个实例保存的状态;方法本身没有 return,调用结果为 None。
动手改写: 创建第二个 Counter,验证它不受第一个影响。
示例 14_02
实例各自保存记录 运行文件:examples/ch14/ex14_02.py
Python 复制代码
class StudyLog:
def __init__(self):
self.minutes = []
def add(self, value):
self.minutes.append(value)
def total(self):
return sum(self.minutes)
a = StudyLog()
b = StudyLog()
a.add(30 )
print(a.total(), b.total())预测完后,查看预期输出 怎么执行 两个 __init__ 分别创建列表,因此 a 的追加不会进入 b。
动手改写: 把 minutes=[] 移成类属性,观察共享问题后恢复。
示例 14_03
方法中的校验 运行文件:examples/ch14/ex14_03.py
Python 复制代码
class Wallet:
def __init__(self):
self.balance_fen = 0
def deposit(self, amount_fen):
if amount_fen < 0 :
raise ValueError("存入金额不能为负" )
self.balance_fen += amount_fen
wallet = Wallet()
wallet.deposit(1500 )
print(wallet.balance_fen)预测完后,查看预期输出 怎么执行 金额使用整数分。验证发生在修改前,避免非法输入已经改变状态后才报错。
动手改写: 传入 -1 并捕获错误,再确认余额不变。
示例 14_04
数据集接口预习 运行文件:examples/ch14/ex14_04.py
Python 复制代码
class TextDataset:
def __init__(self, texts):
self.texts = list(texts)
def __len__(self):
return len(self.texts)
def __getitem__(self, index):
return self.texts[index]
dataset = TextDataset(["hello" , "python" ])
print(len(dataset))
print(dataset[1 ])预测完后,查看预期输出 怎么执行 len 会调用 __len__,索引访问会调用 __getitem__。这里仅保存文本,未涉及 token 化或模型张量。
动手改写: 传入一个原列表后修改原列表,观察 list(texts) 的浅复制效果。
示例 14_05
简单继承 运行文件:examples/ch14/ex14_05.py
Python 复制代码
class Reporter:
def describe(self):
return "普通报告"
class StudyReporter(Reporter):
def describe(self):
return "学习报告"
print(Reporter().describe())
print(StudyReporter().describe())预测完后,查看预期输出 怎么执行 子类覆盖同名方法。调用时看具体对象所属类型提供的实现。
动手改写: 给父类加另一个方法,观察子类未覆盖时是否能调用。
示例 14_06
AI / Agent:两个会话不能共用消息列表 运行文件:examples/ch14/ex14_06.py
Python 复制代码
class ChatSession:
def __init__(self, name):
self.name = name
self.messages = []
def add(self, text):
self.messages.append(text)
return len(self.messages)
alice = ChatSession("Alice" )
bob = ChatSession("Bob" )
count = alice.add("我在学习 Python" )
print(alice.name, count)
print(bob.name, len(bob.messages))
print(alice.messages is bob.messages)预测完后,查看预期输出 怎么执行 每次 ChatSession(...) 都调用 __init__,其中 [] 各创建一次。alice.add 调用时 self 是 alice,只修改她的 messages;方法把新长度 return 给 count。最后的 is 比较是否为同一对象,不是比较内容是否相等。两个会话当前只在内存保存消息;关闭程序后要靠第 12 章的文件操作恢复。
动手改写: 在临时副本里把 messages = [] 移到类的顶层并删除 self.messages 初始化,重新运行并解释结果变化。随后恢复实例属性写法。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 14_01 · 基础 我已独立完成
步数计数器 实现 StepCounter 类,初始 steps=0,add(count) 累加步数;本题 count 约定非负整数。创建对象加 100 再加 50,打印 steps。
作答文件:practice/ch14/ex14_01.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 状态放在 self.steps。
查看参考代码与解析 Python 复制代码
class StepCounter:
def __init__(self):
self.steps = 0
def add(self, count):
self.steps += count
counter = StepCounter()
counter.add(100 )
counter.add(50 )
print(counter.steps)解题思路 用两个实例测试状态隔离;仅打印一个对象无法发现所有共享状态问题。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 14_01
练习 14_02 · 场景 我已独立完成
学习记录平均值 实现 StudyLog 类,实例 minutes 为空列表,add(value) 追加,mean() 空时 None,否则平均值。添加 30、60 后打印 mean()。
作答文件:practice/ch14/ex14_02.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 每个对象在 __init__ 创建自己的列表。
查看参考代码与解析 Python 复制代码
class StudyLog:
def __init__(self):
self.minutes = []
def add(self, value):
self.minutes.append(value)
def mean(self):
if not self.minutes:
return None
return sum(self.minutes) / len(self.minutes)
log = StudyLog()
log.add(30 )
log.add(60 )
print(log.mean())解题思路 方法组合前面学过的列表追加和安全平均值。类新增的是状态归属,不是全新的运算规则。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 14_02
练习 14_03 · 迁移 我已独立完成
可索引的记录集 实现 NumberDataset 类,构造时复制 values 到实例,支持 len 和 [index]。用 [10,20] 创建后依次打印长度与索引 0 的值。
作答文件:practice/ch14/ex14_03.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 实现 __len__、__getitem__。
查看参考代码与解析 Python 复制代码
class NumberDataset:
def __init__(self, values):
self.values = list(values)
def __len__(self):
return len(self.values)
def __getitem__(self, index):
return self.values[index]
data = NumberDataset([10 , 20 ])
print(len(data))
print(data[0 ])解题思路 复制外层列表避免调用者后来 append 改变数据集长度;嵌套对象仍是浅复制。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 14_03
练习 14_04 · 场景 我已独立完成
只接受有效分数 实现 GradeBook,实例 scores=[],add(score) 接受 0—100 的整数,越界抛 ValueError 且不修改状态。这里只要求范围验证。添加 80 后打印 scores。
作答文件:practice/ch14/ex14_04.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 检查应在 append 前面。
查看参考代码与解析 Python 复制代码
class GradeBook:
def __init__(self):
self.scores = []
def add(self, score):
if not 0 <= score <= 100 :
raise ValueError("分数超出范围" )
self.scores.append(score)
book = GradeBook()
book.add(80 )
print(book.scores)解题思路 本题输入类型约定为整数,所以只做范围校验。若对外部 JSON 开放,还需额外验证类型。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 14_04
练习 14_05 · 场景 我已独立完成
AI / Agent:有条数上限的会话记忆 实现 RecentMessages(limit)。limit 必须为大于 0 的 int,bool 不接受,否则 raise ValueError。每个实例有自己的 messages 列表;add(text) 只接受字符串,添加后保留最后 limit 条,返回当前条数。用 limit=2 依次添加 "a"、"b"、"c",打印 messages。
作答文件:practice/ch14/ex14_05.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 先完成参数验证,再初始化实例列表。超过上限时可以用切片 self.messages[-self.limit:]。验证应在修改状态之前。
查看参考代码与解析 Python 复制代码
class RecentMessages:
def __init__(self, limit):
if type(limit) is not int or limit <= 0 :
raise ValueError("limit 必须为正整数" )
self.limit = limit
self.messages = []
def add(self, text):
if not isinstance(text, str):
raise ValueError("text 必须为字符串" )
self.messages.append(text)
self.messages = self.messages[-self.limit:]
return len(self.messages)
session = RecentMessages(2 )
session.add("a" )
session.add("b" )
session.add("c" )
print(session.messages)解题思路 前三次添加后的列表依次为 ['a']、['a','b']、['b','c']。切片创建新列表,再绑定给这个实例的属性。两个实例必须分别初始化;非法添加不能先污染已有记录再报错。这里只按消息条数截取,不按 token 总量,也没有保证对话语义完整;真实上下文管理需要另外设计。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 14_05
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 理解迭代器与生成器的一次性消费 读懂类型标注、星号解包和参数收集 能把紧凑代码还原成执行步骤 可迭代对象与迭代器
列表、字符串、字典等能被 for 遍历,叫可迭代对象。iter(values) 取得迭代器,next(iterator) 取下一个元素;取完再取会抛 StopIteration。for 会自动处理这个终止信号。
列表通常可以多次遍历,每次重新获得迭代器;同一个迭代器消费过的内容不会自动恢复。zip、enumerate、生成器也有类似的一次性消费特征。调试时先 list(iterator) 看内容,可能已经把后续循环要用的数据消费完了。
yield 一次给一个
函数中出现 yield,它就成为生成器函数。调用生成器函数通常先得到生成器对象,代码在迭代取值时逐步执行。执行到 yield 暂停并交出当前值,下次继续从暂停处往后走,局部状态得以保留。
return 结束普通函数并给出最终结果;生成器中的 return 结束迭代。yield 是“这次先给一个,后面还可能有”。它避免一次保存全部输出,但输入本身是否已经全部在内存、内部是否保留大量状态,仍影响内存使用。
列表推导式与生成器表达式
[x*x for x in values] 立刻生成完整列表;(x*x for x in values) 产生按需计算的生成器。sum、any、all 等可以逐个消费生成器,通常无需额外列表。生成器不能直接 len,也不能普通索引访问。
星号在不同位置含义不同
赋值里的 first, *middle, last = values 收集剩余项;调用里的 func(*args) 把可迭代对象展开为位置参数,func(**kwargs) 把映射展开为关键字参数;定义里的 def func(*args, **kwargs) 收集额外参数为元组和字典。不要把这些星号与数字乘法混为一谈。
默认参数、位置参数、关键字参数的复杂组合以后可以继续学。当前目标是读懂训练代码中 model(**batch) 表示按批次字典的键把值作为命名参数传入,具体键必须符合函数接口。
类型标注是说明,不是自动转换
def mean(values: list[float]) -> float | None: 表明希望接收浮点列表,返回浮点或 None。Python 默认不会因为标注自动检查或转换输入;"3" 不会自动变成 3.0。实际验证需要 if/raise,静态类型检查则是额外工具。
本教材使用的 list[...] 与 | None 写法要求相应现代 Python 版本,整体学习包按 Python 3.10+ 编写。类型标注可以逐步添加,先保证函数的实际行为正确。
with 与装饰器先会辨认
with 把一段需要进入和退出管理的工作包起来,例如打开与关闭文件。后续可能看到禁用梯度等上下文,其行为由具体库定义,不是所有 with 都在处理文件。
函数上方的 @something 是装饰器语法,可理解为函数定义完成后再交给 something 包装并绑定回来。第一阶段不必自己构造复杂装饰器;先查具体装饰器的官方说明,别凭 @ 符号猜它做什么。
暂时可以放后的主题
异步编程、并发、复杂正则、元类、多重继承、描述符和性能优化不是本阶段通关要求。看到陌生语法先确认输入、输出和调用路径;一个可运行的 10 行实验比一次背完语言参考更有效。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 15_01
迭代器逐项取值 运行文件:examples/ch15/ex15_01.py
Python 复制代码
iterator = iter([10 , 20 , 30 ])
print(next(iterator))
print(list(iterator))
print(list(iterator))预测完后,查看预期输出 怎么执行 第一个元素已由 next 消费;第一次 list 消费剩余两项;最后再消费已无内容。
动手改写: 重新对原列表调用 iter,确认可以重新开始。
示例 15_02
生成器暂停 运行文件:examples/ch15/ex15_02.py
Python 复制代码
def countdown(start):
while start > 0 :
yield start
start -= 1
numbers = countdown(3 )
print(next(numbers))
print(list(numbers))预测完后,查看预期输出 怎么执行 第一次 next 运行到 yield 3;下一次从后面的 start -= 1 继续,不会从函数第一行重来。
动手改写: 在 yield 前后各加 print,观察真实执行时机。
示例 15_03
星号解包 运行文件:examples/ch15/ex15_03.py
Python 复制代码
first, *middle, last = [1 , 2 , 3 , 4 ]
print(first, middle, last)
def add(a, b):
return a + b
print(add(*[2 , 3 ]))
print(add(**{"a" : 5 , "b" : 6 }))预测完后,查看预期输出 怎么执行 赋值时收集到 middle 列表;调用时 * 展开位置,** 展开键值为命名参数。
动手改写: 尝试给 ** 字典增加不存在的键,阅读调用错误。
示例 15_04
收集可变参数 运行文件:examples/ch15/ex15_04.py
Python 复制代码
def describe(*args, **kwargs):
print(args)
print(kwargs)
describe("A" , "B" , score=90 )预测完后,查看预期输出 程序输出
('A', 'B')
{'score': 90}怎么执行 args 是元组,kwargs 是字典。名字可更改,但这两个名称是常用惯例。
动手改写: 将位置参数变成 0 个,观察空元组。
示例 15_05
带标注的函数 运行文件:examples/ch15/ex15_05.py
Python 复制代码
def average(values: list[float]) -> float | None :
if not values:
return None
return sum(values) / len(values)
print(average([1.0 , 3.0 ]))
print(average([]))预测完后,查看预期输出 怎么执行 标注让接口更容易阅读,函数体仍需要自己处理空列表。标注也不会强制每项自动变成 float。
动手改写: 自己用错误类型调用,确认真正失败发生在哪个操作。
示例 15_06
AI / Agent:用生成器模拟逐段到达的文字 运行文件:examples/ch15/ex15_06.py
Python 复制代码
def simulated_stream():
yield "先学"
yield " Python"
yield ",再做项目。"
pieces = []
stream = simulated_stream()
for piece in stream:
pieces.append(piece)
print(f"收到:{piece}" )
print("完整:" + "" .join(pieces))
print(list(stream))预测完后,查看预期输出 程序输出
收到:先学
收到: Python
收到:,再做项目。
完整:先学 Python,再做项目。
[]怎么执行 调用 simulated_stream 得到生成器,此时还未产出任何片段。for 每次索取下一个值,函数执行到 yield 暂停;下一轮从暂停处继续。遍历结束后,这个 stream 已耗尽,再转成列表是 [];重新调用函数才能得到新的生成器。片段是预设字符串,不是实际模型 token,也没有真实网络延迟。join 用空字符串连接,保留片段自身的空格。
动手改写: 把 join 的分隔符改成一个空格,观察为何会多出空格。再创建一个新的 stream,验证它能重新输出全部片段。
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 15_01 · 基础 我已独立完成
偶数生成器 实现 even_numbers(stop),使用 yield 产生 0 到 stop 之前的偶数,stop 为非负整数。打印 list(even_numbers(7))。
作答文件:practice/ch15/ex15_01.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 range 的步长用 2。
查看参考代码与解析 Python 复制代码
def even_numbers(stop):
for number in range(0 , stop, 2 ):
yield number
print(list(even_numbers(7 )))解题思路 函数返回生成器,list 在展示时消费它。空范围自然产生空列表。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 15_01
练习 15_02 · 场景 我已独立完成
按批产生列表 实现 batches(values,size),size 是整数,<=0 时在消费时抛 ValueError;每次 yield 一个至多 size 项的切片,保留最后不足一批的数据。打印对 [1,2,3,4,5] 与 2 的结果。
作答文件:practice/ch15/ex15_02.py
预期输出 程序输出
[[1, 2], [3, 4], [5]]本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 range(0,len(values),size),再切 [start:start+size]。
查看参考代码与解析 Python 复制代码
def batches(values, size):
if size <= 0 :
raise ValueError("size 必须为正数" )
for start in range(0 , len(values), size):
yield values[start:start + size]
print(list(batches([1 , 2 , 3 , 4 , 5 ], 2 )))解题思路 最后切片自动截到结尾。生成器中的验证在迭代开始时执行,而不是仅调用函数拿到对象时执行。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 15_02
练习 15_03 · 迁移 我已独立完成
配置展开调用 实现 format_run(name,epochs),返回 "名称:轮数"。用 config={"name":"demo","epochs":3} 的 ** 展开调用并打印。
作答文件:practice/ch15/ex15_03.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 字典键必须与形参名匹配。
查看参考代码与解析 Python 复制代码
def format_run(name, epochs):
return f"{name}:{epochs}"
config = {"name" : "demo" , "epochs" : 3 }
print(format_run(**config))解题思路 函数不知道参数是否来自字典展开,它收到的只是相应绑定。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 15_03
练习 15_04 · 推理 我已独立完成
生成器消费观察 建立 (x*x for x in range(3)),连续两次 print(list(g)),写出结果并解释。
作答文件:practice/ch15/ex15_04.py
预期输出 给我一点提示 第一次 list 取走所有元素。
查看参考代码与解析 Python 复制代码
g = (x * x for x in range(3 ))
print(list(g))
print(list(g))解题思路 想重新得到数据需要新建生成器,或第一次就保留生成的列表。不要把已经被日志打印消耗的生成器继续当作完整输入。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 15_04
练习 15_05 · 迁移 我已独立完成
AI / Agent:从事件流中提取文字片段 实现 text_pieces(events) 生成器。events 中每项保证是字典;只 yield kind 为 "text" 且 content 为非空字符串的内容,跳过其他项。用 [{"kind":"start"}, {"kind":"text","content":"Py"}, {"kind":"usage","content":3}, {"kind":"text","content":"thon"}] 生成片段并拼接打印。
作答文件:practice/ch15/ex15_05.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 先判 kind,再取 content 并检查类型与是否等于空字符串。不要 strip 后再输出,文本片段的空格可能是正文的一部分。
查看参考代码与解析 Python 复制代码
def text_pieces(events):
for event in events:
if event.get("kind" ) != "text" :
continue
content = event.get("content" )
if isinstance(content, str) and content != "" :
yield content
events = [
{"kind" : "start" },
{"kind" : "text" , "content" : "Py" },
{"kind" : "usage" , "content" : 3 },
{"kind" : "text" , "content" : "thon" },
]
print("" .join(text_pieces(events)))解题思路 start 和 usage 是非文本事件,不能把整个事件字典都拼进答案。生成器只产出 Py 和 thon,join 后得到 Python。额外测试保留单个空格片段、跳过空串和非字符串内容。本题使用自定义事件格式;实际 SDK 的事件名称与字段可能不同。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 15_05
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 建立清洗、验证、分割、编码与分批的顺序 区分文本、词、token ID 与张量 实现简单指标并识别数据泄漏风险 先看清数据走过哪些步骤
原始记录 → 检查字段类型 → 文本清洗 → 去空与去重 → 划分数据集 → 仅用训练数据建立统计规则 → 转换为数值表示 → 分批。这个顺序不是所有项目唯一答案,但它能帮助你识别哪些步骤可能把测试信息带入训练。
这一章所有数据都是很小的教学样本,只验证 Python 逻辑。它们不能支持模型效果结论,也不构成完整的深度学习训练系统。
文本清洗不要破坏任务信息
空白标准化可以 " ".join(text.split())。小写化对英文词频方便,但有时大小写本身有意义。数字、标点、代码缩进、换行对不同任务意义不同,不应统一无脑删除。先写明本次实验的清洗规则,再保存原始数据以便回溯。
词频不等于大模型 token 化
这里的 text.split() 只是按空白分词。中文一句话可能没有空格,专业 tokenizer 还可能把单词拆成子词、字节或其他单元。字典映射得到的 toy token ID 只供理解,不可直接喂给任意预训练模型;每个模型需要与其配套的 tokenizer 和特殊 token 约定。
划分时让记录保持完整
不要把 texts 和 labels 分别随机打乱,否则对应关系可能丢失。把它们放在同一条字典记录内,打乱记录列表,再切分。需要复现时使用局部随机生成器并记录 seed。
随机划分不是所有数据的正确策略。时间序列通常考虑时间先后;同一用户、病人、文档来源产生的相近记录通常要按组隔离;重复文本也可能让训练测试互相泄漏。先定义什么是独立样本,再选划分方式。
为什么词表只从训练集建立
假设测试集中出现从未见过的词,模型真实使用时也会遇到这种情况。若提前看全量数据建立词表或统计量,就把测试信息带入了预处理。教学管道中用训练集建词表,验证和测试遇到新词映射到 <UNK>。
同样原则适用于基于数据学习的均值、标准差、特征选择等。固定的、与样本无关的字符规则是另一类处理,但仍要明确记录。
形状与批大小
列表 [1,2,3] 可类比长度为 3 的一维数据;[[1,2],[3,4]] 可类比 2×2。Python 嵌套列表不自动保证每行等长,而真实张量的规则由数值库定义。常见文本批次有“批大小 × 序列长度”,之后才可能再有特征维度。先说清每一轴的含义,再谈 shape。
padding 是把短序列补到约定长度,attention mask 常用来标记哪些位置是真实内容;具体 0/1 含义需查所用模型接口。这里把 pad ID 设为 0,真实词 ID 从 2 开始,1 留给未知词,只是本课程自定义规则。
指标的分母决定含义
分类准确率是预测等于真实标签的个数除以样本数。长度必须一致;空集合的指标要明确约定。严重类别不平衡时,准确率可能具有误导性,后续还需学习精确率、召回率和 F1。不要把一个简单指标当作模型能力的完整描述。
这一阶段到下一阶段的桥
你现在应能读懂列表推导式、字典批次、函数参数、类方法和迭代器。下一阶段再学 NumPy 数组、广播与矩阵乘法,接着 PyTorch 张量、自动求导、Dataset/DataLoader,最后才是 tokenizer、Transformer 和训练/推理流程。新库安装时按其当时的官方兼容矩阵选择 Python 与硬件配置;本教材不预设未来深度学习环境的版本。
开源项目启发:先把工具流程拆成能单独检查的部分
参考微软 AI Agents for Beginners 的工具使用章节,我们把“提出动作”和“执行动作”分别写清楚。后面的自编离线示例用 decide 选择动作,用 search_notes 执行检索。把函数分开后,可以先检查工具本身,再检查控制流程;接入真实模型时,也更容易发现问题发生在哪一层。
时刻 输入或已有状态 本步动作 可检查的结果
开始 query 是“怎么理解函数?” observation 初始化为 None 还没有执行检索
第 1 步 observation 为 None decide 提出 search,执行器调用工具 得到正文与 note-02 来源
第 2 步 observation 已有工具结果 decide 提出 finish 输出正文与来源,停止
只允许 1 步 第 1 步后预算用完 不再执行第 2 步 报告尚未完成,不能冒充已交付
参考 smolagents 的循环实现,我们保留两个独立条件:是否已经结束、是否还有执行步数。这里仅借鉴可检查的循环结构,所有代码和数据均为本教材重新编写。工具超时、网络失败、真实模型调用还没有实现,后续阶段再逐层补齐。
从文本到模型:提前分清四种数量
LLMs-from-scratch 的文本处理章节把文本、token ID、取样与嵌入逐步连接起来。当前阶段先练清楚数据的形状,后续再学习算法和 PyTorch。
名称 此阶段可以观察什么 不应混淆什么
字符串长度 len("hi AI") 为 5 不代表模型 token 数
空白分词数 "hi AI".split() 得到 2 项 不是通用 tokenizer
词表 ID 自己给词分配整数编号 编号不是向量,也没有“越大越相似”的意义
batch 大小 一次取几条样本 不是每条样本长度
本章原有的词表、填充和批次示例继续使用教学用小数据。完成后再阅读真实 tokenizer 的输出,比较同一个字符串为什么可能被分成不同数量的 token。
实操:把“流程跑通”与“任务做对”分别验收
为后面的检索流程写四张测试卡:已知关键词、未知关键词、零步预算、一步预算。每张卡先写你预期的 status、answer 和 steps,再运行。本章新增综合题会自动检查这些边界。
再手动试“列表和函数有什么不同?”:示例检索器只取第一个命中,不能完整回答比较题。把它记在错题本中,说明“程序按规则执行成功”与“满足用户需求”为什么不同。未来加入模型后,这种对照样例仍有用,不能只根据程序没有报错来判断回答质量。
示例实验室 先预测,再运行,再完成每例后的改写任务。输入栏是运行时需键入的内容,不要写入代码文件。
示例 16_01
清洗并保序去重 运行文件:examples/ch16/ex16_01.py
Python 复制代码
raw = [" Hello Python " , "" , "hello python" , "Learn AI" ]
seen = set()
cleaned = []
for text in raw:
clean = " " .join(text.lower().split())
if clean and clean not in seen:
seen.add(clean)
cleaned.append(clean)
print(cleaned)预测完后,查看预期输出 程序输出
['hello python', 'learn ai']怎么执行 先规范化,才能识别空白和大小写差异造成的重复。保留第一次出现顺序;原始输入仍保存着。
动手改写: 解释对源代码文本直接使用这个规则会有什么问题。
示例 16_02
记录整体划分 运行文件:examples/ch16/ex16_02.py
Python 复制代码
import random
records = [{"id" : i, "label" : i % 2 } for i in range(6 )]
shuffled = records.copy()
random.Random(42 ).shuffle(shuffled)
train = shuffled[:4 ]
test = shuffled[4 :]
print(len(train), len(test))
print(set(row["id" ] for row in train).isdisjoint(row["id" ] for row in test))
print([row["id" ] for row in records])预测完后,查看预期输出 程序输出
4 2
True
[0, 1, 2, 3, 4, 5]怎么执行 copy 保护原顺序,记录中的 id 与 label 一起移动。这里只检查 ID 无重叠,完整数据泄漏检查还要考虑内容和来源。
动手改写: 让不同 ID 的文本完全相同,解释为什么 ID 检查不够。
示例 16_03
训练词表与未知词 运行文件:examples/ch16/ex16_03.py
Python 复制代码
train_texts = ["hello python" , "hello world" ]
vocab = {"<PAD>" : 0 , "<UNK>" : 1 }
for text in train_texts:
for word in text.split():
if word not in vocab:
vocab[word] = len(vocab)
encoded = [vocab.get(word, vocab["<UNK>" ]) for word in "hello ai" .split()]
print(vocab)
print(encoded)预测完后,查看预期输出 程序输出
{'<PAD>': 0, '<UNK>': 1, 'hello': 2, 'python': 3, 'world': 4}
[2, 1]怎么执行 ai 不在训练词表,因此映射到 1。词表只从 train_texts 建立。真实模型不能随意使用这个自建词表代替其配套 tokenizer。
动手改写: 添加一个训练词后重新建表,观察 ID 是否受顺序影响。
示例 16_04
补齐与掩码 运行文件:examples/ch16/ex16_04.py
Python 复制代码
sequences = [[2 , 3 , 4 ], [2 ]]
width = max(len(sequence) for sequence in sequences)
padded = []
masks = []
for sequence in sequences:
padding = width - len(sequence)
padded.append(sequence + [0 ] * padding)
masks.append([1 ] * len(sequence) + [0 ] * padding)
print(padded)
print(masks)预测完后,查看预期输出 程序输出
[[2, 3, 4], [2, 0, 0]]
[[1, 1, 1], [1, 0, 0]]怎么执行 每行长度变成批内最大长度。mask=1 表示本例真实位置,0 表示补齐位置。此例假定批次非空,练习中处理空输入。
动手改写: 加入空序列,确认该行全部为 padding。
示例 16_05
准确率与对齐 运行文件:examples/ch16/ex16_05.py
Python 复制代码
def accuracy(predictions, labels):
if len(predictions) != len(labels):
raise ValueError("长度不一致" )
if not labels:
return None
correct = sum(pred == label for pred, label in zip(predictions, labels))
return correct / len(labels)
print(f"{accuracy([1, 0, 1, 1], [1, 1, 1, 0]):.1%}" )预测完后,查看预期输出 怎么执行 布尔值参与 sum 时 True 当作 1,False 当作 0。先检查长度,避免 zip 的截短悄悄忽略样本。
动手改写: 构造全部猜主要类别但准确率很高的例子,说明指标局限。
示例 16_06
二维形状检查 运行文件:examples/ch16/ex16_06.py
Python 复制代码
rows = [[1 , 2 ], [3 , 4 ], [5 , 6 ]]
width = len(rows[0 ]) if rows else 0
is_rectangular = all(len(row) == width for row in rows)
print(len(rows), width)
print(is_rectangular)预测完后,查看预期输出 怎么执行 外层长度是行数,内层长度是列数;所有行等长才符合本例矩形结构。Python 列表本身不会强制这一规则。
动手改写: 把最后一行改为 [5],确认检查能发现不齐。
示例 16_07
AI / Agent:串起决策、工具、观察和结束 运行文件:examples/ch16/ex16_07.py
Python 复制代码
def search_notes(query):
notes = [
{"keyword" : "列表" , "text" : "列表保存有顺序的元素。" , "source" : "note-01" },
{"keyword" : "函数" , "text" : "return 把结果交回调用位置。" , "source" : "note-02" },
]
for note in notes:
if note["keyword" ] in query:
return {"text" : note["text" ], "source" : note["source" ]}
return {"text" : "没有找到相关笔记。" , "source" : None }
def decide(state):
if state["observation" ] is None :
return {"action" : "search" , "query" : state["query" ]}
return {"action" : "finish" }
state = {"query" : "怎么理解函数?" , "observation" : None }
finished = False
for step in range(1 , 4 ):
decision = decide(state)
print(f"第 {step} 步:{decision['action']}" )
if decision["action" ] == "search" :
state["observation" ] = search_notes(decision["query" ])
elif decision["action" ] == "finish" :
print(state["observation" ]["text" ])
print(f"来源:{state['observation']['source']}" )
finished = True
break
else :
raise ValueError("未知动作" )
if not finished:
print("达到步数上限,尚未完成" )预测完后,查看预期输出 程序输出
第 1 步:search
第 2 步:finish
return 把结果交回调用位置。
来源:note-02怎么执行 输入 query 进入 state;第一次 decide 看见 observation 为 None,提出 search 动作;执行器调用工具 search_notes,把结果写回 observation;第二次 decide 因已有结果而选择 finish;执行器输出结果和来源后结束。决策函数在这里完全由规则实现;没有模型推理、向量检索或生成式回答。检索只返回第一个命中的关键词笔记,能追踪来源不等于覆盖所有问题。固定上限可以限制此循环执行次数,但不能中断某一次已经卡住的工具函数。
动手改写: 把问题改成“什么是张量?”,观察无命中时的结果。把 range(1, 4) 改为 range(1, 2),确认工具虽执行了但流程还未 finish。最后解释:为什么只把一个函数命名为 agent 并不自动获得规划能力?
独立练习 先在 practice 中作答;提示和答案默认收起。勾选表示你已独立完成,不是自动判定通过。
练习 16_01 · 场景 我已独立完成
文本清洗函数 实现 normalize(text),转小写并将连续空白变成单空格,去两端空白。打印对 " Hello AI " 的结果。
作答文件:practice/ch16/ex16_01.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 lower、split、join。
查看参考代码与解析 Python 复制代码
def normalize(text):
return " " .join(text.lower().split())
print(normalize(" Hello AI " ))解题思路 规则应被记录并保持一致。对训练与推理文本使用同一预处理约定,避免流程不一致。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 16_01
练习 16_02 · 场景 我已独立完成
训练词表 实现 build_vocab(texts),初始 {"<PAD>":0,"<UNK>":1},按首次出现为按空白分出的词分配 ID;文本已预处理。打印 ["a b","a c"] 的结果。
作答文件:practice/ch16/ex16_02.py
预期输出 程序输出
{'<PAD>': 0, '<UNK>': 1, 'a': 2, 'b': 3, 'c': 4}本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 未出现才用当前字典长度分配。
查看参考代码与解析 Python 复制代码
def build_vocab(texts):
vocab = {"<PAD>" : 0 , "<UNK>" : 1 }
for text in texts:
for word in text.split():
if word not in vocab:
vocab[word] = len(vocab)
return vocab
print(build_vocab(["a b" , "a c" ]))解题思路 保留两个特殊 ID;同词再次出现不分配新 ID。只对训练集调用这个建表函数。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 16_02
练习 16_03 · 基础 我已独立完成
未知词编码 实现 encode(text,vocab),按空白拆词,用 vocab["<UNK>"] 处理未知词。打印 "a x" 在 {"<UNK>":1,"a":2} 下的结果。
作答文件:practice/ch16/ex16_03.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 get 默认未知词 ID。
查看参考代码与解析 Python 复制代码
def encode(text, vocab):
return [vocab.get(word, vocab["<UNK>" ]) for word in text.split()]
print(encode("a x" , {"<UNK>" : 1 , "a" : 2 }))解题思路 本题约定词表含 <UNK>。编码不修改词表,尤其不能因为看见测试词就自动扩充训练词表。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 16_03
练习 16_04 · 迁移 我已独立完成
准确率函数 实现 accuracy(predictions,labels),等长空列表返回 None,长度不同抛 ValueError,其余返回准确率。打印 [1,0] 对 [1,1] 的结果。
作答文件:practice/ch16/ex16_04.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 长度检查在 zip 前。
查看参考代码与解析 Python 复制代码
def accuracy(predictions, labels):
if len(predictions) != len(labels):
raise ValueError("长度不一致" )
if not labels:
return None
correct = sum(pred == label for pred, label in zip(predictions, labels))
return correct / len(labels)
print(accuracy([1 , 0 ], [1 , 1 ]))解题思路 先约定空输入行为,再计算分母。不能把空输入默认为 100% 正确。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 16_04
练习 16_05 · 挑战 我已独立完成
补齐批次 实现 pad_batch(sequences,pad_id=0),返回 (padded,masks),补到本批最大长度,空批返回 ([],[]),不修改输入。打印 [[2,3],[4]] 的结果。
作答文件:practice/ch16/ex16_05.py
预期输出 程序输出
([[2, 3], [4, 0]], [[1, 1], [1, 0]])本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 空批先返回;每行构建新列表。
查看参考代码与解析 Python 复制代码
def pad_batch(sequences, pad_id=0 ):
if not sequences:
return [], []
width = max(len(sequence) for sequence in sequences)
padded = []
masks = []
for sequence in sequences:
extra = width - len(sequence)
padded.append(sequence + [pad_id] * extra)
masks.append([1 ] * len(sequence) + [0 ] * extra)
return padded, masks
print(pad_batch([[2 , 3 ], [4 ]]))解题思路 返回两个对齐结构。空序列可以作为批中的一项;全空序列时宽度为 0。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 16_05
练习 16_06 · 场景 我已独立完成
验证记录 ID 唯一 实现 unique_ids(records),如果各字典的 id 唯一返回 True,重复则 False;空列表返回 True。打印 [{"id":1},{"id":1}] 的结果。
作答文件:practice/ch16/ex16_06.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 取 id 列表再比较集合长度。
查看参考代码与解析 Python 复制代码
def unique_ids(records):
ids = [record["id" ] for record in records]
return len(ids) == len(set(ids))
print(unique_ids([{"id" : 1 }, {"id" : 1 }]))解题思路 本题假定 id 字段存在且可哈希。真实导入流程需先验证字段类型,再执行唯一性检查。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 16_06
练习 16_07 · 综合 我已独立完成
AI / Agent:实现可测试的两步工具流程 实现 lookup_agent(query, max_steps=2)。query 必须是字符串;max_steps 必须是非负 int(不接受 bool),否则 raise ValueError。固定笔记字典为 {"列表":"有序容器", "函数":"可复用的处理步骤"},按 query 完全匹配查找。第 1 步保存查找结果;第 2 步返回 {"status":"done", "answer":结果或"未找到", "steps":2}。若步数不够,返回 {"status":"limit", "answer":None, "steps":已执行步数}。只需这两种动作,最多执行两步。调用 "函数" 并依次输出 status、answer、steps。
作答文件:practice/ch16/ex16_07.py
预期输出 本题含额外函数测试,请使用题目指定的函数或类名。
给我一点提示 先验证输入,再用 for 从 1 到 max_steps 迭代。第 1 步查询,第 2 步立即 return。循环正常结束则说明还没完成。max_steps=0 时应执行零步。
查看参考代码与解析 Python 复制代码
def lookup_agent(query, max_steps=2 ):
if not isinstance(query, str):
raise ValueError("query 必须是字符串" )
if type(max_steps) is not int or max_steps < 0 :
raise ValueError("max_steps 必须为非负整数" )
notes = {"列表" : "有序容器" , "函数" : "可复用的处理步骤" }
observation = None
steps = 0
for step in range(1 , max_steps + 1 ):
steps = step
if step == 1 :
observation = notes.get(query, "未找到" )
else :
return {"status" : "done" , "answer" : observation, "steps" : steps}
return {"status" : "limit" , "answer" : None , "steps" : steps}
result = lookup_agent("函数" )
print(result["status" ])
print(result["answer" ])
print(result["steps" ])解题思路 第 1 步只查找,第 2 步才交付结果,因此 max_steps=1 必须报告 limit。max_steps 很大时也在第 2 步提前返回,不能为了用完预算继续循环。未知问题得到 done/未找到,表示流程已结束但没有证据;不能伪造知识补空缺。先画出 max_steps=0、1、2、5 四条执行轨迹,再运行额外测试。本题是离线规则流程,完成它意味着理解了 Python 控制流与状态传递。
保存参考答案 .py · 看过后,明天关掉答案重写一次。
自检:python tools/check_practice.py 16_07
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 把需求拆成数据、函数和验证规则 运行完整项目并核对输出文件 在已有程序上独立增加一个功能 项目练习的正确顺序
先读项目需求和输入数据,不要先打开 app.py。用中文写出 5—8 个步骤,在 starter.py 里实现最小版本。先用内存里的三条假数据验证,再加入文件读取,最后才加命令行界面。app.py 是完整参考实现,分步任务并不要求一开始就复刻它的全部结构。starter.py 中的 NotImplementedError 是提醒你填入代码的练习标记;它报错并不表示 Python 安装失败。每次只完成一个函数,再运行底部的小样例检查。
每个项目目录都有 README.md、TASKS.md、starter.py 和 app.py。需要输入数据的项目另有 data/。默认运行会把结果写进该项目 outputs/。这些都是专用练习输出;重跑项目 02—04 会覆盖同名报告,项目 01 的 add 会追加记录。不要将练习输出路径改成自己的重要文件。
四次递进练习
学习打卡本 :日期、分钟、主题 → 验证 → JSON 保存 → 汇总。重点是函数边界、文件持久化、错误恢复。
成绩表清洗 :含错误的 CSV → 按字段检查 → 有效记录与错误记录分流 → 排名和统计。重点是字符串转数字、边界和错误报告。
文本语料清洗 :JSONL → 解析 → 规范化 → 去空去重 → 词频。重点是字典、集合和数据可追溯性。
数据集管道 :记录验证 → 划分 → 训练词表 → 编码 → 补齐 → 分批。重点是数据对齐、未知词、形状和复现。
把一个任务拆成函数的完整示范
以成绩表为例,输入是路径,不是分数列表。读取后每条 CSV 记录仍是字符串字典。先约定 student_id、name、score 三列必须存在;再对每行去空白、转整数、检查范围。合法记录加入 valid,非法记录加入 errors。两个结果列表形成“验证层”的输出。
统计层只接收 valid,所以不必在每次求平均时重新猜哪些分数可用。最后输出层把统计对象序列化成 JSON,把 errors 写成 CSV。这样如果平均分错了,可以直接测试统计层;如果文件找不到,则看读取层;不必每次从头怀疑全部代码。
项目验收的五个问题
用最小样本手算,程序结果一致吗?
没有数据、只有一条、全是坏数据时会怎样?
用户看到的错误能找到对应记录吗?
换一个启动目录或另一台电脑,路径是否仍然正确?
同样输入再跑一次,哪些文件覆盖、哪些记录追加,行为是否符合说明?
新出现的 argparse 不用死记
项目里的 argparse 来自标准库。ArgumentParser 建立命令说明,add_argument 声明参数,parse_args 把终端输入读成对象。args.input 对应 --input;命令中的带空格主题要加引号。先运行 python app.py --help,按照给出的命令执行,再回看源代码中相应定义。
注意运行位置:本章给出的命令从学习包根目录 执行。如果已进入某个项目目录,命令可以简化为 python app.py。Windows/Mac 的解释器名字按第 00 章替换。
不看答案的通关挑战
给成绩报告增加“分数区间人数”,区间为 0—59、60—79、80—89、90—100。先手算样例,应为 2、1、1、1。把逻辑写成独立函数,测试 59、60、79、80、89、90、100。再把结果加入输出 JSON。完成后用自己的话解释:为什么区间不能互相重叠,为什么不能把 100 漏掉。
另一个挑战:给学习打卡增加“按主题累计”,清洗主题两端空格但不随意更改中文内容。对 30 分钟 Python、20 分钟 Git、15 分钟 Python,结果应为 Python=45、Git=20。把这题与第 08 章的分类累计联系起来。
最后一次自我验收
以下命令核验参考项目,不会替你完成 starter.py:
终端命令 复制代码
python tools/check_projects.py
自己的实现仍要用 TASKS.md 的验收案例检查。工具通过只能证明参考实现通过了已写检查;你能否不看答案重建其中一个项目,才是本阶段最重要的学习证据。
PROJECT 01
学习打卡与统计 前置:第 00—13 章
写一个命令行学习记录本:添加日期、学习分钟和主题;下次打开仍可读取;汇总次数、总分钟和平均分钟。分钟必须为非负整数,主题不能为空。文件损坏时报告问题,不把坏文件当空记录覆盖。
1 先写核心函数
先对内存中的记录列表写 summarize(records),输入 30、60 分钟时总量为 90、平均 45.0。空列表返回平均 None。此时完全不需要文件或命令行。
2 加入文件持久化
用 JSON 保存记录列表。文件不存在视作首次使用,返回空列表;文件存在但内容错误则抛出清楚异常。分清这两种情况,避免误覆盖用户数据。
3 检查输入再修改
将日期用 date.fromisoformat 验证;分钟用整数且非负检查;主题 strip 后检查非空。所有检查通过后才 append。
4 加入命令行入口
argparse 是标准库命令行参数解析器。add 子命令收 minutes 与 topic;summary 子命令显示统计;--store 指定记录位置。看不懂时先运行 --help,再按已给命令练习。
5 独立扩展
增加按主题汇总,不直接改现有统计函数;给新函数写 3 个例子。进一步可做删除记录,但先为每条记录设计稳定 ID,并明确删除规则。
运行命令 终端命令 复制代码
python projects/01_study_log/app.py add 30 "变量与类型" --date 2026-10-07
python projects/01_study_log/app.py add 60 "循环练习" --date 2026-10-08
python projects/01_study_log/app.py summary预期结果与输出位置 初始无记录时执行两次 add,分别显示已保存 1 条和 2 条;summary 显示记录数 2、总分钟 90、平均分钟 45.0。反复 add 会继续追加,所以重复练习前可使用 --store 指向另一个新的练习文件。
保存项目任务书 · 保存完整参考代码
展开完整实现(建议先尝试) Python 复制代码
"""学习记录本。先运行 python app.py --help 查看命令。"""
import argparse
import json
from datetime import date
from pathlib import Path
BASE = Path(__file__).resolve().parent
def validate_record(record):
if not isinstance(record, dict):
raise ValueError("记录必须是对象" )
if not isinstance(record.get("date" ), str):
raise ValueError("记录日期必须是字符串" )
date.fromisoformat(record["date" ])
minutes = record.get("minutes" )
if type(minutes) is not int or minutes < 0 :
raise ValueError("分钟必须是非负整数" )
topic = record.get("topic" )
if not isinstance(topic, str) or not topic.strip():
raise ValueError("主题不能为空" )
def load_records(path):
if not path.exists():
return []
records = json.loads(path.read_text(encoding="utf-8" ))
if not isinstance(records, list):
raise ValueError("记录文件最外层必须是列表" )
for record in records:
validate_record(record)
return records
def summarize(records):
total = sum(record["minutes" ] for record in records)
count = len(records)
return {"count" : count, "total" : total, "mean" : total / count if count else None }
def add_record(path, minutes, topic, day):
record = {"date" : day, "minutes" : minutes, "topic" : topic.strip()}
validate_record(record)
records = load_records(path)
records.append(record)
path.parent.mkdir(parents=True , exist_ok=True )
temporary = path.with_name(path.name + ".tmp" )
temporary.write_text(json.dumps(records, ensure_ascii=False , indent=2 ), encoding="utf-8" )
temporary.replace(path)
return len(records)
def main():
parser = argparse.ArgumentParser(description="学习打卡记录本" )
parser.add_argument("--store" , type=Path, default=BASE / "outputs" / "study_log.json" )
sub = parser.add_subparsers(dest="command" , required=True )
add = sub.add_parser("add" , help="添加一条记录" )
add.add_argument("minutes" , type=int)
add.add_argument("topic" )
add.add_argument("--date" , default=date.today().isoformat())
sub.add_parser("summary" , help="查看汇总" )
args = parser.parse_args()
try :
if args.command == "add" :
count = add_record(args.store, args.minutes, args.topic, args.date)
print(f"已保存 {count} 条记录" )
else :
stats = summarize(load_records(args.store))
print(f"记录数:{stats['count']}" )
print(f"总分钟:{stats['total']}" )
print("平均分钟:暂无" if stats["mean" ] is None else f"平均分钟:{stats['mean']:.1f}" )
except (ValueError, OSError) as error:
parser.exit(1 , f"无法完成:{error}\n" )
if __name__ == "__main__" :
main()逐步对照任务书,确认每个函数在处理哪一层数据。
PROJECT 02
成绩 CSV 清洗与报告 前置:第 00—13 章
读取含坏数据的成绩表。逐条检查 ID、姓名、分数和重复 ID,保留有效记录,把无效行及原因写入错误报告,输出人数、平均分、通过率与排名。CSV 的逻辑记录可能跨物理行,因此报告记录号不伪称绝对物理行号。
1 列出数据约定
必需列为 student_id、name、score;ID 和姓名非空;score 是 0—100 的整数字符串;相同 ID 首条有效记录保留,后续重复记错。
2 手算样本
先查看 data/scores.csv。有效分数为 80、60、100、59、0,总分 299,平均 59.8;达到 60 有 3 人,通过率 60%。有 4 条错误记录。
3 一条一条验证
先处理字段缺失,再转整数,再检查范围,最后检测已保留 ID 是否重复。错误放入列表并继续,不让一条坏数据阻止所有正常记录。
4 统计与排序
只对有效数据计算。空数据平均分与通过率为 None,排名为空。sorted 用负分数与 ID 组成 key,保证同分顺序稳定可说明。
5 保存并核验
写 report.json 和 errors.csv,读回来确认 valid_count+invalid_count 等于输入数据记录数。修改一条坏记录后重跑,确认计数相应变化。
运行命令 终端命令 复制代码
python projects/02_score_report/app.py
python projects/02_score_report/app.py --help预期结果与输出位置 显示:有效 5 条,无效 4 条;平均分 59.8;通过率 60.0%。outputs/report.json 保存完整结果,outputs/errors.csv 保存记录号与错误原因。重复运行覆盖这两个专用输出文件。
保存项目任务书 · 保存完整参考代码
展开完整实现(建议先尝试) Python 复制代码
"""成绩清洗报告;只覆盖 --out 指定目录中的本项目输出文件。"""
import argparse
import csv
import json
from pathlib import Path
BASE = Path(__file__).resolve().parent
def analyze(path):
valid = []
errors = []
seen = set()
with path.open(encoding="utf-8-sig" , newline="" ) as file:
reader = csv.DictReader(file)
required = {"student_id" , "name" , "score" }
if not required.issubset(set(reader.fieldnames or [])):
raise ValueError("缺少必需列 student_id、name、score" )
for record_number, row in enumerate(reader, start=1 ):
try :
if None in row:
raise ValueError("字段数超过表头列数" )
student_id = (row.get("student_id" ) or "" ).strip()
name = (row.get("name" ) or "" ).strip()
if not student_id or not name:
raise ValueError("ID 或姓名为空" )
try :
score = int(row.get("score" ) or "" )
except ValueError:
raise ValueError("分数不是整数" ) from None
if not 0 <= score <= 100 :
raise ValueError("分数超出 0 到 100" )
if student_id in seen:
raise ValueError("重复 ID" )
except ValueError as error:
errors.append({"record" : record_number, "reason" : str(error)})
continue
seen.add(student_id)
valid.append({"student_id" : student_id, "name" : name, "score" : score})
count = len(valid)
total = sum(row["score" ] for row in valid)
passed = sum(row["score" ] >= 60 for row in valid)
return {
"valid_count" : count,
"invalid_count" : len(errors),
"total" : total,
"mean" : total / count if count else None ,
"pass_rate" : passed / count if count else None ,
"ranking" : sorted(valid, key=lambda row: (-row["score" ], row["student_id" ])),
"errors" : errors,
}
def save_report(report, folder):
folder.mkdir(parents=True , exist_ok=True )
(folder / "report.json" ).write_text(json.dumps(report, ensure_ascii=False , indent=2 ), encoding="utf-8" )
with (folder / "errors.csv" ).open("w" , encoding="utf-8-sig" , newline="" ) as file:
writer = csv.DictWriter(file, fieldnames=["record" , "reason" ])
writer.writeheader()
writer.writerows(report["errors" ])
def main():
parser = argparse.ArgumentParser(description="成绩表清洗与统计" )
parser.add_argument("--input" , type=Path, default=BASE / "data" / "scores.csv" )
parser.add_argument("--out" , type=Path, default=BASE / "outputs" )
args = parser.parse_args()
try :
report = analyze(args.input)
save_report(report, args.out)
except (OSError, ValueError, csv.Error) as error:
parser.exit(1 , f"处理失败:{error}\n" )
print(f"有效 {report['valid_count']} 条,无效 {report['invalid_count']} 条" )
if report["mean" ] is None :
print("没有有效分数,无法计算平均分与通过率" )
else :
print(f"平均分 {report['mean']:.1f}" )
print(f"通过率 {report['pass_rate']:.1%}" )
if __name__ == "__main__" :
main()逐步对照任务书,确认每个函数在处理哪一层数据。
PROJECT 03
文本语料清洗与词频 前置:第 00—13 章和第 16 章
把原始 JSONL 清洗成可追踪的小语料:验证 id 与 text,统一英文大小写及空白,去除空文本和重复文本,保留原记录 ID,输出干净 JSONL、错误与重复记录报告和词频。
1 定义清洗规则
仅针对本项目英文教学文本:lower 后按空白 split/join。保留标点。去重依据规范化后的完整文本;第一条有效记录保留。输出词频按空白分词,不是模型 tokenizer。
2 分离解析与清洗
逐行 JSON 解析可能失败;对象结构和字段类型也可能不对。先确认 dict,再检查 id 与 text 的类型,最后做字符串方法。
3 分别记拒绝与重复
坏 JSON、缺字段、非字符串、空文本、重复已保留 ID 计为 rejected;同规范化文本计为 duplicate。两类都记物理行号和原因。不要悄悄消失记录。
4 用小样本核对
样本共 9 个非空行,保留 3,重复文本 1,拒绝 5。干净文本为 hello python、learn python、hello world;python 和 hello 各 2 次。
5 独立扩展
增加 --min-length 过滤太短文本,给报告增加过滤原因。再测试中文和代码文本,说明为什么当前规则不能直接代表所有语料任务。
运行命令 终端命令 复制代码
python projects/03_text_cleaner/app.py
python projects/03_text_cleaner/app.py --help预期结果与输出位置 输出非空行 9、保留 3、重复 1、拒绝 5。outputs/clean.jsonl 保存干净记录;outputs/report.json 保存每个被排除行的原因和排序词频。
保存项目任务书 · 保存完整参考代码
展开完整实现(建议先尝试) Python 复制代码
"""英文教学语料清洗器,空白分词不是大模型 tokenizer。"""
import argparse
import json
from collections import Counter
from pathlib import Path
BASE = Path(__file__).resolve().parent
def normalize(text):
return " " .join(text.lower().split())
def clean_file(path):
kept = []
rejected = []
duplicates = []
seen_text = set()
seen_ids = set()
input_count = 0
with path.open(encoding="utf-8" ) as file:
for line_number, line in enumerate(file, start=1 ):
if not line.strip():
continue
input_count += 1
try :
record = json.loads(line)
if not isinstance(record, dict):
raise ValueError("记录必须是 JSON 对象" )
if not isinstance(record.get("id" ), str) or not record["id" ].strip():
raise ValueError("id 必须为非空字符串" )
if not isinstance(record.get("text" ), str):
raise ValueError("text 必须为字符串" )
record_id = record["id" ].strip()
text = normalize(record["text" ])
if not text:
raise ValueError("清洗后文本为空" )
if record_id in seen_ids:
raise ValueError("与已保留记录的 ID 重复" )
except (ValueError, json.JSONDecodeError) as error:
rejected.append({"line" : line_number, "reason" : str(error)})
continue
if text in seen_text:
duplicates.append({"line" : line_number, "reason" : "规范化文本重复" })
continue
seen_text.add(text)
seen_ids.add(record_id)
kept.append({"id" : record_id, "text" : text})
counts = Counter(word for record in kept for word in record["text" ].split())
frequencies = sorted(counts.items(), key=lambda item: (-item[1 ], item[0 ]))
report = {"input_count" : input_count, "kept_count" : len(kept),
"duplicate_count" : len(duplicates), "rejected_count" : len(rejected),
"duplicates" : duplicates, "rejected" : rejected, "word_counts" : frequencies}
return kept, report
def save_outputs(records, report, folder):
folder.mkdir(parents=True , exist_ok=True )
with (folder / "clean.jsonl" ).open("w" , encoding="utf-8" ) as file:
for record in records:
file.write(json.dumps(record, ensure_ascii=False ) + "\n" )
(folder / "report.json" ).write_text(json.dumps(report, ensure_ascii=False , indent=2 ), encoding="utf-8" )
def main():
parser = argparse.ArgumentParser(description="清洗英文教学 JSONL 语料" )
parser.add_argument("--input" , type=Path, default=BASE / "data" / "raw.jsonl" )
parser.add_argument("--out" , type=Path, default=BASE / "outputs" )
args = parser.parse_args()
try :
records, report = clean_file(args.input)
save_outputs(records, report, args.out)
except (OSError, UnicodeError) as error:
parser.exit(1 , f"处理失败:{error}\n" )
print(f"非空行 {report['input_count']},保留 {report['kept_count']},重复 {report['duplicate_count']},拒绝 {report['rejected_count']}" )
if __name__ == "__main__" :
main()逐步对照任务书,确认每个函数在处理哪一层数据。
PROJECT 04
数据集划分 编码与分批 前置:第 00—16 章
读取小型带标签文本数据集,验证记录、ID 与规范化文本唯一性;可复现地划分训练测试;只用训练文本建词表;把各批编码、补齐并生成掩码;保存配置与输出。此项目不训练模型,专门练 Python 数据流。
1 先写四个纯函数
normalize、split_records、build_vocab、pad_batch。每个先用 2—3 条手造数据检查。把数据处理和文件输出分开,错误更容易定位。
2 约定保留比例
train_ratio 满足 0<ratio<1,至少 2 条数据;n_train=int(n*ratio),再限制到 1 到 n-1,保证两边非空。小数据可能导致实际比例不同,报告实际条数。
3 一次打乱完整记录
复制外层记录列表,用 random.Random(seed).shuffle。ID、text、label 一起移动,避免标签错配。样本按规范化文本完全重复时直接拒绝;相近内容或同源分组需要更复杂检查。
4 训练侧建立词表
保留 <PAD>=0、<UNK>=1,只看 train。测试中的未知词映射为 1。保存 vocab.json,让下一次编码使用相同映射。
5 切批与补齐
每批独立算最大长度;保存 ids、labels、input_ids、attention_mask。最后不足 batch_size 的批保留。让你能检查每个维度与记录对齐。
6 验收与扩展
检查训练测试 ID 无交集、样本数守恒、每批 labels 数与 input_ids 行数相等、每行掩码长度与 ID 长度相等。自行加入分层划分或按来源分组,先写清规则再实现。
运行命令 终端命令 复制代码
python projects/04_dataset_pipeline/app.py
python projects/04_dataset_pipeline/app.py --seed 7 --batch-size 3 --train-ratio 0.75预期结果与输出位置 默认 8 条数据,训练 6、测试 2;批大小 2 时训练 3 批、测试 1 批。outputs/ 中有 train.jsonl、test.jsonl、vocab.json、batches.json、manifest.json。默认重复运行应得到相同输出(同一运行环境)。
保存项目任务书 · 保存完整参考代码
展开完整实现(建议先尝试) Python 复制代码
"""仅用标准库的数据管道;toy 词表不能替代预训练模型 tokenizer。"""
import argparse
import json
import random
from pathlib import Path
BASE = Path(__file__).resolve().parent
def normalize(text):
return " " .join(text.lower().split())
def load_records(path):
records = []
ids = set()
texts = set()
with path.open(encoding="utf-8" ) as file:
for number, line in enumerate(file, start=1 ):
if not line.strip():
continue
row = json.loads(line)
if not isinstance(row, dict):
raise ValueError(f"第 {number} 行不是对象" )
if not isinstance(row.get("id" ), str) or not row["id" ].strip():
raise ValueError(f"第 {number} 行 ID 非法" )
if not isinstance(row.get("text" ), str):
raise ValueError(f"第 {number} 行 text 非字符串" )
if type(row.get("label" )) is not int or row["label" ] not in (0 , 1 ):
raise ValueError(f"第 {number} 行 label 必须是整数 0 或 1" )
record_id = row["id" ].strip()
text = normalize(row["text" ])
if not text or record_id in ids or text in texts:
raise ValueError(f"第 {number} 行为空文本、重复 ID 或重复文本" )
ids.add(record_id)
texts.add(text)
records.append({"id" : record_id, "text" : text, "label" : row["label" ]})
return records
def split_records(records, train_ratio=0.75 , seed=42 ):
if not 0 < train_ratio < 1 :
raise ValueError("train_ratio 必须在 0 与 1 之间" )
if len(records) < 2 :
raise ValueError("至少需要 2 条有效记录" )
shuffled = list(records)
random.Random(seed).shuffle(shuffled)
cut = max(1 , min(len(records) - 1 , int(len(records) * train_ratio)))
return shuffled[:cut], shuffled[cut:]
def build_vocab(records):
vocab = {"<PAD>" : 0 , "<UNK>" : 1 }
for row in records:
for word in row["text" ].split():
if word not in vocab:
vocab[word] = len(vocab)
return vocab
def pad_batch(sequences):
width = max((len(sequence) for sequence in sequences), default=0 )
padded = [sequence + [0 ] * (width - len(sequence)) for sequence in sequences]
masks = [[1 ] * len(sequence) + [0 ] * (width - len(sequence)) for sequence in sequences]
return padded, masks
def make_batches(records, vocab, size):
if size <= 0 :
raise ValueError("batch_size 必须为正整数" )
result = []
for start in range(0 , len(records), size):
rows = records[start:start + size]
sequences = [[vocab.get(word, 1 ) for word in row["text" ].split()] for row in rows]
padded, masks = pad_batch(sequences)
result.append({"ids" : [row["id" ] for row in rows],
"labels" : [row["label" ] for row in rows],
"input_ids" : padded, "attention_mask" : masks})
return result
def write_jsonl(path, rows):
with path.open("w" , encoding="utf-8" ) as file:
for row in rows:
file.write(json.dumps(row, ensure_ascii=False ) + "\n" )
def run(input_path, output_dir, train_ratio=0.75 , seed=42 , batch_size=2 ):
records = load_records(input_path)
train, test = split_records(records, train_ratio, seed)
vocab = build_vocab(train)
batches = {"train" : make_batches(train, vocab, batch_size),
"test" : make_batches(test, vocab, batch_size)}
manifest = {"seed" : seed, "requested_train_ratio" : train_ratio, "batch_size" : batch_size,
"total" : len(records), "train_count" : len(train), "test_count" : len(test),
"vocab_size" : len(vocab), "normalization" : "lower + whitespace split/join" ,
"tokenizer" : "teaching-only whitespace tokenizer" }
output_dir.mkdir(parents=True , exist_ok=True )
write_jsonl(output_dir / "train.jsonl" , train)
write_jsonl(output_dir / "test.jsonl" , test)
for name, value in [("vocab.json" , vocab), ("batches.json" , batches), ("manifest.json" , manifest)]:
(output_dir / name).write_text(json.dumps(value, ensure_ascii=False , indent=2 ), encoding="utf-8" )
return manifest
def main():
parser = argparse.ArgumentParser(description="教学文本数据集管道" )
parser.add_argument("--input" , type=Path, default=BASE / "data" / "samples.jsonl" )
parser.add_argument("--out" , type=Path, default=BASE / "outputs" )
parser.add_argument("--seed" , type=int, default=42 )
parser.add_argument("--batch-size" , type=int, default=2 )
parser.add_argument("--train-ratio" , type=float, default=0.75 )
args = parser.parse_args()
try :
result = run(args.input, args.out, args.train_ratio, args.seed, args.batch_size)
except (ValueError, OSError) as error:
parser.exit(1 , f"处理失败:{error}\n" )
print(f"共 {result['total']} 条,训练 {result['train_count']},测试 {result['test_count']}" )
print(f"词表 {result['vocab_size']} 项,批大小 {result['batch_size']}" )
if __name__ == "__main__" :
main()逐步对照任务书,确认每个函数在处理哪一层数据。
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 下一章 → 本章学会什么 安排可执行的复习节奏 通过阶段检查找到薄弱点 知道进入 NumPy 与 PyTorch 前应会什么 一个可调整的八周路线
这只是安排参考,不是保证八周精通。建议每周 5 天,每天 45—75 分钟;基础薄弱或工作繁忙时,每周内容可以拆成两周。
周次 重点 必须留下的实作成果
1 第 00—03 章 能独立运行文件,完成输入转换和文本清洗
2 第 04—06 章 写成绩分级、累计、菜单退出,手工追踪循环
3 第 07—08 章 会列表与字典,解释共享对象,写分组和词频
4 第 09—10 章 把旧程序改成函数,处理空数据并完成额外测试
5 第 11—13 章 读写 CSV/JSON,创建环境,定位并修复报错
6 第 14—15 章 看懂 self、数据集类、生成器与 ** 参数展开
7 第 16—17 章项目 01—03 独立写一个有持久化输出的完整程序
8 项目 04 与综合验收 画出数据处理路径,增加一个功能,重做错题
记忆靠提取,不靠反复划线
学完当天合上教材,用空文件写一个同类例子;次日重写错题;第 3 天换输入和场景;第 7 天混合不同章节;第 14 天在项目中复用。这些间隔只是可操作的复习安排,不是对所有人最佳的固定规律。
每张记忆卡只放一个问题,例如“sort 返回什么?”、“空列表的 all 是什么?”、“input 得到什么类型?”、“为什么 b=a 后改 b 会影响 a?”先口头回答,再看背面并运行小例子。不要把十个语法点塞在一张卡里。
一页关键记忆表
问题 记住的核心 最小例子
= 与 == 赋值与比较 x=3;x==3
索引与切片 单点与区间,终点不含 s[0];s[:3]
input 类型 先是字符串 int(input())
append 与 extend 加一个与逐个加 a.append([1,2])
sort 与 sorted 原地改并返回 None 与新列表 b=sorted(a)
b=a 与 copy 共享对象与新外层 b=a.copy()
print 与 return 显示与把值交回 return total
break 与 continue 结束循环与跳本轮后半段 if bad: continue
in 在 dict 中 默认查键 "name" in row
None 与 0 缺失与有效数值可能不同 value is None
get 的默认值 读取缺失键,不自动写回 d.get(k,0)
CSV 与 JSON 文本字段表格与嵌套对象 int(row["score"])
路径 当前目录与脚本目录区别 Path(__file__).parent
yield 与 return 暂停给一个与结束 yield batch
常见报错恢复路线
“找不到 Python”:关闭并重开终端,检查安装,分别试 Windows 的 py、Mac 的 python3。
“can't open file”:先确认完整解压,检查当前目录与文件名,路径有空格时加引号。不要把教材里的尖括号占位文字当作真实路径。
“ModuleNotFoundError”:标准库先检查是否拼错和是否用同名文件遮蔽;第三方库检查当前解释器与 pip 对应,不能只看另一个终端是否曾安装。
“TypeError 或 ValueError”:打印 type 和 repr,确认数据内容。字符串 "80" 与整数 80 外观相似,操作规则不同。
“网页勾选丢失”:浏览器本地存储不是跨电脑云同步;导入之前导出的进度 JSON。你的代码保存在 .py 文件,与网页勾选独立。
阶段自测 A 语法解释
先口头回答,再在对应章节验证:
print("3"*2) 与 print(3*2) 分别是什么?答案:33 与 6;前者字符串重复。
list(range(2,7,2)) 是什么?答案:[2,4,6],不包含 7。
a=[1]; b=a; b.append(2) 后 a 是什么?答案:[1,2],共享对象。
函数只有 print 没有 return,返回什么?答案:None。
bool("False") 是什么?答案:True,非空字符串。
空列表可以直接求平均吗?答案:不能直接除以 len;需定义空数据行为。
d.get("x",0) 会创建 x 键吗?答案:不会。
第二次 list(g) 为什么可能为空?答案:同一个生成器已经消费完。
标注 x: int 会自动转型吗?答案:不会。
训练集和测试集标签为什么不能分别打乱?答案:会破坏样本与标签配对。
阶段自测 B 独立编程
不给参考代码,按下面案例自己验证:
写 summarize_minutes([0,30,45]),返回总分钟 75、学习天数 2、按全部 3 天计算的日均 25.0;空列表日均 None。要求说明“按全部天数”与“只按学习日”的区别。
写 top_words("a b a c b a",2),返回 [("a",3),("b",2)];同频按词的字母顺序。空文本返回空列表。
写 clean_scores(["80","x","101","0"]),返回有效分数 [80,0] 与无效项位置 [2,3],位置从 1 开始。把它接到 CSV 读取程序。
写 chunks([1,2,3,4,5],2),返回 [[1,2],[3,4],[5]];size<=0 抛 ValueError;不改变输入列表。
完成标准不是记住这几个答案,而是你能为每个函数换 3 组输入,手算预期,然后解释结果不一致时是哪一步出错。
项目自评 100 分
需求与输入输出约定 15 分;核心逻辑正确 30 分;边界与错误处理 20 分;函数拆分与命名 15 分;可重复运行与路径 10 分;能解释并独立扩展 10 分。80 分可作为继续下一阶段的参考线,但若还分不清 return、列表共享或字典列表层次,应先回补,即使总分够也别跳过。
进入深度学习前的能力清单
能够独立读写 CSV/JSONL;能解释嵌套列表与字典的层级;能编写带参数、返回值和边界检查的函数;能定位类型、路径和导入错误;能读懂类、迭代器与批次;会建立环境并记录依赖;知道训练测试应保持独立。这些基础比提前背某个模型 API 更耐用。
下一阶段的顺序建议是:NumPy 数组与形状 → 数学中的向量、矩阵和导数直觉 → PyTorch 张量与自动求导 → 小型分类任务完整训练流程 → tokenizer、Transformer 与模型推理 → 根据目标学习微调、评估或检索增强。每一阶段仍然用小数据、可核对输出和独立实现来检验理解。
官方资料与核对日期
本教材是为零基础重新组织的原创讲解和练习,并非 Python 官方教程的翻译。安装与环境说明参考官方资料;语法细节有争议时以对应版本文档为准。核对日期:2026-10-07。
以后向助教提问的模板
“我正在做第 XX 章第 XX 题;我理解目标是……;我的代码是……;输入是……;我预计输出……;实际输出或完整报错是……;我已经试过……。请先提示定位思路,不直接给完整答案。”
能把问题这样说清楚,本身就是一种重要的编程能力。你不需要一次记住所有方法,但要能把数据、步骤和错误描述清楚。
AI / Agent 穿插练习后的自测
请先不运行代码,用自己的话回答,再到对应章节验证。
f-string 拼出一段提示词,是否已经调用了大模型?没有,只生成了 Python 字符串。
对中文文本直接 split(),能否算出模型 token 数?不能。默认 split 按空白切分;具体模型使用的 tokenizer 有自己的规则。
列表里的 role 字段会自动触发某种模型权限吗?不会;它在本教材里只是普通数据。实际服务如何解释字段要看它自己的接口约定。
工具返回 {"count": 3},与打印这段文字有何不同?返回值可供调用方继续取字段;打印本身不把这个字典作为函数结果交回。
模型回复能被 json.loads 解析,是否一定能执行?不能。还要检查结构、参数和允许的工具名;不能把回复直接交给 eval 或 exec。
规则检索返回了一篇文档,是否说明回答可靠?不能。还要看检索是否相关、答案是否由证据支持;本章小程序只演示流程。
后续学习的连接点
掌握字符串、列表和字典后,再学习 tokenizer、张量和批次;掌握函数、异常和文件后,再学习真实 API 的请求、响应、失败处理与日志;掌握类和生成器后,再阅读 Agent 框架中的状态对象与流式事件。每次只替换一层,例如先保留现有工具函数,只把固定决策函数换成真实模型接口,便于定位问题。
官方延伸资料需要联网;阅读教材和运行本轮代码不需要联网。以下是概念核对入口,不要求你现在背诵接口字段:
本教材的工具调用字典是为教学自定义的简化格式,不能直接当作任何厂商的 API 请求格式使用。
高星 GitHub 项目:v1.1 的参考记录
星数只反映关注量,不能代替对代码、解释和前置知识的检查。v1.1 制作时先读取项目原始内容与许可证,再选择适合第一阶段的部分。以下星数是 2026-10-20 查询 GitHub API 的快照,后续会变化。
项目与星数快照 参考范围 本教材中的具体落点
Python-100-Days · 187,325基础语法到应用练习的课程组织 保留先语法、再场景、再独立作答的顺序;增加逐步分析和边界输入
AI Agents for Beginners · 77,523工具、参数、执行结果和状态之间的关系 第 09 / 13 / 16 章:工具返回字典、数据校验、决策与执行分开
LLMs-from-scratch · 106,931第 2 章的文本数据与取样衔接 第 03 / 15 / 16 章:字符、词、token、流片段和批次之间的区别
smolagents · 29,947Agent 运行循环的停止条件 第 06 / 16 章:完成状态、步数上限、未完成时的明确返回
v1.1 采用概念与教学组织参考,示例、题目、数据和解析重新编写,未搬入这些项目的框架源码、书籍或图片。Python-100-Days 在该次检查中未发现仓库级许可证,因此仅参考公开目录结构并链接原作;微软项目为 MIT,smolagents 为 Apache-2.0;LLMs-from-scratch 的 LICENSE.txt 含书籍和相关图片的排除条款,不能笼统当作所有材料都可复制。
学习包中的 开源参考与改编说明.md 记录作者、精确提交版本、对应文件及许可证链接,方便将来逐项核对。阅读原项目需要联网;运行当前教材不依赖它们。
我的错题与理解 写下“我原来以为……,运行后发现……,因为……”。笔记随导出进度保存;代码请保存在 .py 文件。
← 上一章 回到学习入口 ↑ Leon · Python 零基础实操教程 · v1.4 · 离线学习包 代码在本机 Python 中运行。基础课程无需第三方包。