←返回大模型学习资料
学习资料第 1 章 · Python 基础

CHAPTER 01 · CODE FOUNDATION

Python
基础

Python 是进入大模型世界的一把通用钥匙:你会用它读取数据、调用模型、组织实验,也会用它把一个想法变成可以重复运行的程序。本章先从下载、安装和第一次运行讲起,再走到变量、控制流、类与对象、文件处理和一个完整的词频统计项目。每个主题小节都是一页独立阅读单元,读完后用底部的“上一节/下一节”继续。

难度:入门建议:先通读,再逐段运行代码:Python 3.11+小节:122 张独立学习卡片
TEXTBOOK MAP · PYTHON 3

本章学习地图:从环境到项目

按官方教程的“章—节—小节”思路重排,给零基础读者补上前置步骤。

说明:Python 官方教程本身面向“已经接触过编程、但刚开始学 Python”的读者;本页面在其分类基础上增加下载、安装、命令行和零基础解释,示例与图示均为本站重新编写。

使用方式:每读完一个小节,至少运行一个示例;每完成一个练习,把答案写进自己的笔记。代码块右上角可以复制,复制后请在自己的环境中运行。

01 · SETUP & FIRST RUN

从下载 Python 开始:安装、验证与第一次运行

如果你还没有安装过 Python,不要急着跳到变量和循环。能把解释器下载到电脑、知道它安装在哪里、在终端里找到它、创建一个独立环境,并亲手运行第一段脚本,是后续所有课程的地基。本节不假设你已经会用命令行,而是把“下载什么、点哪里、输入什么、看到什么、出错怎么办”逐步说清楚。

版本建议:本章示例以 Python 3.11 及以上为基准。学习时优先选择 Python 官方下载页 ↗ 和官方文档,不要从不明网盘或打包软件中获取解释器。小版本号有差异很正常,关键是使用 Python 3,并在终端确认实际版本。

1.1.1 先认识教程里的输入、输出和提示符

在开始安装之前,先学会读懂教程中的三种文字。带有 >>> 的行,表示你在交互式解释器中输入的内容;紧接着、没有提示符的行,通常是 Python 返回的结果;以 # 开头的内容是注释,只给人阅读,不会被解释器执行。把这三类内容混在一起复制,是零基础最常见的第一个坑:你可能把输出再输入一次,也可能把 >>> 当作代码的一部分。

阅读代码块的固定顺序

先找输入,再找输出,最后找注释和解释。脚本代码块一般不带提示符,复制时可以整段运行;交互式代码块则要逐行输入,看到 ... 说明上一行开启了一个还没有结束的代码块,例如函数、循环或多行字符串。等你看到新的 >>>,才表示这一段已经执行完。

教程里的输出是“运行结果的样子”,不是需要再次输入的答案。若输出和示例不同,先检查 Python 版本、输入值和当前目录,再判断代码是否真的有问题。学习编程不是追求每台电脑显示完全相同的空格,而是能解释差异来自哪里。

PYTHON REPL · 输入、输出与注释
>>> 2 + 3
5
>>> # 这一行是注释,不会产生输出
>>> name = "Python"
>>> print(name)
Python
怎样读:第一行是输入,第二行是返回值;注释没有输出;赋值语句本身通常不显示结果,调用 print() 才把字符串写到屏幕。

如果你看到示例中的 ...,不要把它理解成省略号。它是交互式解释器的续行提示符,表示 Python 正在等待你补完缩进代码。网页教程为了排版,可能把输入和输出放在同一个框中;在自己的 .py 文件里,你只保留真正的 Python 代码和注释,不保留提示符或示例输出。

STARTER 00把一段教程还原成脚本

把上面的交互式示例改写成一个名为 read_demo.py 的脚本:删除所有 >>>,保留注释,增加一行 print(2 + 3),然后在终端运行。记录脚本与 REPL 的区别:哪一部分需要手动输入,哪一部分可以重复执行,输出来自哪条语句。

脚本中应保留 name = "Python"、print(name) 和注释,不要把 5 或 Python 作为额外代码行。脚本每次启动都会从头执行;REPL 则会保留当前会话已经创建的名称。

1.1.2 先弄清楚:你下载的到底是什么

Python 不是一个只能双击打开的应用窗口,而是一套由解释器、标准库、命令行工具和文档组成的运行环境。你写的 .py 文件只是普通文本,真正让它产生结果的是解释器。安装 Python 的动作,本质上是在电脑上放置一个可以读取这些文本、创建对象并执行语句的程序,同时把常用的标准库和包管理工具准备好。

初学者经常把 Python、编辑器和第三方库混为一谈。Python 是语言及运行时;VS Code、PyCharm、IDLE 是编辑代码的工具;NumPy、Pandas、PyTorch 是后来按项目安装的库。编辑器可以换,Python 解释器不能缺;库可以按需安装,不需要在第一天把大模型相关的所有包一次装完。把三者分开理解,安装时就不会被“我该点哪个下载按钮”吓住。

在浏览器中搜索时,优先确认域名是 python.org,页面标题通常会写 Python 的正式版本和发布说明。不要只看搜索结果里的“高速下载”或“破解版”字样。安装程序本身会影响系统权限、文件关联和命令行路径,来源不明的安装包可能捆绑广告、篡改环境变量,甚至埋入恶意程序。学习编程的第一条工程习惯,就是先确认软件来源。

选择版本时,通常选 Python 3 的最新稳定版本即可。不要为了追逐最前沿的小版本而反复卸载重装,也不要因为某篇旧教程写着 3.8 就强行寻找旧安装包。绝大多数基础语法在不同 3.x 版本中保持一致;如果某个库明确要求特定版本,再用虚拟环境隔离它。你可以把“版本号、下载日期、安装方式”记录在项目的 README 中,之后遇到差异时有依据可查。

1.1.3 Windows:从官方安装器到 PATH

在 Windows 上,进入 Python 官方下载页后选择 Windows installer。多数普通电脑选择 64 位安装器;只有非常老的系统或明确的兼容性要求,才需要考虑其他架构。下载完成后先查看文件名和数字签名信息,再双击运行。安装向导最关键的一步不是选择主题颜色,而是确认是否勾选Add python.exe to PATH。它会让终端在常见路径中找到 Python。

如果你忘记勾选 PATH,安装仍然可能成功,只是输入 python 时系统找不到命令。此时可以重新运行安装器选择 Modify,也可以使用 Windows 的“应用执行别名”或安装器提供的修复选项。初学阶段不要随意手动修改一长串环境变量;先记住当前问题是“程序已经存在,但命令搜索路径没有包含它”,排错会清楚很多。

Windows 还常见 py 这个 Python Launcher。它可以帮助系统选择已安装的 Python 版本,例如 py -3 启动 Python 3,py -3.11 请求特定版本。不同电脑上 python 和 py 的可用情况可能不同,这不是代码差异。后续示例默认写 python,如果命令不存在,就先试 py,并把你的实际命令记下来。

POWERSHELL · 检查 Windows 安装
python --version
py --version
python -c "print('Python 已经可以运行')"
where.exe python
where.exe py

python --version 或 py --version 应输出一个以 3. 开头的版本号。where.exe 会列出命令对应的路径;如果出现多个路径,说明电脑可能安装过多个 Python。多版本并不一定是坏事,但你必须知道项目实际使用的是哪一个解释器。最稳妥的做法是先选定一个版本创建虚拟环境,以后通过虚拟环境运行代码。

如果输入 python 后跳转到 Microsoft Store,常见原因是 Windows 的应用执行别名拦截了命令。可以在“设置 → 应用 → 高级应用设置 → 应用执行别名”中关闭对应的 Python 别名,然后重新打开终端验证。修改后一定要关闭旧终端窗口并重新打开,因为环境变量通常在终端启动时读取,旧窗口不会自动刷新。

1.1.4 macOS:安装器、Homebrew 与命令名

macOS 系统可能自带一些系统组件,但这不等于你拥有一个适合学习的独立 Python 环境。不要删除系统目录里的 Python,也不要把系统解释器当作自己的项目环境。可以从 Python 官方网站下载 macOS installer;如果你已经使用 Homebrew,也可以通过它安装一个由包管理器维护的 Python。两种方式都可以,关键是安装后用终端确认路径和版本。

TERMINAL · 检查 macOS 安装
python3 --version
which python3
python3 -c "print('Python 已经可以运行')"
# 如果使用 Homebrew:
brew --version
brew list python

macOS 上常用命令名是 python3,因为系统或其他工具可能仍保留一个名为 python 的命令。后续建立虚拟环境时,把 python 替换为 python3 即可。不要为了让教程中的命令看起来完全一样而建立软链接;先尊重系统现有约定,等理解 PATH 和解释器选择后再做定制。

第一次从网上下载的脚本或安装器可能触发 macOS 的安全提示。确认来源确实是官方页面后,再在系统设置中允许打开;不要为了绕过提示而关闭整个系统的安全保护。编程学习会经常接触来自社区的代码,建立“看来源、读说明、在隔离环境测试”的习惯,比任何一条快捷命令都重要。

1.1.5 Linux:发行版包管理器与系统 Python

Linux 发行版通常通过包管理器提供 Python。Ubuntu、Debian 常见 apt,Fedora 常见 dnf,Arch Linux 使用 pacman。命令会因发行版和权限设置不同而变化,学习时以自己系统的官方文档为准。不要直接删除系统 Python:很多系统工具依赖它,误删可能导致包管理、桌面组件或服务无法工作。

TERMINAL · Linux 只做检查
python3 --version
command -v python3
python3 -m pip --version
python3 -m venv --help

如果系统没有 venv 模块,终端可能提示需要额外安装发行版的虚拟环境包。请按照发行版文档安装,不要把一条来自论坛的命令原样复制到生产机器。你只需要先让 python3、pip 和 venv 三项检查有清楚结果,之后再继续本章。

表 1 · 不同系统的常用 Python 检查命令
任务Windows PowerShellmacOS / Linux检查重点
查看版本python --version
py --version
python3 --version确认是 Python 3,并记录实际命令名
查看解释器路径where.exe python
where.exe py
command -v python3确认路径是否指向预期安装或 .venv
创建虚拟环境python -m venv .venvpython3 -m venv .venv在当前项目目录生成独立环境
激活环境.venv\Scripts\Activate.ps1source .venv/bin/activate提示符通常出现 (.venv)
退出环境deactivate只退出当前终端的虚拟环境,不会删除它

1.1.6 选择编辑器:先降低反馈成本

Python 自带 IDLE,适合快速试验;VS Code 需要安装 Python 扩展,优点是项目管理、终端、搜索和调试功能比较完整;PyCharm 提供更集成的开发体验。三者没有“唯一正确答案”。选择标准可以很朴素:能否看到当前解释器、能否运行当前文件、能否在出错时跳到行号、能否搜索变量、能否打开内置终端。如果这些问题的答案都为“可以”,工具就已经够用。

编辑器里最值得先设置的是保存编码、缩进和解释器选择。文件编码建议使用 UTF-8,缩进使用 4 个空格,自动换行可以打开但不要让它改变代码。编辑器右下角或状态栏通常会显示当前 Python 解释器;当运行结果和终端不同,第一件事就是确认编辑器没有偷偷使用另一个解释器。不要在还没理解运行方式之前安装大量插件,插件越多,出问题时变量越多。

建议在硬盘上建立一个专门的学习目录,例如 llm-learning。目录名尽量避免空格、表情符号和过深的嵌套,尤其是在刚开始接触命令行时。可以先创建以下结构:01-python-basics/ 放本章代码,data/ 放样例输入,outputs/ 放结果,notes/ 放笔记。目录结构不影响 Python 语法,却会直接影响你是否能找到文件和复现实验。

1.1.7 第一个脚本:从文本文件到终端输出

新建文件 hello_python.py,不要保存成 hello_python.py.txt。Windows 默认可能隐藏已知文件扩展名,建议在文件资源管理器的“查看”设置中显示扩展名。文件名最好只使用英文字母、数字、下划线和短横线;中文文件名并非一定错误,但在跨平台和命令行传参时更容易遇到编码或转义问题。

PYTHON · hello_python.py
from platform import python_version

print("你好,Python")
print(f"当前版本:{python_version()}")
print("下一步:把输入交给程序处理。")

在终端切换到文件所在目录后运行。Windows PowerShell 使用 cd 进入目录,macOS 和 Linux 也使用同一个命令;路径中有空格时,用引号把整个路径包起来。第一次运行时不要只点击编辑器里的三角按钮,最好同时在终端运行一次,这样你能看到当前工作目录、命令名和完整输出,日后排错更有把握。

TERMINAL · 运行脚本
cd path/to/llm-learning/01-python-basics
python hello_python.py
# macOS / Linux 也可能写成:
python3 hello_python.py

终端中只出现一次提示符而没有输出,可能表示你运行的不是刚保存的文件;输出旧内容,可能表示编辑器没有保存;提示“找不到文件”,通常表示当前目录不对。把命令拆成两步检查:先用 dir(Windows)或 ls(macOS/Linux)确认文件在当前目录,再执行 Python 命令。问题越具体,解决越快。

1.1.8 交互式解释器:用最小实验确认直觉

输入 python 或 python3 后,如果看到 >>>,就进入了交互式解释器。它适合验证一个表达式、查看类型、试用标准库。输入 2 + 3 会立即得到 5;输入 print("hi") 会看到文字。退出可以输入 exit(),Windows 也可以按 Ctrl+Z 后回车,macOS/Linux 通常按 Ctrl+D。

PYTHON REPL · 最小实验
>>> 2 + 3
5
>>> type("3")
<class 'str'>
>>> len(["文本", "代码"])
2
>>> exit()

交互式环境中看到的 >>> 不是 Python 代码的一部分,不要把提示符一起复制到 .py 文件。教程里的输出也不是需要再次输入的内容。一个简单的区分方法是:带提示符的行是你输入的,没有提示符、紧接在下面的行是解释器返回的结果。这个细节经常决定复制的代码能否运行。

1.1.9 pip 和虚拟环境:把项目放进自己的小房间

pip 是 Python 的包安装工具,但不等于 Python 本身。安装第三方库时,优先使用 python -m pip,因为这会明确告诉系统“用当前这个 Python 解释器调用 pip”。如果电脑上有多个 Python,直接输入 pip 可能连接到另一个版本,导致你明明安装成功,运行程序时却仍提示模块不存在。

TERMINAL · 创建虚拟环境
mkdir llm-learning
cd llm-learning
python -m venv .venv

# Windows PowerShell
.venv\Scripts\Activate.ps1

# macOS / Linux
source .venv/bin/activate

python -m pip install --upgrade pip
python -c "import sys; print(sys.executable)"
python -m pip --version

激活成功后,终端提示符前面通常会出现 (.venv)。此时安装的包默认只放在当前项目的虚拟环境里,不会污染系统环境。完成练习后可以输入 deactivate 离开。虚拟环境目录通常不需要提交到 Git 仓库,只要记录依赖清单或安装命令,别人就能重新创建。

PowerShell 如果提示脚本执行策略限制,不要直接复制不明来源的“永久解除限制”命令。先确认当前终端类型,再阅读 Microsoft 和 Python 官方说明;也可以暂时使用命令提示符激活,或者直接调用虚拟环境里的 Python 可执行文件。安全策略是系统在保护你,不是 Python 安装失败的证据。

验证虚拟环境时,最有价值的是查看解释器的绝对路径。路径中出现项目目录下的 .venv,说明后面的运行和安装会落在这个环境。若路径仍然指向系统目录,说明激活没有生效;此时不要继续安装库,先把解释器选择问题解决。大模型项目里依赖数量多,越早保持环境清楚,越少遇到“在我电脑上能运行”的困扰。

1.1.10 用一个小图理解“安装—运行—反馈”

图 1 · Python 学习环境的四个层次本站自制流程图,使用 HTML/CSS 绘制。
官方来源下载
↓
Python 解释器
↓
编辑器保存 .py 文件
终端选择目录与命令
↓
运行结果或 traceback

编辑器负责让你写得舒服,终端负责让运行过程透明,解释器负责真正执行。三者分工清楚,出错时就知道该检查哪一层。

1.1.11 常见安装问题的排查顺序

遇到问题时,不要从网上搜一大串命令逐条尝试。第一步问“命令是否存在”:运行 python --version 或 python3 --version。第二步问“命令指向谁”:用 where.exe python 或 which python3 查看路径。第三步问“我运行的文件是否存在”:用 dir 或 ls 查看当前目录。第四步才检查代码本身。这个顺序把系统问题、路径问题和语法问题分开,不会把不同层次的故障混在一起。

“不是内部或外部命令”通常表示 PATH 没有包含解释器;“No module named xxx”通常表示库安装到了另一个解释器,或者当前虚拟环境没有激活;“Permission denied”通常涉及文件权限或安全策略;“SyntaxError”则说明解释器已经找到并开始读取代码。先辨认错误类型,再选择解决方案,比记住某条万能命令可靠得多。

如果程序运行后立即闪退,尽量从终端启动,不要只双击文件。双击会让窗口在执行结束后关闭,错误信息来不及阅读。终端启动会保留完整 traceback,你可以复制最后几行到笔记中。即使暂时看不懂,也先记录文件名、行号、异常类型和最后一条消息,这四项是后续搜索和提问最有用的信息。

不要把 API 密钥、网盘下载链接中的私人参数、浏览器 Cookie 或个人文件路径写进练习代码。Python 能读写电脑上的文件,也能发送网络请求,因此“能运行”不等于“可以放心运行”。第一次接触第三方包或远程脚本时,先看项目主页、许可证和安装说明;对需要管理员权限、关闭安全软件或执行远程脚本的教程保持警惕。

表 2 · 安装阶段的错误排查表
看到的现象最可能的层次先做的检查不要急着做什么
python 不是命令PATH 或命令名Windows 试 py --version;macOS / Linux 试 python3 --version,再查看路径不要马上重装多个版本
跳转到 Microsoft StoreWindows 应用执行别名关闭 Python 别名,重新打开终端后再验证不要把 Store 里的占位入口当成解释器
No module named ...解释器或虚拟环境不一致运行 python -c "import sys; print(sys.executable)" 和 python -m pip --version不要直接重复执行 pip install
找不到文件当前工作目录或路径用 dir / ls 列出当前目录,并打印 Path.cwd()不要把别人的绝对路径硬贴进代码
双击后窗口立即消失启动方式从终端运行脚本,保留完整 traceback 和行号不要用暂停窗口掩盖真正错误

1.1.12 环境验收清单:通过后再进入语法

CHECKPOINT 01安装与第一次运行

请在自己的电脑上完成以下任务,并把终端截图或文字结果保存到笔记:

  1. 确认 Python 3 的版本号,并记录使用的命令是 python、python3 还是 py。
  2. 确认解释器绝对路径,说明它是否来自项目的 .venv。
  3. 创建并运行 hello_python.py,让它输出版本和一句学习目标。
  4. 进入交互式解释器,完成 2 + 3、type("3") 和 len([1, 2, 3]) 三个实验。
  5. 故意在脚本中少写一个右括号,记录 traceback 的最后一行,再恢复代码。
你应当能看到 Python 3.x 版本、一个明确的解释器路径、脚本的三行输出,以及一个指向具体行号的 SyntaxError。如果命令找不到,先区分 Windows 的 python/py 与 macOS/Linux 的 python3;如果模块找不到,先检查虚拟环境是否激活。

完成清单后,你已经具备了继续学习的最低环境:知道代码文件在哪里,知道哪个解释器在运行,知道如何在终端查看版本,知道如何建立隔离环境,也知道如何保留一条错误信息。接下来学习变量和控制流时,每个例子都可以放进这个目录,运行结果也会有明确的来处。不要把安装看成课程前的杂事,它本身就是第一次工程实践。

如果你暂时没有电脑,也可以先使用浏览器中的在线 Python 环境完成概念练习,但要把它当作临时实验台,而不是长期项目环境。在线环境可能限制文件读写、第三方库和版本,网络断开时也无法继续。等条件允许,仍建议在本机完成一次安装和虚拟环境配置,因为后面的大模型开发需要接触真实目录、凭据管理和命令行工具。

本节的目标不是让你背下所有安装命令,而是建立一种可迁移的排错框架:确认来源,确认版本,确认路径,确认解释器,确认文件,再运行最小例子。今后安装 NumPy、PyTorch、模型 SDK 或命令行工具时,你仍然可以使用同一套框架。工具会变化,检查顺序不会变化。

还有一个容易忽略的细节是“终端窗口继承的环境”。你在一个窗口里激活了 .venv,再打开另一个窗口,第二个窗口通常不会自动激活;编辑器新建的终端也可能从系统 Python 开始。看到命令前没有 (.venv) 时,不要凭印象猜测,重新运行 python -c "import sys; print(sys.executable)"。把路径当作事实依据,是比看界面图标更可靠的检查方式。

安装器和虚拟环境都可能占用几十到几百兆空间,但磁盘空间不是选择版本的主要标准。真正需要关注的是兼容性、更新维护和你能否复现。以后安装大模型框架时,某些包还会下载更大的模型权重或编译工具;提前把代码、环境和模型文件分开存放,可以避免系统盘突然被占满,也便于清理实验结果而不误删源代码。

你可以给自己的安装记录写一个小表格:操作系统、Python 版本、解释器路径、编辑器、虚拟环境目录、最后一次验证日期。每次升级 Python 或切换电脑时复制这张表,再逐项检查。这个动作看似行政化,却能把“环境记忆”从脑子里移到可检索的文本中;当你向别人提问时,也能一次提供足够背景,减少来回沟通。

如果你要把项目交给同学运行,除了代码,还要提供一个最小的运行说明:进入哪个目录、如何创建虚拟环境、如何激活、需要安装哪些包、用什么命令启动、预期看到什么输出。说明中不要写“按平时的方法运行”,因为每个人的终端和编辑器都可能不同。能让另一个初学者照着文字完成一次运行,才算真正完成了环境配置。

最后提醒一个学习节奏问题:安装成功不代表今天必须继续学到很晚。先把版本、路径和第一段输出保存下来,休息后再回来看变量、字符串和函数,记忆会更稳定。如果安装环节卡住,可以把错误原文、操作系统和执行过的命令记录下来;不要用猜测替代证据,也不要因为一个 PATH 问题否定自己学习编程的能力。

把这次安装当作一次小型实验来记录:先提出“终端能找到哪个解释器”的问题,执行命令收集证据,再根据结果决定下一步。你不需要一次记住所有系统设置,只要能复述自己的检查过程,就已经从“照着教程点选”迈向“理解开发环境”。

1.1.13 新手最容易忽略的两个检查

第一,确认文件扩展名真的正确;第二,确认编辑器和终端使用的是同一个虚拟环境。很多“代码没有变化”和“刚安装的库不存在”都不是语法问题,而是文件名或解释器不一致。把这两个检查写在自己的桌面便签上,直到你能熟练用命令验证为止。

完成安装后不要急着清理下载记录。至少保留版本号、解释器路径、虚拟环境创建命令和第一次运行的输出;以后升级或换电脑时,这四项会成为可靠的对照。能复述“我从哪里下载、安装到了哪里、用哪个命令运行、结果如何”,才算真正完成本节。

重新安装前,先完成三步诊断

遇到 python 找不到、版本和预期不同或包安装后无法导入时,重新安装往往不是第一选择。先确认命令名,再确认解释器,再确认包管理器属于同一个解释器。下面的命令不会改动项目,只会把三条证据打印出来;把结果复制到学习记录中,之后才能判断问题是在 PATH、虚拟环境还是依赖安装。

TERMINAL · 安装问题的最小诊断
python --version
python -c "import sys; print(sys.executable)"
python -m pip --version

# macOS / Linux 如果命令名不同
python3 --version
python3 -m pip --version

三条输出中的 Python 路径应该指向同一套环境。若 python -m pip 显示的安装位置和 sys.executable 毫无关系,说明你可能把包装到了另一个解释器。此时先激活正确的虚拟环境,再用 python -m pip install 包名,不要单独使用来源不明的 pip 命令。诊断清楚后再决定是否升级或重装,能够避免重复下载和环境越修越乱。

02 · ORIENTATION

先建立一个正确的运行直觉

学习 Python 的第一道门槛,往往不是语法,而是不知道“代码到底在哪里发生”。你在编辑器里写下的是源代码;Python 解释器负责读取这些文字、执行其中的语句,并把结果交给屏幕、文件或另一个程序。把这条链路想清楚,遇到错误时就不会只盯着红色提示,而会去问:错误发生在读取、计算、数据组织,还是输出的哪一步?

大模型学习尤其需要这种运行直觉。以后你会写数据预处理脚本、训练循环、评估程序和 API 调用,它们看起来复杂,但底层仍然是“输入 → 处理 → 输出”的组合。Python 负责把这些步骤组织起来;模型只是其中一个比较特殊的处理环节。先把普通 Python 学会,后面才不会把所有问题都误认为是“模型太难”。

把程序看成一条流水线,还有一个好处:你会自然地给中间结果命名。读取文档后得到 raw_text,清洗后得到 clean_text,切分后得到 tokens,统计后得到 counts。如果每一步都覆盖同一个变量名,短代码看起来很省事,调试时却无法知道哪一步已经改变了数据。命名多几个字符,通常能节省很多排错时间。

解释器也不是一个“理解意图”的老师。它只按照语言规则执行:先解析符号和缩进,再求值,再调用对象提供的操作。如果你想让两个文本相加,Python 会按字符串拼接;如果你想让文本和整数直接相加,它会报告类型错误。报错并不说明 Python 在故意为难你,而是在告诉你:当前操作需要的对象条件没有满足。

初学时建议同时使用两种视角。第一种是人类视角:这段程序要完成什么任务?第二种是解释器视角:这一行创建了什么对象,下一行读取了什么值,哪个名称在当前作用域里可见?当两种视角的答案不一致,bug 往往就藏在那一行。后面学习函数、容器和文件时,始终保留这个习惯。

项目目录也属于程序的一部分。一个朴素但清楚的结构可以是:src/ 放源代码,data/ 放可公开的样例数据,outputs/ 放运行结果,notes/ 放学习记录,README.md 说明如何运行。不要把下载的原始资料、密钥文件和自动生成的大文件无差别地提交到公开仓库;这既是工程习惯,也是保护个人信息的基本措施。

运行一个脚本时,还要区分“当前工作目录”和“脚本所在目录”。终端提示符所在的位置决定了相对路径如何解释,编辑器的运行按钮则可能替你设置另一个工作目录。遇到 FileNotFoundError,不要先怀疑文件消失了,先打印 Path.cwd(),确认程序到底从哪里开始寻找。这个小习惯会在读取训练数据、保存模型和加载配置时反复帮到你。

解释器报错时,阅读 traceback 可以遵循固定顺序:先看最后一行的异常类型和消息,再看最靠近自己代码的文件名与行号,最后回到那一行的输入状态。最后一行通常回答“发生了什么”,上面的调用栈回答“它是怎样走到这里的”。初学者常从第一行开始逐字阅读,结果被大量内部信息吓住;先抓住末尾结论更有效。

学习代码时不要只复制粘贴。复制后至少做一次微小改动:换一个变量名、增加一条打印、把输入换成边界值,观察输出如何变化。你需要建立的是“代码—状态—结果”的对应关系,而不是收藏一段看起来熟悉的文本。每一次有意识的改动,都是一次低成本实验。

最后给环境建立一个可重复的起点:记下 Python 版本,保存依赖安装命令,给每个示例准备一个最小输入。这样当代码在另一台电脑上运行不一致时,你有足够信息比较环境差异。可复现不是大型研究项目才需要的标准,学习阶段越早养成,后面越不容易把时间浪费在“我这里为什么不一样”上。

图 1 · 一段 Python 程序是怎样产生结果的本站自制示意图,使用 HTML 内嵌 SVG 绘制。
source.py你写下的指令Python 解释器读取 · 解析 · 执行遇到错误就停下并报告状态与输出变量 · 文件 · 屏幕 · 模型调用

程序不是“凭空变聪明”,而是在每一步改变数据状态。调试就是找出哪一步的状态与预期不一样。

1.2.1 解释器、脚本和交互式环境

打开终端输入 python,通常会进入交互式解释器。你输入一行,它立即返回结果,适合试验表达式、查看类型和验证一个小想法。把代码保存为 .py 文件再运行,则叫脚本方式,更适合长期保存和重复执行。两者不是两种语言,而是同一个解释器的两种使用方式。

TERMINAL · 第一次运行
python --version
python
>>> 2 + 3
5
>>> print("你好,Python")
你好,Python
>>> exit()

不同系统可能使用 python3 作为命令名;Windows 上也可能通过 py 启动。命令名不同不代表代码不同。以后如果教程中的命令和你的电脑不一样,先确认“它想启动的是哪个 Python 解释器”,再替换命令名。

1.2.2 为每个项目准备独立环境

随着学习深入,你会安装不同版本的库。把所有库混在系统 Python 里,项目之间容易互相影响。虚拟环境可以理解为项目自己的“小房间”:在里面安装的包默认只服务当前项目。它不会改变 Python 语言本身,却能让实验更容易复现。

TERMINAL · 建立项目环境
mkdir llm-learning
cd llm-learning
python -m venv .venv

# Windows PowerShell
.venv\Scripts\Activate.ps1

# macOS / Linux
source .venv/bin/activate

python -m pip install --upgrade pip

看到命令行前面出现 (.venv),通常说明环境已经激活。离开环境可以输入 deactivate。建立清晰的目录也很重要:源代码、输入数据、输出结果和笔记各放在可理解的位置,不要把所有东西都塞进一个文件夹。

如果激活命令被系统策略阻止,不要为了“赶紧跑起来”去复制一条不明来源的解决命令。先确认你使用的是 PowerShell、命令提示符还是类 Unix 终端,再查 Python 官方文档或本机的安全策略。学习环境遇到权限问题很常见,能说清“系统拒绝了什么操作”比盲目重复命令更重要。

编辑器方面,选择能显示缩进、括号匹配和语法高亮的工具即可。不要在学习初期花很多时间追求复杂插件。一个好的判断标准是:你能否方便地运行当前文件、看到完整 traceback、搜索变量名,并在保存后重新运行。工具的作用是降低反馈成本,而不是替代理解。

编码风格从第一天就值得练习。函数和变量使用描述性的小写下划线命名;常量可以使用全大写;顶层代码不要堆成一堵墙;一个函数如果需要滚动很久才能看完,就值得重新拆分。PEP 8 不是“必须逐字符服从的考试答案”,而是一套让团队成员更容易预测代码形状的共同约定。

安全提醒:不要直接运行来源不明的脚本,也不要把 API 密钥、账号密码或个人文件写进公开代码。学习阶段养成“先读代码,再运行;先用假数据,再接真实数据”的习惯。
CHECKPOINT 02环境与运行练习

完成下面三件事,再继续阅读:

  1. 创建一个虚拟环境,并在其中运行一段输出个人学习目标的脚本。
  2. 分别用交互式解释器和 .py 文件执行 2 ** 10,比较两种方式的体验。
  3. 故意把 print 写错一次,观察错误信息中出现了哪一类词。
交互式解释器适合快速试验,脚本适合保存和复用。拼写错误通常会得到 NameError;不要只记住错误名称,要同时看报错指向的行号和具体名称。

1.2.3 从“能运行”到“能解释”:建立执行记录

当你第一次运行一段代码时,可以在纸上或笔记里画四列:行号、当前名称、对象内容、屏幕输出。比如执行 name = "小林" 后,名称列写 name,对象列写一个字符串;执行 print(name) 后,输出列才出现文字。这个记录方法很慢,但它能把“变量会自己变化”的模糊感觉,变成一组可以逐行检查的事实。熟练后不必每次都画表,但遇到陌生代码或复杂 bug 时仍然值得回到这张表。

解释器读取程序时并不是把整篇文件当成自然语言文章理解。它先检查缩进、括号和关键字是否符合语法,再执行能够执行的表达式。语法错误往往发生在真正运行之前;名称错误、类型错误和索引错误则通常发生在运行过程中。区分这两类问题很重要:前者先检查符号和缩进,后者要观察变量的值、类型和边界。

一条语句可能同时完成多个动作,例如 result = clean(text).lower() 既调用函数,又调用字符串方法,最后再把结果绑定到名称。初学时建议把它拆成几行:先得到清洗结果,再统一大小写,再打印类型。拆开不是因为 Python 不能写得紧凑,而是为了让每一个中间状态都可见。等逻辑稳定后再合并,通常比一开始写成“聪明的一行”更省时间。

函数调用还会形成一层层的调用栈。main() 调用 read_notes(),后者又调用 Path.read_text();最内层出错时,解释器会把经过的路径列出来。阅读 traceback 时,先找到最接近自己文件的那一帧,再回看传入的参数。不要被标准库内部的几十行细节带偏;你的任务是先确认“我把什么交给了哪个函数”。

输出结果也分为“给人看的展示”和“给程序继续处理的数据”。print() 会把值转换成文字送到终端,终端关闭后它就不再是结构化数据。如果下一步还要排序、统计或保存,应该让函数返回列表、字典或对象,再由最后一层负责展示。这个习惯能让同一个处理函数同时服务命令行、Notebook 和 Web 接口。

日志和临时打印有不同的用途。学习时用 print(f"debug: {value=}") 快速查看状态很方便;长期运行的程序则应该记录时间、级别和上下文,并避免把密码、密钥或整份隐私文本写进日志。无论用哪种方式,都要在确认问题后删除无关输出,保持最终结果简洁。过多的打印会让真正重要的错误被淹没。

当前工作目录是一个经常被忽视的“隐藏输入”。同样的 Path("data.txt"),从项目根目录运行和从脚本子目录运行,可能指向两个不同位置。你可以用 Path.cwd() 把它显示出来,并在 README 中写明推荐的启动目录。需要让代码不依赖启动位置时,再学习以 __file__ 为基准构造路径;不要在还没理解相对路径时到处复制绝对路径。

编码同样属于运行环境。Python 3 的源代码通常默认以 UTF-8 解释,但编辑器可能把文件保存为其他编码,终端也可能使用不同字体或代码页。出现中文乱码时,分别检查“文件实际编码”“读取时指定的编码”“终端显示能力”三个环节。不要因为终端显示方框就断定字符串已经损坏,先把它写入 UTF-8 文件,再用另一个工具打开比较。

可重复运行需要清理上一次的状态。交互式解释器会保留已经定义的变量,脚本每次启动则从全新的进程开始。你在 REPL 中先定义了 answer = 42,后来删除了定义但没有重启解释器,旧名称仍可能存在。遇到“这段代码单独运行却失败”,先关闭并重新打开交互式环境,再按脚本中的顺序执行,排除隐藏状态。

一段程序的边界可以通过输入和输出契约来描述。输入契约说明类型、范围、是否允许空值;输出契约说明返回类型、排序规则和失败方式。哪怕只是一个学习函数,也可以在文档字符串中写一句:“接收非空文本,返回小写词列表;空文本返回空列表”。契约越清楚,后面的测试和重构越容易。

版本控制的价值不只是备份文件。每完成一个可运行的小步骤,就可以提交一次清晰的变更,写下“加入读取示例”“修复相对路径”“处理空输入”。以后发现新改动破坏了结果,你能比较前后差异,而不是从一份巨大文件中猜测哪里被改过。学习阶段不必追求复杂分支模型,但应该保留可回退的历史。

不要把“环境配置成功”误解成“代码一定正确”。解释器、依赖和路径只能保证程序有机会运行,不能保证需求被正确实现。把需求写成几个可观察的例子:输入空字符串应该输出什么,输入两个词应该统计几次,文件不存在应该提示什么。例子既是学习笔记,也是最小测试;它们比一句“应该能处理各种情况”更有约束力。

当程序变长时,先找重复的动作,再决定是否抽成函数。复制三遍的清洗逻辑,未来极易出现一处修改、两处遗漏;抽成函数后,输入和输出边界会变得明显。但也不要为了“看起来专业”把每一行都包装成函数。函数应该有可解释的职责,名称应该让读者猜到它会不会修改输入、是否会读文件、失败时如何表现。

学习大模型时,你会频繁处理“模型没有按预期返回”的情况。此时也用同一个运行直觉:请求参数是什么,原始响应是什么,解析步骤改变了什么,最终展示的文本是什么。不要只看最后一句答案就判断模型好坏;把中间状态保存下来,才能区分网络问题、格式问题、提示词问题和业务判断问题。Python 基础阶段练习的正是这种分层观察能力。

如果代码涉及外部服务,先用本地假数据完成流程,再接入真实 API。这样可以把“程序逻辑错误”和“网络、认证、额度错误”分开。API 密钥从环境变量或安全的本地配置读取,不要写进示例代码;返回内容也要设置长度和错误处理,避免一次调试把大量文本打印到终端。安全和可观察性应该从第一个脚本开始培养。

一个值得保留的学习记录格式是:今天的问题、最小复现代码、实际输出、原因、修复、下一次检查点。最小复现代码只保留能触发问题的几行,不要直接把整个项目发出去。你会发现,写复现步骤的过程本身就会迫使你弄清楚输入和状态;很多“神秘 bug”在缩小范围时就自然消失了。

最后,建立“先预测、再运行、后解释”的节奏。看到 items = [1, 2]; items.append(3); print(len(items)),先写下你认为会输出什么,再运行验证;如果错了,回到对象状态解释差异。预测不是考试,而是让大脑主动生成模型。随着练习积累,你会越来越早发现缩进、类型和边界问题,而不是等程序跑完才被动接受结果。

DEEP DIVE 02运行轨迹记录练习

写一个包含两个函数的小脚本:第一个函数接收文本并返回去掉首尾空格后的字符串,第二个函数接收这个结果并返回长度。先预测输入 " Python " 的每一步状态,再运行并把“原始值、清洗值、长度、类型”记录到笔记。随后把输入换成空字符串和中文句子,比较结果。

重点不是函数数量,而是能够说明每个函数的输入、输出和是否修改原对象。你应该看到清洗结果是新字符串,原始字符串仍保留空格;空字符串长度为 0,中文字符串的长度按 Unicode 字符计数。若结果不同,先检查文件编码和实际输入是否包含不可见空格。

1.2.4 建立自己的运行检查表

每次运行前可以快速问五个问题:我使用的是哪个解释器?当前工作目录在哪里?输入文件是否真的存在?程序应该输出什么?如果失败,我准备从 traceback 的哪一行开始看?这五个问题会把“点一下运行按钮”的动作变成有意识的实验。它们也适用于以后运行 Notebook、训练脚本和模型评估程序。

运行后再问五个问题:实际输出是否符合预期?中间数据的数量有没有突然变成 0?类型有没有在某一步改变?是否产生了不应该覆盖的文件?这次改动是否只改变了一个变量?如果把这些问题写在项目模板里,复习时就不会只凭记忆回想某段代码“当时为什么能跑”。

初学者容易把错误分成“我的问题”和“电脑的问题”,但更有用的分法是层次:来源和安装、解释器和依赖、路径和编码、语法和名称、数据和业务逻辑。每一层都有自己的证据和工具。看到错误先归类,再决定检查版本、打印路径、查看类型还是修改条件,排错会从情绪反应变成一套流程。

当你向同学或老师提问时,提供最小复现代码、操作系统、Python 版本、完整错误末尾和预期结果。不要只说“运行不了”或发一张被裁剪的截图。高质量的问题描述不仅更容易得到答案,也会反过来训练你识别输入、状态和输出。编程能力的一部分,就是把问题说到别人可以验证。

把本节的运行直觉迁移到大模型调用:源代码是请求构造,解释器执行网络库,输入是提示词和参数,状态是请求 ID、原始响应和解析对象,输出是最终答案或评估指标。模型表现不稳定时,先保存这些中间状态,再讨论提示词或模型版本。能看到完整链路,才有机会做可靠实验。

1.2.5 一分钟自测

在不运行的情况下,解释下面四件事的差别:脚本文件、解释器进程、当前工作目录和标准输出。脚本是保存的输入,解释器是执行者,工作目录影响相对路径,标准输出只是某种展示渠道。能把这四个概念说清楚,后面遇到路径、编码或导入错误时就有了排查地图。

再用自己的话解释“程序状态”:它包括当前变量指向的对象、容器中的元素、文件读到的位置以及函数返回的中间结果。调试并不是盯着最后一句输出猜原因,而是找出状态第一次偏离预期的地方。这个定义会贯穿整个 Python 和大模型学习过程。

把“运行环境”变成一张可核对的记录

只说“我在电脑上运行了”还不够,因为同一份代码可能由不同解释器、从不同目录启动。遇到结果不一致时,先把事实打印出来,再讨论代码逻辑。sys.executable 表示当前真正执行脚本的解释器,Path.cwd() 表示当前工作目录,__file__ 则指向当前源文件。三者不是同一个概念:虚拟环境可能改变解释器路径,终端启动位置可能改变工作目录,源文件仍然位于项目的另一个目录。

下面的小工具可以保存成 diagnose_runtime.py。它不修改文件,也不依赖第三方库,适合在“明明安装过却找不到模块”或“相对路径读不到文件”时先运行一次。记录输出时,把终端命令也一并写下;只保存结果而不保存启动方式,过几天仍然无法复现。

PYTHON · diagnose_runtime.py
from pathlib import Path
import sys

def runtime_snapshot():
    script = Path(__file__).resolve()
    return {
        "python": sys.executable,
        "version": sys.version.split()[0],
        "cwd": str(Path.cwd()),
        "script": str(script),
        "first_arg": sys.argv[1] if len(sys.argv) > 1 else "(没有脚本参数)",
    }

for name, value in runtime_snapshot().items():
    print(f"{name}: {value}")
可能的输出: python: D:\projects\llm-learning\.venv\Scripts\python.exe version: 3.12.4 cwd: D:\projects\llm-learning script: D:\projects\llm-learning\diagnose_runtime.py first_arg: notes.txt
表 2.2 · 运行记录中的四个事实
记录项回答的问题发现异常时先检查不要直接假设
sys.executable究竟是哪一个 Python 在执行?是否来自预期的虚拟环境终端提示符有 (.venv) 就一定正确
sys.version解释器版本是什么?是否满足项目的最低版本电脑装过新版本就代表当前命令使用新版本
Path.cwd()相对路径从哪里开始?输入文件是否位于这个目录相对路径总是相对于脚本文件
sys.argv脚本收到了什么参数?参数数量、顺序和空格命令行数字会自动变成整数

这张表还有一个排错价值:它把“配置问题”和“程序问题”分开。若解释器路径不对,先修复启动环境;若解释器和目录都正确,再检查变量、类型和条件。命令行参数中的路径有空格时,要在终端加引号;脚本内部不要把带引号的文字再次当成路径的一部分。做完诊断后,再回到最小示例,确认修复只改变了一个因素。

1.2.6 解释器参数、模块启动与脚本参数

官方教程把“唤出解释器”和“传入参数”分开介绍,是因为启动方式本身就是程序的一部分。python -c 适合执行一条很短的语句,python -m 模块名 适合按照模块方式启动工具,python -i script.py 则是在脚本运行结束后保留交互环境,方便检查变量。先用这些小命令做环境诊断,比为了验证一个版本专门新建文件更快。

命令行不是“黑盒按钮”,它会给脚本输入

用户在命令后面写下的文字会进入 sys.argv。第 0 项通常是脚本名称,后面的项才是用户参数;它们全部是字符串,数字仍需自行转换和验证。命令行参数的优点是同一份脚本可以处理不同文件,缺点是参数缺失、顺序错误和路径中包含空格都需要明确处理。学习阶段先读懂 sys.argv,再使用 argparse 生成更友好的帮助页面。

PYTHON · arguments.py
import sys

print("脚本名称:", sys.argv[0])
print("收到参数:", sys.argv[1:])

if len(sys.argv) > 1:
    filename = sys.argv[1]
    print(f"准备读取:{filename}")
else:
    print("请在命令后提供文件路径")
运行示例:python arguments.py notes.txt 会把 notes.txt 作为字符串交给脚本;参数中有空格时,应在终端用引号包住完整路径。
TERMINAL · 三种快速启动方式
python -c "print(2 ** 10)"
python -m pip --version
python -i arguments.py notes.txt

# macOS / Linux 常见命令名
python3 -c "import sys; print(sys.version)"
表 2.1 · 解释器启动命令的层次
命令形式谁接收后面的内容典型用途容易混淆的地方
python / python3解释器进入 REPL 或运行脚本不同系统的命令名可能不同
python -c "..."解释器选项执行一条短语句引号由终端处理,复杂代码不宜全塞进一行
python -m module解释器选项按模块方式启动工具模块名不是文件路径,不能随意写斜杠
python script.py args脚本与脚本参数重复运行同一程序args 全部先以字符串进入 sys.argv
python -i script.py解释器和脚本脚本结束后检查状态交互会话会保留脚本创建的名称

不要把解释器选项和脚本参数混为一谈。python -m pip 中的 -m 是解释器选项;notes.txt 是脚本收到的参数。遇到命令很长时,把它拆成“解释器、选项、脚本、脚本参数”四部分阅读。这个拆分方法也适用于以后启动训练脚本、评测脚本和 Web 服务。

1.2.7 源文件编码与交互式帮助

现代 Python 3 源文件默认采用 UTF-8,但“默认”不代表编辑器一定按 UTF-8 保存。中文变量、注释和字符串都可以正常使用,前提是文件实际编码、读取方式和终端显示方式彼此一致。建议在编辑器中明确选择 UTF-8,并在读取外部文本时写出 encoding="utf-8";遇到乱码时分别检查这三个环节,不要只改终端字体。

交互式解释器还可以帮助你主动探索对象。输入 help(str) 可以打开字符串类型的帮助,输入 dir(list) 可以查看对象拥有的名称,输入 list.append? 这类写法则取决于具体工具,不是所有终端都支持。先用官方文档确认概念,再用 help() 查看本机版本的细节;两者一个负责系统解释,一个负责就地查阅。

PYTHON REPL · 交互式探索
>>> text = "Python"
>>> type(text)
<class 'str'>
>>> len(text)
6
>>> "py" in text.lower()
True
>>> help(len)

退出帮助页面通常按 q,退出解释器可以调用 exit()。不要在帮助页面中继续输入 Python 代码;看到分页器时先退出,再回到 >>> 提示符。把“查帮助—做最小实验—记录结果”作为固定循环,你就不必把所有函数参数都提前背下来。

用一个小实验看见输入、状态和输出

当你觉得“代码只是从上往下读”,可以把同一份数据在几个关键节点打印出来。下面的例子故意保留 raw、cleaned 和 words 三个名称:它们分别代表原始输入、清洗后的状态和下一步要处理的列表。这样即使最后的结果不对,也能定位是清洗、切分还是输出格式出了问题。调试完成后,再删掉不必要的打印,避免把调试信息误当成程序的正式结果。

PYTHON · trace_steps.py
raw = "  Python, model  "
cleaned = raw.strip().replace(",", "")
words = cleaned.casefold().split()

print("raw    =", repr(raw), type(raw).__name__)
print("cleaned=", repr(cleaned), type(cleaned).__name__)
print("words  =", words, type(words).__name__)
print("count  =", len(words))
预期输出: raw = ' Python, model ' str cleaned= 'Python model' str words = ['python', 'model'] list count = 2

这段代码还展示了一个重要区别:replace() 和 casefold() 都返回新的字符串,raw 不会被改变;split() 则把字符串转换成列表。以后遇到“为什么这里不能调用列表方法”或“为什么原文本还带空格”,先打印对象的值和类型,不要只凭变量名猜测。对于包含密码、个人资料或模型密钥的程序,调试时应打印长度、字段名和是否为空,而不是把敏感内容完整写到终端或日志中。

03 · VALUES & I/O

变量、数据类型与输入输出

程序要处理信息,首先要有“信息放在哪里”的约定。Python 里的变量名不是一个贴满内容的盒子,更像是指向对象的标签:标签可以重新指向别的对象,对象本身也可能允许被修改。这个角度能解释很多初学者困惑的现象,例如为什么两个列表会一起变化,为什么字符串不能直接改某一个字符。

1.3.1 变量名是标签,不是固定类型的盒子

执行 score = 95 时,Python 创建或取得一个整数对象,并让名字 score 指向它。随后执行 score = "优秀",只是让同一个名字改为指向字符串对象;变量名没有被永久锁定为整数。动态类型让代码写起来灵活,但也要求你在关键位置主动确认类型。

PYTHON · 变量与类型
score = 95
print(score, type(score))

score = "优秀"
print(score, type(score))

name = "Matt"
message = f"{name} 的当前得分是 {95}"
print(message)
输出示例: 95 <class 'int'> 优秀 <class 'str'> Matt 的当前得分是 95

命名时要让名字表达内容,而不是表达类型。student_count 比 x 更容易维护,raw_text 比 data1 更能提醒你它还没有清洗。Python 社区通常使用小写字母加下划线的 snake_case,常量才会用全大写。命名不是装饰,它直接影响你能否在两周后读懂自己的代码。

变量名还应表达数据的“阶段”。例如 answer_text 表示用户刚输入的原始文本,answer_number 表示已经转换后的数字,is_valid 表示一个布尔判断。这样写可以避免把未经校验的输入误当成可信数据。大模型应用中,原始响应、解析后的 JSON、最终展示文本也最好使用不同名称,避免在处理中悄悄覆盖重要证据。

type() 适合学习和排查,但正式代码不应该到处依赖“先打印类型再决定做什么”。更常见的做法是让函数接收清晰的输入,并在边界位置做一次校验。需要判断一个值是否属于某一类时,可使用 isinstance();需要判断它是否表示一个有效业务状态时,则应该写一个更有意义的条件函数。

1.3.2 基本类型:数字、布尔值、空值和字符串

int整数:计数、索引、离散数量
float浮点数:比例、测量、近似小数
bool真假:条件判断的结果
None没有值:表示暂时不存在
表 3 · 常见值类型、示例与边界
类型示例常见用途初学者要留意
int 整数42计数、索引、章节编号除法 / 通常得到 float
float 浮点数0.75比例、概率、测量值是近似值,比较时考虑误差
str 字符串"Python"标题、提示词、文件内容数字文本仍是文本,先校验再转换
bool 布尔值True条件、开关、是否完成0、空字符串和空容器通常为假
None 空值None尚未产生或明确缺失的结果检查空值用 is None,不要与 0 混淆

整数和浮点数都能进行算术运算,但除法 / 通常返回浮点数;// 表示向下取整除法;% 返回余数;** 表示乘方。机器学习中经常出现比例、损失和概率,知道结果的类型可以避免把“显示为 0”误认为“真的没有”。浮点数是有限精度的近似值,比较它们时不要轻易使用 == 判断“是否完全相同”。

PYTHON · 运算与比较
completed = 7
total = 10
rate = completed / total

print(rate)       # 0.7
print(17 // 3)    # 5
print(17 % 3)     # 2
print(2 ** 3)     # 8
print(rate >= 0.6) # True

bool 只有 True 和 False 两个值,但很多对象都可以在条件中被当作真假:空字符串、空列表、空字典、数字 0 和 None 通常会被当作假;非空容器和非零数字通常会被当作真。读代码时,看到 if items:,可以把它理解为“如果 items 里有内容”。

1.3.3 字符串:文本不是数字,字符也有位置

字符串用引号包裹。"2026" 是文本,不是整数;它可以拼接、切片和查找,但不能直接参与需要数字的加法。字符串是不可变对象:你可以产生一个新字符串,却不能通过下标把原字符串的某一个字符就地替换。这一点和列表的可变性形成重要对比。

PYTHON · 字符串索引与切片
word = "Python"
print(word[0])       # P
print(word[-1])      # n
print(word[0:2])     # Py
print(word[2:])      # thon
print(len(word))     # 6

cleaned = word.strip().lower()
print(cleaned.replace("py", "my"))  # mython

大模型应用会大量处理字符串:提示词、文档、模型输出、日志、文件路径都属于文本。现在养成分清“原始文本”和“清洗后的文本”的习惯,后面做 Token 化和检索时会少很多混乱。常见操作包括 strip() 去掉首尾空白、lower() 统一大小写、split() 切分、join() 合并。

字符串的索引从 0 开始,切片的终点不包含在结果里。这个规则初看有点反直觉,却能让长度计算和区间拼接保持一致:text[:i] + text[i:] 恰好等于原字符串。处理 Token、字符窗口或日志片段时,先在纸上标出起点和终点,再写切片表达式,可以避免大量“少一个、多一个”的错误。

字符串方法通常返回新字符串,而不改变原字符串。cleaned = raw_text.strip() 会产生清洗后的值,raw_text 仍保留原样。保留原始数据很有用:当清洗规则需要调整时,你可以重新处理原文,而不用从已经被修改的结果中猜测丢掉了什么。不可变性在这里反而提供了安全边界。

数字转换也不是“输入越早转越好”。如果只是显示用户输入,保留文本没有问题;只有在需要计算、比较或排序时才转换。转换前可以先检查空字符串、正负号、小数点和范围。把校验写清楚后,错误信息会从“程序报错”变成“请输入 0 到 100 之间的整数”,使用体验也会更好。

1.3.4 输入和输出:用户输入永远先是字符串

input() 会暂停程序,读取用户输入,并返回字符串。即使用户输入了 18,得到的仍是 "18";需要参与计算时,要明确转换为 int 或 float。输出则通常使用 print(),需要把变量嵌入句子时优先使用 f-string。

PYTHON · 输入、转换与格式化
name = input("你的名字:").strip()
age_text = input("你的年龄:").strip()
age = int(age_text)

print(f"{name},明年你是 {age + 1} 岁。")

转换失败时会抛出异常,例如把 "十八" 交给 int()。初学阶段先理解“输入是字符串 → 校验 → 转换 → 计算”这条流程,不要急着把所有输入都写成一行。分行写更容易打印中间结果,也更方便定位问题。

还要注意“显示格式”和“数据本身”是两回事。f"{rate:.2%}" 只改变展示方式,不会把 rate 变成百分号字符串;如果后面还要比较或计算,继续使用原来的数字。类似地,print() 把内容显示给人看,但函数要把结果交给下一步处理,就应该使用 return。很多小程序难以扩展,根源就是过早把所有结果都打印掉了。

字符串清洗也应有顺序。先去掉两端空白,再决定是否统一大小写,最后再切分或替换。顺序不同可能得到不同结果:先切分再清洗,会留下带空格的空词;先把原文全部小写,则可能丢失人名、缩写或专有名词的大小写信息。学习阶段不用追求一条“万能清洗链”,而要能说清每一步为什么放在这里。

布尔值常常来自比较表达式,而不是手动写出来。is_ready = completed >= total 把一个判断保存下来,后面可以在多个分支中复用。注意比较时要明确边界:达到总数算完成,还是必须超过总数才算完成?代码无法替你决定业务含义,但变量名和测试案例可以把这个决定留下来。

字符串格式化还有一个重要用途:让输出既对人友好,又保留稳定结构。日志中可以使用固定前缀和字段名,例如 print(f"status=ok words={len(words)}");给人看的报告则可以使用自然语言。无论哪种形式,都不要把错误信息写成“出问题了”这种无法行动的句子,说明输入、限制和下一步通常更有帮助。

CHECKPOINT 03变量与输入输出练习

写一个“学习资料登记器”:输入姓名、已学章节数和总章节数,输出完成比例以及一条鼓励语。

  1. 确保章节数从字符串转换成整数。
  2. 完成比例保留两位小数,使用 f-string 格式化。
  3. 如果总章节数为 0,先思考应该如何处理,而不是让程序直接崩溃。
name = input("姓名:").strip()
done = int(input("已完成章节:"))
total = int(input("总章节数:"))
if total > 0:
    rate = done / total
    print(f"{name} 已完成 {rate:.2%}")
else:
    print("总章节数必须大于 0")

1.3.5 赋值、引用与可变性:变量到底“存”在哪里

把变量理解成标签后,还需要区分“重新绑定”和“修改对象”。执行 items = [1, 2],再执行 items = [3, 4],是让名称换了一个列表;执行 items.append(3),则是修改原来那个列表。两者的结果都可能在屏幕上显示为新内容,但对其他仍指向旧列表的名称影响完全不同。理解这个区别,才能解释函数参数、容器复制和缓存中的许多现象。

PYTHON · 重新绑定与原地修改
first = ["原始"]
second = first

first.append("新增")
print(second)          # ['原始', '新增']

first = ["换了一份"]
print(second)          # 仍是 ['原始', '新增']

如果需要一份互不影响的列表,可以使用 first.copy()、切片 first[:] 或 list(first)。这些方法对一层列表足够;当列表里还嵌套字典或其他列表时,只复制外层仍然可能共享内部对象。此时要根据数据结构选择浅拷贝或 copy.deepcopy(),不要把“复制”当成一个不需要说明的魔法动作。

整数、浮点数、布尔值和字符串通常被称为不可变对象。对不可变对象进行“改变”,实际是创建新对象再重新绑定名称。列表和字典则是可变对象,可以在原地增删元素。这个分类不是为了背术语,而是帮助你预测函数调用后的影响:函数如果收到了一个列表,函数内部的 append() 可能让调用者看到变化;收到一个整数,函数内部的加法不会改变调用者的整数。

1.3.6 数字、精度与表达式边界

Python 的整数可以表示很大的数,但浮点数使用有限精度。像 0.1 + 0.2 这样的结果可能显示为 0.30000000000000004,这不是计算机“算错了”,而是二进制无法精确表示许多十进制小数。做金额、评分阈值或概率比较时,应明确允许的误差,或根据任务使用 decimal.Decimal。学习阶段至少要知道,浮点数不适合无条件地用 == 比较。

PYTHON · 浮点数比较
import math

value = 0.1 + 0.2
print(value)
print(math.isclose(value, 0.3, rel_tol=1e-9))

运算符优先级也属于“值的含义”。乘除通常先于加减,比较通常晚于算术,and 和 or 还具有短路行为。不要依赖记忆去读一条很长的表达式,尤其是条件中混合算术和逻辑时。加括号不仅能改变结果,也能把你的业务意图直接告诉读者。清晰的括号往往比少写两个字符更专业。

布尔表达式返回的并不总是布尔值。a and b 会在 a 为假时直接返回 a,否则返回 b;a or b 会在 a 为真时直接返回 a。这让 name or "匿名" 很方便,但也可能让初学者误以为结果一定是 True 或 False。读到这种写法时,先检查参与运算的值,再判断最终类型。

1.3.7 字符串、Unicode 与可读格式

Python 的字符串以 Unicode 表示,中文、英文、表情符号可以放在同一个字符串里。len() 返回的是 Python 看到的字符数量,不一定等同于屏幕上的“字宽”,也不等同于 UTF-8 文件占用的字节数。需要和外部系统交互时,要区分文本和字节:text.encode("utf-8") 得到字节,data.decode("utf-8") 把字节还原为文本。多数基础脚本只处理文本,但这个边界会在文件、网络和模型 API 中反复出现。

字符串拼接可以用 +,但大量拼接更适合使用列表收集片段再 "".join(parts)。f-string 适合把少量值嵌入句子;模板内容很长或需要翻译时,可以把模板和数据分离。大模型提示词也不要把所有内容都硬编码在一条超长字符串里:将角色、任务、约束和输入分别保存,可以更容易测试、复用和审计。

PYTHON · f-string 与 join
name = "小林"
topics = ["变量", "函数", "文件"]

lines = [f"- {topic}" for topic in topics]
report = f"{name} 的学习清单:\n" + "\n".join(lines)
print(report)

转义字符会影响字符串的实际内容。\n 表示换行,\t 表示制表,\\ 表示反斜杠。Windows 路径中的反斜杠可能刚好组成转义序列,使用 Path 或原始字符串可以减少混淆。正则表达式又有自己一层转义规则,因此看到多个反斜杠时,不要凭视觉猜测;把字符串打印出来,或逐步构造模式。

1.3.8 输入验证:把不可信文本变成可用数据

input() 得到的内容来自用户,文件和网络响应也可以看作外部输入。外部输入默认是不可信的,不代表用户有恶意,而是它可能为空、包含空格、大小写不同、超出范围或格式错误。建议采用四步:读取原始文本、去掉不需要的空白、验证格式和范围、转换成目标类型。每一步单独写出来,错误提示才有机会准确。

PYTHON · 可重复的整数输入
def ask_positive_int(prompt):
    while True:
        raw = input(prompt).strip()
        try:
            value = int(raw)
        except ValueError:
            print("请输入整数,例如 3")
            continue
        if value <= 0:
            print("请输入大于 0 的数字")
            continue
        return value

chapter_count = ask_positive_int("章节数:")
print(f"计划学习 {chapter_count} 章")

验证规则要和业务目的相匹配。年龄可以是整数,比例可能是 0 到 1 之间的小数,章节名称则不应被强制转成数字。不要为了让程序“不报错”而吞掉所有异常,也不要把空值自动替换成一个看似合理的默认值。默认值只有在业务上确实有意义时才使用,并在界面或日志中说明它被采用了。

1.3.9 None、相等和缺失状态

None 表示“目前没有值”或“结果尚未产生”,它不等于空字符串、0 或 False。函数没有写 return 时,默认返回 None;字典的 get() 在键不存在时也可以返回 None。如果缺失状态会影响后续处理,应在边界处明确判断。检查空值时写 value is None,不要依赖不同对象之间的相等比较。

字典中可以用一个特殊标记区分“键不存在”和“键存在但值为 None”。这在读取配置和解析模型响应时很有用,因为缺失字段与明确给出空值可能代表不同含义。初学阶段可以先使用 if key in data;等遇到需要区分两种状态的函数,再了解哨兵对象。重要的是不要悄悄把所有异常输入都压成同一个值。

1.3.10 类型提示和自我检查

Python 的类型提示不会强制解释器在运行前检查所有类型,但它能向人和工具表达函数契约。写成 def summarize(text: str, limit: int = 5) -> list[str]:,读者马上知道输入和输出的大致形状。类型提示不是替代理解,也不是保证输入永远正确;它更像路标,配合运行时验证、测试和清楚的命名使用。

本节完成后,你应该能回答四个问题:这个名称当前指向什么对象?对象是否允许原地修改?外部文本何时转换成目标类型?缺失值和空值如何区分?如果还不能回答,不必继续堆更多语法,先用几组最小输入运行上一段代码。能解释值的来源和变化,才是变量小节的真正目标。

DEEP DIVE 03输入边界实验

写一个函数,读取用户输入的百分比并返回 0 到 1 之间的浮点数。请测试空字符串、80、80%、-1、101 和中文数字,分别给出可行动的提示。再把函数改成返回 (value, error_message),比较“抛异常”和“返回错误信息”两种接口的使用体验。

应先决定是否接受带百分号的文本,再统一处理;范围检查要覆盖边界 0 和 100;转换失败只捕获 ValueError,不要用裸 except:。返回错误信息时,调用者可以决定如何展示;抛异常时,调用者必须在合适的边界捕获。

1.3.11 组合赋值、解包和局部转换

组合赋值如 count += 1、total *= 2 能让更新动作更紧凑,但仍要注意对象是否可变。对于数字,它等价于重新计算后绑定;对于列表,items += other 通常会就地扩展原列表。学习阶段可先写成普通赋值比较前后状态,理解差异后再使用组合形式。

解包可以让数据形状变得显式:name, score = record 要求正好两个元素,name, *scores = record 则把剩余元素收集成列表。处理外部数据时,不要默认长度正确,先验证再解包。一个清楚的错误提示“记录需要 2 列,实际得到 3 列”,比直接暴露 ValueError 更有帮助。

PYTHON · 解包与星号表达式
record = ("Python", 92, "已学")
title, score, status = record
print(f"{title}:{score} 分,{status}")

first, *middle, last = [1, 2, 3, 4]
print(first, middle, last)

1.3.12 让输出适合人,也适合机器

给人看的输出可以有中文句子和百分比,给机器继续读取的输出应该有稳定字段和明确类型。不要把两者混在一行后再让下游程序用字符串切片解析。学习时可以同时输出一段友好摘要和一个 JSON 文件;摘要帮助检查,JSON 保留结构。这个习惯会在模型评测报告和日志分析中持续受益。

格式化数字时,先确定展示精度不会改变决策含义。0.004 显示为 0.00 可能让读者误以为它是零;金额、概率和指标应在说明中写清单位与舍入规则。显示格式不应覆盖原始数值,保存结果时保留足够精度,报告层再决定如何呈现。

到这里,变量小节的知识已经连接到输入、容器和函数。请回看自己的代码,标出每个变量的来源、类型和生命周期;如果一个名称在十几行中不断改变含义,就拆成两个更准确的名称。变量越接近真实业务概念,后续阅读和调试越轻松。

1.3.13 变量小节的迁移任务

把“学习资料登记器”升级为可重复调用的函数:接收姓名、已完成章节数和总章节数,返回一个字典,包含原始输入、转换后的数字、完成比例和状态。不要在函数内部打印。再写一个单独的展示函数,把字典格式化成给人看的句子。这样你能同时练习类型转换、返回值、字典和展示层分离。

为这个函数准备至少六个案例:正常比例、完成为 0、完成等于总数、完成超过总数、总数为 0、输入不是数字。先决定每个案例的业务规则,再选择返回错误信息或抛出异常。不要把所有错误都改成 0;错误值一旦混进统计结果,后面很难分辨是没有完成还是输入无效。

PYTHON · 返回结构而不是打印
def make_progress(name, done, total):
    if total <= 0:
        raise ValueError("总章节数必须大于 0")
    if not 0 <= done <= total:
        raise ValueError("已完成章节数超出范围")
    return {
        "name": name.strip(),
        "done": done,
        "total": total,
        "rate": done / total,
    }

record = make_progress("小林", 3, 8)
print(f"{record['name']}:{record['rate']:.0%}")

观察这个例子时,特别留意输入检查的位置和返回字典的结构。函数没有决定最终界面长什么样,也没有把百分比提前变成字符串,因此调用者仍然可以排序和计算。把数据和展示分开,是从小脚本走向可复用程序的重要一步。

如果你能不看本节代码重新写出这个迁移任务,并解释每个字段为什么存在,说明变量、类型与输入输出已经形成了自己的模型。接下来学习控制流时,继续使用同一份记录器作为练习对象,不断加入边界,而不是每一节都从完全陌生的例子开始。

1.3.14 变量与数据的可读性检查

打开一份两周前写的脚本,把所有单字母变量标出来,问自己能否在不运行的情况下说出它代表什么。临时的循环下标可以保留,承载业务含义的变量则应改成 document_count、raw_response 或 is_valid。命名让数据的来源和阶段可见,也让错误消息更容易与代码对应。

再检查每一次类型转换是否处在正确边界:输入刚读入时通常是字符串,校验通过后才转换成数字;文件读出来是文本,解析 JSON 后才得到字典或列表;模型响应是外部数据,验证字段后才交给业务逻辑。把转换和验证混在一行里,会让失败时难以说明究竟是哪一步出了问题。

最后用一个小表记录关键变量的“进入、改变、离开”。如果一个变量在十几个地方被重新绑定,考虑拆成多个阶段名称;如果一个列表被多个函数共享,说明是否需要复制或改成返回新列表。可读性不是排版装饰,而是让状态变化可以被另一个人复查。

1.3.15 Python 速览:从计算器走到第一个小程序

把前面的知识串起来,你已经可以完成官方教程意义上的一次 Python 速览:用数字做计算,用字符串保存文本,用列表组织一组值,再用 for 把每一项交给同一个规则处理。这个阶段不追求一次学会所有语法,而是练习“表达式产生值、赋值保存值、语句改变状态”的基本节奏。每运行一行,都问自己它产生了什么对象、对象是否被修改、下一行会读取什么。

一个小程序的四层拆解

第一层:数据。先准备数字、文本和列表;第二层:变换。用方法或表达式产生新值;第三层:遍历。逐项处理列表;第四层:展示。把结果格式化给人看。把四层分开后,你能很快替换其中一层,例如把本地列表换成文件读取,把展示文本换成 JSON 保存,而不必重写全部代码。

PYTHON · 第一个完整小程序
topics = ["变量", "列表", "函数", "文件"]
completed = {"变量", "列表"}

finished = [topic for topic in topics if topic in completed]
remaining = [topic for topic in topics if topic not in completed]

print(f"已完成 {len(finished)} 项:{'、'.join(finished)}")
print(f"还剩 {len(remaining)} 项:{'、'.join(remaining)}")
预期输出: 已完成 2 项:变量、列表 还剩 2 项:函数、文件

注意这个例子同时使用了列表、集合、列表推导式、成员判断、len() 和 join()。如果你觉得一行太密,可以把推导式改写成普通 for 循环,结果应该完全相同。能够在两种写法之间来回转换,说明你理解的是数据流,而不是只记住一段漂亮的语法。

BRIDGE 03把速览改成可复用函数

把上面的程序改成 summarize_progress(topics, completed) 函数,返回包含 finished、remaining 和 rate 的字典。分别测试全部完成、一个也没完成、列表为空、completed 中有不在 topics 的名称四种情况,并说明你的函数是否保留输入顺序。

输出字典中的列表应保持 topics 原有顺序;完成集合中多出的名称不应凭空出现在结果里;空列表时应提前定义比例是 0、None 还是抛出异常。函数只负责计算和返回,展示交给调用者。

1.3.16 计算器的细节:下划线、复数与优先级

在交互式解释器中,上一条表达式的结果通常会绑定到特殊名称 _,便于继续计算;脚本中不要把它当成可靠的业务变量,因为它依赖交互会话状态。Python 还支持复数,虚部使用 j 表示,例如 3 + 4j;复数可以参与加减乘除,但不能直接使用普通的大小比较。绝大多数入门任务只需要整数和浮点数,知道这些边界即可,不必为了记全类型而打断主线。

PYTHON REPL · 计算器状态
>>> 17 // 3
5
>>> _ + 1
6
>>> z = 3 + 4j
>>> z.real, z.imag
(3.0, 4.0)

表达式的求值顺序也值得从第一天练习。乘方先于乘除,乘除先于加减,比较晚于算术,and 和 or 还会短路。遇到混合表达式时加括号,既能保证结果,也能让读者看出意图。不要用“我记得 Python 应该这样算”代替一个三行实验;先预测,再在 REPL 中验证。

1.3.17 字符串的引号、转义与原始文本

单引号和双引号都可以创建字符串,选择一种并保持一致即可;如果文本本身包含引号,可以换另一种包裹,或使用反斜杠转义。三重引号适合多行文本和文档字符串,原始字符串前加 r 可以减少反斜杠转义,常用于正则表达式或 Windows 路径。原始字符串也有边界:它不能用一个孤立的反斜杠结束,真正处理文件路径时仍优先使用 Path。

PYTHON · 多种字符串写法
single = 'Python'
double = "他说:\"先运行再解释\""
raw = r"C:\data\notes.txt"
multi = """第一行
第二行"""

print(single, double)
print(raw)
print(multi)

字符串拼接有两种常见情形:相邻的字符串字面量在源代码中可以自动连接,变量之间则需要使用 +、f-string 或 join()。长文本最好拆成有意义的片段,避免一行滚动很久。写提示词、模板和文件内容时,先把数据与说明分开,再决定最终展示格式,后续修改会更安全。

04 · CONTROL FLOW

条件判断和循环:让程序按照信息做决定

如果程序只能从第一行执行到最后一行,它最多是一个计算器。条件判断让程序根据情况选择路径,循环让程序重复处理一批数据。数据预处理、批量评估、遍历文档、清洗词条,本质上都离不开这两种控制能力。

1.4.1 先理解布尔表达式,再写 if

if 后面放的是一个能够得到真或假的表达式。比较运算符包括 ==、!=、>、<、>= 和 <=;逻辑运算符 and、or、not 可以把多个条件组合起来。最容易犯的错误是把赋值符号 = 和比较符号 == 混在一起:前者改变变量,后者询问两个值是否相等。

PYTHON · if / elif / else
score = 82

if score >= 90:
    level = "优秀"
elif score >= 60:
    level = "合格"
else:
    level = "需要补强"

print(f"本次结果:{level}")

冒号表示一个代码块即将开始,缩进表示哪些语句属于这个块。Python 不用大括号包围代码块,因此缩进不是排版偏好,而是语法的一部分。统一使用 4 个空格,避免在同一文件里混用制表符和空格。条件分支要有明确的覆盖范围;如果多个条件可能同时成立,就按从特殊到一般的顺序排列。

等值与同一:== 比较两个对象的值是否相等,is 比较是否是同一个对象。日常业务判断通常使用 ==;检查空值时常写 value is None。不要用 is 代替所有的相等比较。

1.4.2 for 循环:遍历对象,不是盲目数数

Python 的 for 循环会从可迭代对象中依次取出元素。列表、字符串、字典、集合和 range() 都可以被遍历。与其先想“循环几次”,不如先问“我要处理哪一批对象”。当你确实需要下标时,优先使用 enumerate(),这样代码能同时拿到位置和元素。

PYTHON · for / range / enumerate
topics = ["Python", "数学", "机器学习"]

for index, topic in enumerate(topics, start=1):
    print(f"第 {index} 项:{topic}")

for number in range(1, 6):
    print(number * number)

range(1, 6) 包含 1 到 5,不包含终点 6。这种“左闭右开”的规则在切片和很多 Python API 中都会出现。它让连续区间的长度等于终点减起点,也使多个区间更容易首尾连接。遇到下标越界时,优先检查起点、终点和步长,而不是随意加一个数字。

1.4.3 while 循环:条件为真就继续

while 适合“还没有完成就继续”的场景,例如反复请求用户输入、逐步消费队列、直到满足停止条件。每次循环都要确认条件最终会变假;如果状态没有更新,就会得到无限循环。break 立即跳出最近一层循环,continue 跳过本轮剩余代码,进入下一轮。

PYTHON · while 与 break
attempts = 0

while attempts < 3:
    answer = input("输入 quit 结束:").strip().lower()
    attempts += 1
    if answer == "quit":
        print("已结束")
        break
    print(f"这是第 {attempts} 次输入")
else:
    print("已达到尝试次数")

循环的 else 不属于 if,而是在循环没有被 break 打断时执行。初学阶段不必刻意大量使用它,但读官方代码时要能看懂。更重要的是为循环写出结束条件,并在小数据上运行验证,不要一上来就拿一个几百万行文件测试。

控制流调试的一个实用方法,是先用极小输入手工走一遍。列表只有两个元素时,写下每一轮循环的下标、当前元素和累计结果;分支只有边界值、普通值和异常值三组测试。这样做看似慢,却能把“我感觉它应该这样”的模糊判断变成可检查的表格。后面处理数据集时,同样的思路可以扩展为单元测试。

多个条件组合时,先写出自然语言,再翻译成代码。例如“已经完成并且分数达到 80”对应两个条件的 and;“选择了 Python 或 PyTorch”对应 or;“不是空文本”可以写成 not text。不要依赖运算符优先级去猜复杂表达式,必要时加括号,让意图显式出现。

循环里修改正在遍历的容器是一个常见陷阱。删除列表元素会让后面的下标向前移动,删除字典键可能直接触发运行时错误。更稳妥的做法是遍历副本、创建新容器,或先记录要删除的键,循环结束后再统一处理。大模型数据清洗时,保留原始数据、生成清洗后的新列表通常也更利于追溯。

性能方面,先关注正确性,再关注速度。一个清晰的双层循环在小数据上完全够用;当数据量增大,才通过集合的快速成员判断、字典聚合、生成器或专门的数组库优化。过早把所有循环改写成难读的一行,往往既没有真正加速,也让错误更难定位。

1.4.4 用流程图检查分支是否完整

拿到一个值
↓
条件成立?
是执行处理
否走另一条路径
↓
输出或进入下一轮

写代码前先把“条件成立”和“条件不成立”都说出来;只有一条分支的想法,往往会漏掉边界情况。

表 4 · 控制流工具选择速查
工具适合的问题结束或分支方式常见提醒
if / elif / else根据条件选择路径条件命中后执行对应代码块先判断特殊情况,再判断一般情况
for逐个处理已有对象迭代对象耗尽后自然结束需要下标时优先用 enumerate()
while直到状态满足停止条件条件变假,或显式 break每轮都要让状态接近结束,避免死循环
continue当前项无须继续处理跳过本轮,进入下一轮确认跳过后不会遗漏必要的状态更新
match按固定模式或命令分派匹配第一个符合的 case保留 case _ 处理未知输入

条件表达式也会影响代码的可读性。与其写一串否定和嵌套,不如先把复杂判断拆成有名字的布尔变量,例如 has_input、within_limit、is_ready。名字把业务含义说出来,if has_input and within_limit: 就比重复一长串字符串长度和数字比较更容易检查。以后评估模型时,“是否有答案”“是否通过格式校验”也常常会写成类似的条件。

循环的每一次迭代都应该有一个可观察的状态。调试时可以临时打印当前下标、元素和累计结果,确认它们按照预期变化;确认后再删除或改成日志。若循环同时承担读取、清洗、计算和打印四件事,问题会很快变得混杂。把其中一部分提取成函数或新列表,通常比继续增加缩进更容易维护。

边界值是控制流练习的重点。范围为空时,for 循环一次也不执行;range(1, 1) 不是包含一个 1 的范围;while 的条件一开始为假时,循环体会被跳过。每个循环至少用“空输入、一个元素、多个元素”三种情况运行一次,这个习惯比背诵语法更接近实际编程。

CHECKPOINT 04条件与循环练习

编写一个程序,遍历 1 到 30:

  1. 能被 3 整除的数字输出“AI”。
  2. 能被 5 整除的数字输出“DATA”。
  3. 同时满足时输出“AI DATA”。
  4. 其他数字输出数字本身。
先计算 label = [],分别判断 3 和 5;把满足的文字加入列表,最后用 " ".join(label) 输出。两个条件都要判断,不能在第一个 if 满足后就跳过第二个。

1.4.5 条件表达式的完整性:先列出情况,再写分支

写条件之前,先把可能的情况列出来,往往比直接敲 if 更快。例如一个资料处理器至少有“文件不存在、文件为空、只有空白、存在有效内容、内容格式错误”五种状态。如果代码只写了“有内容”和“没内容”,格式错误可能被误当成正常数据。用自然语言列举状态,再把每种状态映射到一条路径,可以显著减少遗漏。

条件判断的顺序也会表达业务优先级。一个成绩既可能达到优秀,也可能达到合格;从高到低判断,第一次命中就足够。相反,如果两个标签可以同时成立,就不应该用互斥的 elif,而要分别判断后收集结果。选择 if、elif 还是多个独立 if,取决于状态之间是否互斥,而不是个人习惯。

PYTHON · 互斥与并行条件
score = 92
labels = []

if score >= 60:
    labels.append("及格")
if score >= 90:
    labels.append("优秀")

print(labels)  # ['及格', '优秀']

复杂条件最好先命名。把 not text.strip() or len(text) > 2000 直接塞进一层嵌套,读者很难知道它表达的是“内容不可用”。拆成 is_blank、is_too_long、is_valid 后,条件变成对业务概念的组合,也更容易针对每个布尔变量编写测试。

1.4.6 for、range、enumerate、zip:把遍历写成数据关系

for 不是只能配合列表使用。字符串会逐字符遍历,字典默认遍历键,dict.items() 同时提供键和值,文件对象逐行提供文本,生成器则在需要时产生下一个结果。写循环前先确认对象能提供怎样的迭代接口,再选择是否需要下标、键值对或多个序列的对应关系。

range() 适合生成整数序列,enumerate() 适合给已有对象编号,zip() 适合把多个等长序列并排处理。不要用 range(len(items)) 取代所有循环;如果只需要元素,直接遍历元素更清楚。如果两个序列长度不一致,普通 zip() 会在较短序列结束时停止,必要时再考虑 itertools.zip_longest() 并明确缺失值。

PYTHON · enumerate 与 zip
topics = ["Python", "概率", "向量"]
status = ["已学", "待学", "待学"]

for index, (topic, state) in enumerate(zip(topics, status), start=1):
    print(f"{index}. {topic}:{state}")

遍历字典时,for key in data 得到键,for value in data.values() 得到值,for key, value in data.items() 同时得到两者。不要把字典当作列表用整数下标访问;字典的优势是用有意义的键表达关系。处理模型响应时,先确认字段是否存在,再取值,避免把嵌套结构当成平面列表。

1.4.7 while、break、continue 与循环不变量

while 适合停止条件不由固定次数决定的任务。写它时先找一个会变化的状态,例如剩余尝试次数、队列长度或当前文件位置,再说明每一轮如何让状态接近结束。这个“每轮都更接近终点”的描述就是循环不变量。若找不到这样的状态,就应该怀疑循环可能永远不会结束。

break 用于确认目标已找到、用户主动退出或发生不可继续的状态;continue 用于跳过当前无效项。两者都能减少嵌套,但滥用会让控制路径分散。建议为每个提前跳转写一句原因,或者把复杂循环拆成两个函数,让退出条件在函数名和文档中可见。

循环的 else 表示“循环自然结束,没有被 break 打断”。它适合搜索任务:遍历完所有候选仍未找到目标时,执行 else。如果团队成员不熟悉这种写法,普通的布尔标记可能更直观。无论选哪种方式,都要写一个“找到”和“找不到”的测试,确认两条路径都能到达。

1.4.8 列表推导式与生成器:简洁要建立在可读之上

列表推导式把“遍历、变换、筛选”放在一个表达式里,例如 [word.lower() for word in words if word]。当逻辑只有一层时,它很清楚;一旦出现多重条件、异常处理或副作用,就应该退回普通循环。不要为了少写几行而牺牲逐步调试的能力,尤其是学习和数据清洗初期。

生成器表达式使用圆括号,按需产生结果,不会一次性创建完整列表。处理很大的文本时,(line.strip() for line in file if line.strip()) 能减少内存占用。但生成器只能向前消费一次,不能像列表一样随意索引和重复遍历。选用生成器前,先确认下游代码是否需要长度、下标或多次迭代。

PYTHON · 推导式与显式循环
raw = [" Python ", "", "模型 "]
cleaned = [item.strip().lower() for item in raw if item.strip()]
print(cleaned)

result = []
for item in raw:
    item = item.strip()
    if item:
        result.append(item.lower())
print(result)

1.4.9 嵌套循环、复杂度与实际取舍

双层循环不一定错误,但要知道它的成本如何随数据规模增长。遍历每个文档,再遍历文档中的每个词,是自然的嵌套关系;若每个词又扫描一遍停用词列表,规模大时就会变慢。把停用词存进集合,或先构建字典索引,能够减少重复比较。优化前先用小数据验证逻辑,再用计时或数据规模解释为什么需要改变算法。

不要仅凭“这一行看起来高级”判断性能。列表推导式、集合查找、字典聚合、生成器各有适用场景;真正影响速度的往往是重复扫描、网络请求和不必要的文件读写。大模型应用中,调用一次远程模型可能比几千次本地循环更慢,因此应先区分 CPU、内存、磁盘和网络瓶颈,再决定优化方向。

1.4.10 用状态机思路处理多步流程

当程序有“等待输入—校验—重试—成功—失败”多个阶段时,可以把阶段写成一个明确的状态变量,而不是不断增加嵌套。状态机不是必须引入类或框架,几个字符串和一个循环就可以开始。重要的是规定每个状态允许哪些输入、会转移到哪里,以及什么时候结束。这个思路也适合模型工作流:准备提示词、发送请求、解析结果、验证格式、记录失败。

图 4 · 循环的状态变化先定义状态,再决定重复条件。
未完成
↓
本轮输入有效?
是更新结果并前进
否提示并保留状态
↓
完成或达到上限

每一轮都要说明状态如何变化;没有状态变化的 while 循环,通常就是无限循环的来源。

1.4.11 控制流调试和测试

调试分支时,至少准备三类输入:最小正常值、边界值和明显异常值。比如章节数用 0、1、总数、超过总数和非数字文本测试;循环用空列表、一个元素和多个元素测试。每次只改一个条件,观察哪一组输入的结果发生变化。把输入和预期输出写成表格,比凭感觉连续点击运行按钮更有效。

对于循环,可以暂时打印迭代次数、当前元素和累计结果;对于分支,可以打印命中的标签。确认逻辑后再删除调试输出,或改用日志级别控制。不要在生产脚本中留下会泄露完整文档或密钥的打印。可观察不等于无边界地输出所有内容。

本节完成标准是:你能解释一个条件为什么覆盖某类输入,能说明一个循环如何结束,能选择直接遍历、编号遍历或并排遍历,能在空输入和边界输入上预测结果,并能把一个混乱流程拆成几个清楚的状态。掌握这些能力后,后面处理列表、字典和文件时,错误会明显减少。

DEEP DIVE 04资料处理状态机

设计一个命令行流程:用户输入资料标题;空标题要求重试;输入 quit 结束;最多允许 5 次有效标题;最后输出标题列表和重复标题。请画出状态转移图,再分别测试立即退出、全是空输入、达到上限和包含重复标题四种情况。

把“输入次数”“有效标题列表”和“是否退出”作为明确状态;空输入不应增加有效标题计数;quit 应先于普通标题判断;重复检查可以在最终阶段使用 Counter。评分重点是状态和结束条件是否可解释,而不是循环写得多短。

1.4.12 pass、match 与选择合适的工具

pass 表示“这里暂时什么也不做”,常用于先搭出函数或分支的结构。它不是跳过循环的命令,也不会自动忽略错误。写占位代码时可以使用,但在提交前应补上明确行为,或用注释说明为什么暂时留空。清楚的占位比一段永远不会执行的假逻辑更安全。

较新的 Python 提供 match 进行结构匹配,适合根据命令、元组形状或字典字段选择处理方式。初学阶段先用 if/elif 掌握条件逻辑,再把重复的模式匹配改成 match。无论使用哪种语法,都要写默认分支,说明遇到未知输入时程序如何处理。

PYTHON · match 的最小示例
command = "list"

match command:
    case "list":
        message = "显示资料"
    case "quit":
        message = "结束程序"
    case _:
        message = "未知命令"

print(message)

1.4.13 控制流与大模型数据管道

批量处理模型输入时,外层循环遍历文档,内部条件判断是否为空、是否超长、是否已处理;函数负责清洗或调用接口;异常处理决定这一条失败后是重试、跳过还是停止。把每个决策写成清楚的分支,才能统计成功数、失败数和跳过数。否则最终只看到一个总数,却不知道数据在哪一步消失。

批处理要避免一条坏数据让全部任务停止,也要避免无声跳过错误。可以在循环中捕获具体异常,把文档 ID 和错误类型写入失败清单;达到连续失败上限时停止并提醒人工检查。这个策略把控制流和可观察性连在一起,是从练习脚本走向真实数据管道的关键。

当一个循环超过几十行时,先问能否抽出“处理单条记录”的函数;当嵌套超过三层时,先问能否使用提前返回、状态对象或拆分阶段。缩进深度不是绝对质量指标,但过深通常意味着职责和状态没有分离。清晰的控制流会让测试案例更少、更有针对性。

最后复习左闭右开、空序列、短路求值和修改容器这四个高频边界。它们分别对应范围计算、循环是否执行、条件是否安全、迭代期间数据是否稳定。每个边界写一个三行以内的实验,保存输出;以后遇到类似代码,先查自己的实验记录,再查官方文档。

1.4.14 控制流小结:把每条路径写成可验证的承诺

每一个分支都应该回答“满足条件时做什么,不满足时做什么”;每一个循环都应该回答“从哪里开始,怎样前进,何时结束”;每一个提前退出都应该回答“为什么此刻不再继续”。如果这些问题只能靠阅读大量上下文才能回答,说明控制流需要拆小或补充命名。

为程序设计路径覆盖表:正常输入、空输入、边界输入、格式错误和外部故障各走哪条路径。执行一次测试后,在表中标记实际结果;没有覆盖的路径就是下一条练习。这样做比追求“每一行都跑过”更关注真实风险,也更容易迁移到模型评测和批量数据处理。

把流程图、伪代码和最终 Python 代码互相对照,检查是否遗漏了一个分支。伪代码不需要使用 Python 语法,它的任务是先确认业务顺序;代码则负责把顺序变成可执行规则。先想清楚再编码,通常能减少反复修改缩进和条件的时间。

如果你能独立实现一个带重试上限、空输入处理、重复检测和最终汇总的命令行循环,说明控制流已经从“记住关键字”进入“管理状态”。下一步学习数据结构时,把每一轮的状态放进字典或对象,观察代码如何变得更有结构。

控制流学习的最后一道检查,是能把自己的流程先写成自然语言,再翻译成 Python,并为每条路径准备一个最小输入。只要你能说清“何时继续、何时跳过、何时结束、失败后怎么办”,语法就不再是孤立的关键字,而是对状态变化的准确描述。

把控制流练习写成一个可复用模板:先列状态,再列分支,再列循环不变量,最后写测试输入。模板不会替你解决问题,但能提醒你在编码前检查空输入、边界和失败路径。

1.4.15 官方教程里的遍历技巧:键值、编号、反向与排序

循环并不只是一种写法。处理字典时用 items() 同时取得键和值;处理序列时用 enumerate() 增加编号;需要把两组数据并排处理时用 zip();需要逆序查看时用 reversed();需要稳定展示时用 sorted()。这些函数不会改变原对象,通常返回一个可以遍历的结果,因此可以组合起来,但组合前要先确认每一步的数据形状。

先选择你真正需要的信息

表 4.1 · 五种遍历方式的选择
写法循环变量得到什么适合场景边界提醒
for value in items元素本身只需要逐项处理不要为了下标额外创建范围
enumerate(items, 1)编号和元素生成带序号的报告编号从几开始要写清楚
data.items()键和值读取字典记录不要把键当成列表下标
zip(a, b)对应位置的元组并排处理多组数据默认在较短序列结束时停止
sorted(items)排序后的新序列稳定展示或排名排序依据和同分规则要明确
PYTHON · 组合遍历工具
scores = {"Python": 92, "函数": 88, "文件": 92}

for index, (name, score) in enumerate(
    sorted(scores.items(), key=lambda pair: (-pair[1], pair[0])),
    start=1,
):
    print(f"{index}. {name}: {score}")
预期输出: 1. Python: 92 2. 文件: 92 3. 函数: 88

这里的排序键是一个二元组:先用负分数实现降序,再用名称实现同分时的字母或文字顺序。排序稳定性意味着同一个键的元素会保持原有相对次序,但如果你希望报告每次都一致,最好显式提供第二排序键。先把“排名规则”写成自然语言,再翻译成 key 表达式,结果会更容易复查。

遍历字典和集合时不要依赖屏幕上碰巧出现的顺序;遍历列表和字符串时,则可以利用它们的顺序语义。若业务要求“去重但保留第一次出现的顺序”,可以用一个集合记录是否见过,再用列表收集第一次出现的值。这种“两个容器协作”的写法,比强行让单个容器同时承担互相冲突的职责更清楚。

TRAVERSAL 1.4.15把遍历规则写成报告

准备三个等长列表:主题、完成状态和预计分钟数。用 zip() 组合,用 enumerate() 编号,只输出未完成主题;再用 sorted() 生成一份按预计时间从长到短的副本。最后把列表长度故意改成不一致,观察普通 zip() 的行为,并写下你是否需要 itertools.zip_longest()。

只输出未完成主题时,判断应放在循环体内;排序前不要修改原列表;长度不一致时普通 zip() 会在较短列表结束,若缺失项本身有意义,才使用带默认值的 zip_longest() 并在报告中标明缺失。

易错:遍历对象不等于修改对象

很多初学者看到一个列表,就会在遍历它的同时删除元素。这样做容易跳过相邻元素,因为删除动作会让后面的元素向前移动,而循环的内部位置已经继续向后走。更稳妥的做法是建立一个新列表,把符合条件的项目收集起来;如果确实要原地修改,则先遍历切片副本,并把这个选择写在注释或函数名里。

PYTHON · filter_topics.py
topics = ["  Python  ", "", "  模型", " ", "数据  "]

kept = []
for topic in topics:
    cleaned = topic.strip()
    if cleaned:
        kept.append(cleaned)

print(kept)
print("原列表仍然存在:", topics)
预期输出: ['Python', '模型', '数据'] 原列表仍然存在: [' Python ', '', ' 模型', ' ', '数据 ']

这里的 strip() 只生成清洗后的字符串,不会偷偷改变原列表;if cleaned 利用空字符串为假来过滤空项。若要保留原始空白用于审计,可以同时保存 raw 和 cleaned;若只关心最终标题,才把清洗列表作为后续输入。遇到 break、continue 或多层循环时,分别写一句“为什么提前结束”“为什么跳过这一项”,能让控制流更容易被别人复核。

还有一个常见误区是把“循环次数”和“处理数量”当成同一件事。continue 仍然消耗了一轮循环,break 则会让剩余数据没有机会处理;如果报告要统计成功、跳过和失败三种数量,就应该为它们分别准备计数器,并在每个出口更新一次。调试时可以先用五条数据打印每一轮的状态,再换成真实输入。这样你看到的不只是最终数字,还能解释每条记录为什么走到这个结果。

05 · DATA STRUCTURES

列表、字典、元组和集合:选择合适的容器

数据结构就是“如何把多个值组织在一起”。大模型工作流里的文档列表、标签映射、配置参数、已处理 ID、Token 序列,都需要容器来保存。不要把四种结构当作四组需要背诵的语法;更好的问题是:我要不要顺序?要不要通过键查找?允许不允许重复?内容会不会改变?

表 5 · 四种常见容器的选择
结构写法顺序重复适合的任务
list 列表["a", "b"]保留允许一批需要按顺序处理的项目
tuple 元组("a", "b")保留允许不希望被修改的固定组合
dict 字典{"id": 1}保留插入顺序键不可重复用名称或键快速取值
set 集合{"a", "b"}不依赖顺序自动去重成员判断、去重、集合运算

1.5.1 列表详解:从创建到可靠操作

列表是 Python 中最常用的容器之一。它可以按顺序保存多个对象,允许重复,也允许在原位置增加、删除和替换元素。学习列表不能只停留在“用方括号写出来”:还要知道下标怎样计算、切片是否修改原数据、方法返回什么、多个名称是否指向同一份列表,以及什么时候应该换成其他容器。本小节把这些问题拆成多个内容框,每个代码示例只承担一个清晰任务。需要查完整接口时,可对照 Python 官方列表类型文档 ↗。

一、先建立模型:列表是“有顺序的元素序列”

列表中的每个元素都有一个位置编号,编号从 0 开始。下面的四个元素中,第一个元素的下标是 0,最后一个元素既可以用 3 表示,也可以用 -1 表示。负数下标从末尾往前数,-1 是最后一个,-2 是倒数第二个。列表保存的是对象的引用,因此元素可以是数字、字符串、布尔值,甚至还可以是另一个列表。

0"Python"
1"数学"
2"模型"
3"Python"
-1最后一项

列表允许重复并不代表重复一定有价值。保留重复项适合记录事件顺序、用户输入或一批待处理任务;如果只关心“是否出现过”,集合通常更合适。先描述数据的关系,再决定使用哪种容器,往往比先背下方法名更重要。

PYTHON · 创建列表并读取元素
topics = ["Python", "数学", "模型", "Python"]

print(topics[0])
print(topics[-1])
print(len(topics))
print("模型" in topics)
print(topics)
预期输出: Python Python 4 True ['Python', '数学', '模型', 'Python']

这里的 len() 返回元素数量,in 做成员判断。成员判断只回答“有没有”,不会告诉你位置;需要位置时使用 index(),但要留意目标不存在会抛出 ValueError。把“判断存在”和“取得位置”分开,是避免异常的第一个习惯。

二、索引、切片和嵌套:一次读懂数据形状

下标访问得到一个元素,切片访问得到一个新列表。切片写成 items[start:stop:step],起点包含、终点不包含,步长默认为 1。省略起点表示从头开始,省略终点表示到末尾;步长为负数时可以倒序读取。左闭右开的规则让 items[:2] 恰好包含前两个元素,也让两个相邻切片更容易拼接。

PYTHON · 切片与步长
chapters = ["环境", "变量", "控制流", "容器", "函数"]

print(chapters[1:4])
print(chapters[:2])
print(chapters[::2])
print(chapters[::-1])

chapters[1:3] = ["类型", "输入输出"]
print(chapters)
预期输出: ['变量', '控制流', '容器'] ['环境', '变量'] ['环境', '控制流', '函数'] ['函数', '容器', '控制流', '变量', '环境'] ['环境', '类型', '输入输出', '容器', '函数']

普通切片表达式会创建一个新的列表,修改这个新列表不会影响原列表;切片赋值则不同,它会修改左侧列表本身,而且右侧可以与原切片长度不同。这个特性可以一次替换、插入或删除一段连续元素,但初学时要先打印修改前后的列表,确认自己是在“取副本”还是在“改原对象”。

嵌套列表常用来表示表格或矩阵,例如 matrix[1][2] 表示第二行第三列。访问嵌套结构时要从外到内逐层确认形状;如果某一行长度不足,错误会在内层下标处出现。真实数据可能不是规则矩阵,先验证每一行长度,再进行列访问。

PYTHON · 嵌套列表
matrix = [
    [1, 2, 3],
    [4, 5, 6],
]

print(matrix[0])
print(matrix[1][2])
print([row[0] for row in matrix])

三、修改列表:区分“重新绑定”和“原地修改”

列表是可变对象。调用 append()、给某个下标赋值,或者使用 sort(),都会改变原来的列表。相反,执行 items = items + [new_item] 通常会创建新列表,再让名称指向新对象。两种写法最终可能显示相似,却会对其他仍指向旧列表的名称产生不同影响。

PYTHON · 别名、原地修改与浅拷贝
source = ["原始"]
alias = source
backup = source.copy()

source.append("新增")
print(alias)
print(backup)

source = ["重新绑定"]
print(alias)
print(source)
预期输出: ['原始', '新增'] ['原始'] ['原始', '新增'] ['重新绑定']

copy()、list(items) 和 items[:] 都可以复制列表的第一层。它们是浅拷贝:如果列表里还有嵌套列表或字典,内部对象仍可能共享。需要完全独立的嵌套结构时,再根据数据形状考虑 copy.deepcopy();不要默认“复制了外层”就等于所有内容都分离。

批量清洗资料时,保留一份原始列表很有价值。清洗后的结果可以放进 cleaned,原始输入保留在 raw_items;当规则改变时重新处理原始数据,而不是从已经删改过的列表中猜测哪些信息被丢掉了。

四、列表方法总览:先看动作,再看返回值

列表方法可以按“增加、删除、查找、重排、复制”来记忆。下表把常用方法的动作、是否改变原列表以及典型返回值放在一起。方法名只是入口,真正需要记住的是它对原对象做了什么,以及下一行代码应该继续使用哪个变量。

表 5.1 · 列表常用方法的动作与返回值
方法作用改变原列表返回值或失败方式常见用法
append(x)末尾添加一个元素是None加入一条新记录
extend(iterable)依次加入一批元素是None合并两个序列
insert(i, x)在指定位置插入是None插入优先项
remove(x)删除第一个等于 x 的元素是None;找不到为 ValueError删除已知值
pop(i)删除并取出指定位置元素是被取出的元素;空或越界为 IndexError处理栈顶任务
clear()删除全部元素是None清空临时缓存
index(x)查找第一次出现的位置否整数;找不到为 ValueError定位记录
count(x)统计元素出现次数否整数统计重复标题
sort()按规则原地排序是None按长度或字母序排列
reverse()原地翻转顺序是None倒序展示结果
copy()复制第一层列表否新列表保留可修改副本

官方文档把这些方法列为列表对象的接口,但学习时不要把它们当作互相孤立的单词。比如“找到并删除”通常需要先判断是否存在,或者捕获 ValueError;“取出最后一项”使用 pop(),而不是先读下标再调用 remove()。动作和返回值一起理解,代码才不容易写反。

五、添加元素:append、extend 与 insert

append(x) 只添加一个元素,即使这个元素本身是列表,也会把整个列表作为一个整体放进去。extend(iterable) 会遍历右侧对象,把其中的元素逐个加入;因此对字符串使用 extend("AI") 会加入两个字符,而不是加入一个字符串。insert(index, value) 则是在指定位置之前插入一个元素。

PYTHON · append、extend 与 insert
tasks = ["读文档"]
tasks.append(["跑示例", "写笔记"])
print(tasks)

tasks = ["读文档"]
tasks.extend(["跑示例", "写笔记"])
tasks.insert(1, "标出疑问")
print(tasks)
预期输出: ['读文档', ['跑示例', '写笔记']] ['读文档', '标出疑问', '跑示例', '写笔记']

插入位置为 0 表示开头,等于当前长度时效果类似末尾追加。频繁在列表开头插入会移动大量元素,数据量大或需要两端操作时,应考虑 collections.deque。选择工具时要看操作模式,而不是只看“列表能不能做到”。

六、删除元素:remove、pop、clear 与 del

remove(value) 按值删除第一次出现的元素,找不到时抛出 ValueError;pop(index) 按位置删除并返回元素,省略索引时默认取最后一项;clear() 会清空所有元素但保留列表对象。知道“要删什么值”用 remove,知道“要取哪个位置”用 pop。

PYTHON · remove、pop 与 clear
queue = ["A", "B", "C", "B"]
queue.remove("B")
last = queue.pop()
print(queue, last)

queue.clear()
print(queue)
预期输出: ['A', 'C'] B []

del items[1] 也能按下标删除,del items[1:3] 可以删除一段切片。删除来自用户输入的值时,先判断 if value in items: 或捕获具体的 ValueError;不要用裸 except: 把真正的问题藏起来。空列表调用 pop() 会触发 IndexError,取出前先决定空结果应该如何处理。

七、查找、计数、排序和复制:不要猜返回值

index(value) 返回第一次出现的位置,也可以指定起点和终点;count(value) 返回出现次数,不改变列表。若只要判断是否存在,value in items 更直接。排序时,sort() 和 reverse() 都是原地修改并返回 None;sorted(items) 和 items[::-1] 会得到新的列表。

PYTHON · index、count、sort、reverse、copy
fruits = ["orange", "apple", "pear", "banana", "apple"]

print(fruits.count("apple"))
print(fruits.index("banana"))
print(fruits.index("apple", 1))

fruits.reverse()
print(fruits)
fruits.sort()
print(fruits)

backup = fruits.copy()
print(backup)
高频错误:不要写成 ordered = fruits.sort() 然后再使用 ordered。原列表确实已经排序,但 ordered 得到的是 None。凡是原地修改方法,都先查它的返回值,再决定是否需要赋值。

排序要求元素之间有共同的比较规则。数字之间、字符串之间通常可以排序;混合了 None、数字和字符串的列表,直接排序会在比较阶段失败。真实资料里可能同时出现空值和文本,先定义缺失值应该排在前面还是后面,再通过 key 函数写出规则。

八、从普通循环到列表推导式

列表推导式适合表达“遍历、筛选、生成新列表”。它不是新的计算能力,只是把循环写得更紧凑。建议先写普通循环,确认输入、条件和结果,再把简单版本改写成推导式。只要出现多层嵌套、复杂异常处理或副作用,就退回普通循环,清楚比短小更重要。

PYTHON · 推导式的最小例子
chapters = ["Python 基础", "数学直觉", "机器学习", "NLP"]

model_topics = [
    chapter for chapter in chapters
    if "学" in chapter
]
print(model_topics)
预期输出: ['数学直觉', '机器学习']

推导式中最左边的表达式决定新列表中的元素,后面的 for 提供对象,最后的 if 负责筛选。可以有多个 for,但阅读成本会快速上升。把变量命名成 non_empty_titles、valid_scores,比写成 result 更容易复查。

PYTHON · 嵌套推导式与矩阵转置
matrix = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
]

transposed = [[row[i] for row in matrix] for i in range(4)]
print(transposed)
PYTHON · 用普通循环读懂同一件事
matrix = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
]

transposed = []
for i in range(4):
    column = []
    for row in matrix:
        column.append(row[i])
    transposed.append(column)

print(transposed)

如果矩阵每一行长度不一致,row[i] 可能触发 IndexError。对外部数据使用推导式前,先检查形状,或使用 zip(*matrix) 并明确缺失值策略。

九、列表在真实任务中的两个边界

列表末尾的 append() 和 pop() 可以实现后进先出的栈,适合撤销记录和待处理任务。队列是先进先出,如果在列表开头反复使用 pop(0),后面的元素需要整体移动;需要两端频繁进出时使用 deque 更合适。

PYTHON · 栈与队列
from collections import deque

stack = ["读取", "清洗"]
stack.append("统计")
print(stack.pop())

queue = deque(["任务 A", "任务 B"])
queue.append("任务 C")
print(queue.popleft())
print(queue)
预期输出: 统计 任务 A deque(['任务 B', '任务 C'])

批量清洗资料时还要避免在遍历列表的同时删除元素,否则下标移动可能跳过项目。更稳妥的做法是生成新列表:kept = [item for item in items if 条件];如果必须原地修改,再遍历副本并写一个回归测试。

DEEP DIVE 1.5.1列表方法综合练习

创建一个待学习主题列表,完成以下任务:在末尾加入一个主题;一次加入三个主题;在第 2 个位置插入“复习”;统计“Python”出现次数并找出第一次出现的位置;删除一个指定主题并用 pop() 取出最后一项;生成一份按名称长度排序的新列表,同时保留原列表;写出一个列表推导式,只保留名称中包含“模型”的主题。

请额外测试空列表、重复主题、删除不存在的主题和包含数字与字符串的混合列表。每一步写下“原列表是否改变、方法返回什么、异常是否可恢复”,再运行代码验证。完成后,把其中一个列表换成 deque,说明为什么两端操作时它更合适。

添加时区分单个元素和一批元素;查找前决定是否允许目标不存在;排序使用 sorted() 保留原列表;删除不存在的主题时捕获 ValueError;空列表取出元素前先判断。最后用一个普通 for 循环解释列表推导式,确认自己理解每一层遍历。

1.5.2 字典:让数据拥有可读的键

字典用键映射到值。与其记住“第 3 个元素代表作者”,不如用 record["author"] 明确表达。读取不存在的键会抛出 KeyError,可以使用 get() 提供默认值。遍历字典时,items() 同时提供键和值;keys() 和 values() 则分别提供一侧。

PYTHON · 创建字典并按键访问
lesson = {
    "title": "Python 基础",
    "chapter": 1,
    "topics": ["变量", "循环", "函数"],
    "published": True,
}

print(lesson["title"])
print(lesson.get("author", "待补充"))
预期输出: Python 基础 待补充
PYTHON · 遍历字典的键和值
lesson = {
    "title": "Python 基础",
    "chapter": 1,
    "published": True,
}

for key, value in lesson.items():
    print(f"{key}: {value}")

字典方法要看“是否修改”和“返回什么”

字典操作的学习重点不是把方法名背成清单,而是先判断它是否会改变原字典,以及返回值是否可以继续使用。update()、setdefault() 会修改原对象;get() 只读取;pop() 会删除并返回值;items()、keys() 和 values() 提供遍历视图。调用前先写出“我希望旧数据变吗”,能避免把结果误存成 None。

表 5.2 · 字典常用操作
操作作用修改原字典缺失键时
data[key]读取必填值否抛出 KeyError
data.get(key, default)读取可选值否返回默认值
data[key] = value新增或更新是自动创建键
data.update(other)批量合并是新键会被加入
data.pop(key)删除并取得值是可提供默认值,否则抛错
key in data判断键是否存在否得到 True 或 False
PYTHON · update、pop 与 del
record = {"title": "Python", "done": False}
record.update({"score": 92, "done": True})

if "score" in record:
    score = record["score"]
removed = record.pop("done", None)
del record["score"]

print(record)
print(removed, score)
预期输出: {'title': 'Python'} True 92

大模型应用中的一条消息、一个文档块、一次评测结果,通常都可以表示成字典;很多字典再放进列表,就形成结构化数据。嵌套结构的关键不是“写得深”,而是每一层都有稳定含义。访问之前先确认数据形状,必要时用 type()、len() 和打印一小段内容检查。

字典键最好使用稳定、可读的名称。"text"、"source"、"score" 比 "x1"、"x2" 更能让后续代码自解释。如果外部数据的键名不统一,可以在进入系统的边界处做一次规范化,而不是让每个函数都记住三种别名。数据结构的稳定接口,会让整个项目的复杂度下降。

列表中的元素也不一定要是简单字符串,它们可以是字典、元组或另一个列表。关键是保持同一列表里元素的形状一致:不要让第一项是字符串、第二项是字典、第三项又是元组,除非你有充分理由。形状混乱会迫使每个消费者都写额外的类型判断,也更容易在遍历中触发错误。

1.5.3 元组和集合:固定组合与快速去重

元组与列表都能保存有顺序的多个值,但元组本身不能修改。它适合表示坐标、RGB 颜色、固定配置或函数返回的多个结果。集合不依赖顺序,元素不能重复;把一个列表交给 set() 可以快速去重,但重新转回列表时不要假设原顺序保持不变。

先看元组:元组适合保存位置有意义、结构固定的组合。它不能通过下标重新赋值,因此坐标和颜色等数据不容易被后续代码意外改掉。

PYTHON · 元组:固定组合
# 中文地名必须是字符串,所以要加引号
point = ("成都", 30.67, 104.06)
print(point[0])
print(point[1:])

rgb = (24, 107, 102)
print(rgb)
预期输出: 成都 (30.67, 104.06) (24, 107, 102)

再看集合:集合适合去重和成员判断,不保证你可以依赖某一种展示顺序。把列表转换为集合会丢掉重复项;如果报告仍需要原来的出现顺序,就保留列表,同时用集合做快速判断。

PYTHON · 集合:去重与集合运算
tags = ["python", "nlp", "python", "data"]
unique_tags = set(tags)
print(unique_tags)
print("nlp" in unique_tags)

required = {"python", "pytorch"}
known = {"python", "nlp"}
print(required - known)  # 还缺哪些
输出说明:集合的打印顺序不应作为业务依据;成员判断会得到 True,差集会得到尚未掌握的标签。

元组的形状与集合的关系

元组通常用来“打包”一个固定结构,集合则用来表达“哪些元素属于同一组”。元组支持索引和切片,但不能对位置重新赋值;集合支持并集、交集、差集和对称差集,但不支持按位置访问。单元素元组必须有逗号,空集合必须调用 set(),因为 {} 表示空字典。

PYTHON · 打包、解包与集合关系
record = ("Python", 92, "已学")
title, score, status = record
print(title, score, status)

single = ("只有一个元素",)
empty_set = set()
frontend = {"Python", "HTML"}
backend = {"Python", "SQL"}
print(frontend | backend)  # 并集
print(frontend & backend) # 交集
print(frontend ^ backend) # 对称差集
预期输出: Python 92 已学 {'Python', 'HTML', 'SQL'} {'Python'} {'HTML', 'SQL'}

上面的经纬度示例若直接运行,中文地名需要使用字符串引号:("成都", 30.67, 104.06)。这是一个值得保留的调试习惯:看到语法错误时,先检查成对出现的引号、括号和冒号。很多错误不是概念不会,而是符号少了一半。

可变性提醒:执行 alias = chapters 时,两个名字指向同一个列表;通过任意一个名字修改,另一个都能看到。需要独立副本时可用 chapters.copy() 或切片 chapters[:]。嵌套列表还涉及浅拷贝与深拷贝,后续遇到多层结构再专门学习。

选择容器时,先写出你要完成的操作,再选择数据结构,而不是反过来。若主要操作是“按照出现顺序逐个处理”,列表自然;若经常问“这个 ID 对应哪条记录”,字典更合适;若只关心某个标签是否出现,集合比列表更直接;若数据组成固定且不应被意外修改,元组能传达这种意图。合适的结构会让代码本身成为解释。

复杂数据通常是容器的组合,而不是新的神秘类型。一个文档记录可以是字典,字典里的 tags 是列表,metadata 又是一个字典;很多文档记录则放进列表。读这种数据时,沿着“外层是什么、键名是什么、值又是什么”的路径访问。每次只深入一层并打印小样本,比一次性打印整个对象更容易理解。

排序也有两个需要分清的概念:排序依据和排序方向。sorted(records, key=lambda item: item["score"], reverse=True) 表示按照记录里的分数降序生成新列表。lambda 在这里只是一个很短的函数;如果排序规则变复杂,应写成有名字的函数。清楚写出排序字段,能避免“默认按字典顺序”带来的意外。

字典和集合的查找通常很快,但速度不是唯一标准。集合会丢失顺序,字典的键必须可哈希,列表可以保留重复项。做数据预处理时,先确认这些语义要求,再考虑性能。一个运行很快但悄悄去掉重复、改变顺序的程序,结果仍然是不可信的。

CHECKPOINT 05容器选择练习

为一个学习记录设计数据结构:

  1. 课程名称、章节号、是否完成,放进一个字典。
  2. 本章的关键词,放进一个列表。
  3. 已经出现过的关键词,放进一个集合。
  4. 一个固定的经纬度坐标,放进一个元组。
record = {"course": "LLM", "chapter": 1, "done": False}
keywords = ["Python", "变量", "函数"]
seen = {"Python", "函数"}
location = ("成都", 30.67, 104.06)

1.5.4 列表的细节:索引、切片与排序策略

列表的下标从 0 开始,负数下标从末尾倒数。items[0] 取第一个元素,items[-1] 取最后一个元素;列表为空时,两者都会抛出 IndexError。如果你不确定列表是否有内容,先写 if items: 或检查长度,再访问具体位置。不要为了消除错误而随意填入一个默认元素,因为那可能掩盖了真正的空输入问题。

切片的形式是 sequence[start:stop:step],终点不包含在结果中。items[:3] 取前三项,items[::2] 每隔一项取一个,items[::-1] 产生倒序副本。切片会创建一个新的外层列表,修改切片通常不会改变原列表;但如果元素是嵌套容器,内部对象仍可能共享。写窗口、分页和批处理时,先明确“是否包含终点”和“是否需要复制”。

sort() 就地改变列表并返回 None,sorted() 返回一个新列表。两者都支持 key 和 reverse 参数。按字符串长度排序、按成绩排序、按字典字段排序,最好把规则写在 key 中,而不是先把数据改造成难以还原的临时格式。排序稳定性意味着相同键的元素会保持原先相对顺序,这在多字段排序中很有用。

PYTHON · 切片与 key 排序
records = [
    {"title": "Python", "score": 92},
    {"title": "向量", "score": 92},
    {"title": "数据", "score": 85},
]

ordered = sorted(records, key=lambda item: (-item["score"], item["title"]))
print(ordered)
print(records)  # 原列表没有被排序

1.5.5 字典的键、默认值和嵌套结构

字典用键找到值,键必须是可哈希的对象,常见的是字符串、数字和元组。键应该表达稳定身份,例如 document_id,不要把会变化的展示文字当作唯一键。重复赋值会覆盖旧值,因此处理外部数据时要先决定“重复键是更新、报错还是合并”。这个决定属于需求,不是字典自动替你完成的。

data["missing"] 在键不存在时抛出 KeyError;data.get("missing") 可以返回 None 或指定的默认值。默认值方便读取可选字段,但不要用它掩盖必填字段缺失。模型响应中,answer 缺失和 answer 为空字符串可能代表不同失败原因,必要时用 if "answer" not in data 区分。

PYTHON · get 与 setdefault
groups = {}
for topic, student in [("Python", "小林"), ("Python", "小周"), ("数学", "小林")]:
    groups.setdefault(topic, []).append(student)

print(groups)
print(groups.get("未开始", []))

嵌套字典和列表能表达 JSON 一样的层次,但访问链过长时可读性会快速下降。可以先把中间对象取出并命名,也可以写一个专门读取路径的函数。修改嵌套结构时尤其要注意原对象是否被多个名称共享。先画出数据形状,再决定循环层级和字段名称,通常比直接从一串括号开始写更稳。

1.5.6 元组、解包与不可变记录

元组适合表达位置有意义且不希望被随意改变的组合,例如坐标、数据库行或函数返回的固定多项结果。元组本身不可变,但如果其中包含列表,列表仍然可以改变。单元素元组必须写尾逗号:("Python",);没有逗号的圆括号只是分组。理解这两个细节,可以避免把元组和列表的语法混在一起。

解包让代码直接得到各个字段:title, score = ("Python", 92)。元素数量不匹配会抛出 ValueError;使用星号可以收集剩余项,例如 first, *middle, last = items。解包适合结构固定的数据,结构不稳定的 JSON 则应先验证键和长度。不要为了追求短代码而忽略输入形状。

1.5.7 集合:去重、成员判断和集合运算

集合只保存不重复的元素,常用于判断一个词是否已经出现、两个标签集合是否有交集。item in seen 通常比在长列表里搜索更适合成员判断,但集合不承诺你可以依赖某种展示顺序。需要保留出现顺序时,可以用列表记录顺序、集合负责判断,两个结构各司其职。

PYTHON · 集合关系
required = {"Python", "函数", "文件"}
completed = {"Python", "文件"}

print(required - completed)  # 尚未完成
print(required & completed) # 已完成交集
print("函数" in required)

集合元素必须可哈希,所以列表、字典等可变容器不能直接放进集合。需要对复杂记录去重时,可以提取稳定的 ID 或把字段组成元组。去重前要先定义“相同”的标准:标题大小写不同算不算同一条?空格不同算不算同一条?代码只会执行你写下的标准,不会自动理解业务语义。

1.5.8 推导式、嵌套容器与数据形状

字典推导式可以把一组记录转成索引,集合推导式可以快速去重,列表推导式可以完成简单筛选。面对嵌套数据时,先确认每一层是什么:外层是文档列表,文档是字典,字典的 tags 又是列表。循环变量名应体现层级,避免都叫 item 导致误取字段。形状清楚后,代码自然会变得清楚。

PYTHON · 从列表建立索引
documents = [
    {"id": "a1", "title": "Python 入门"},
    {"id": "b2", "title": "向量检索"},
]
by_id = {document["id"]: document for document in documents}
print(by_id["a1"]["title"])

推导式中的表达式不要承担副作用。调用网络、写文件、修改外部列表或打印日志都不适合隐藏在一对方括号中。需要逐步检查或处理异常时,普通 for 循环更好。简洁的标准是“读者一眼能说出它遍历什么、筛选什么、产生什么”,而不是“字符更少”。

1.5.9 复制、别名与嵌套数据的风险

使用 copy = original 只是创建别名,两个名称指向同一个对象。浅拷贝只复制外层容器,深拷贝会递归复制内部对象,但代价更高,也可能复制不该复制的资源。最稳妥的设计通常是保持输入数据不变,生成一个新结果;需要原地修改时,在函数名称或文档中说明。数据处理可追溯性比少创建一个列表更重要。

函数参数默认值尤其容易暴露可变对象陷阱。不要写 def collect(item, items=[]):,因为这个列表会在多次调用之间被共享。使用 None 作为默认值,在函数内部创建新列表,能够避免隐藏状态。这个例子虽然简单,却能帮助你理解为什么模型评测脚本不能把上一次运行的结果悄悄留到下一次。

1.5.10 容器选择与性能判断

列表适合按顺序追加和索引,字典适合按键查找,集合适合成员判断和去重,元组适合固定结构。不要因为某种结构“更高级”就到处使用它。先写清楚操作:我要保持顺序吗?需要重复吗?会频繁按键读取吗?需要修改吗?再选择容器。数据量变大后,合适的结构可能比微调循环更有效。

复杂对象的内存和序列化也要考虑。把一整份文档复制进多个字典,会增加内存和写入成本;保存 JSON 时,路径对象、集合和自定义类需要先转换成可表达的形式。学习阶段不必过早优化,但要知道“容器只是内存中的对象”,当数据要跨进程、跨机器或跨语言传递时,还需要明确格式和边界。

本节完成标准是:你能根据顺序、重复、键、可变性和访问方式选择容器;能解释切片和排序是否修改原数据;能处理缺失键和空集合;能画出嵌套数据的形状,并写一个最小索引。掌握这些后,函数参数和文件 JSON 的结构会更容易读懂。

DEEP DIVE 05学习资料索引设计

准备 5 条包含 id、title、tags 和 done 字段的学习资料。请分别用列表保存原始顺序、字典建立 ID 索引、集合收集所有标签;要求报告重复 ID、未完成资料和某个标签对应的资料标题。再故意加入缺失字段,设计不会直接崩溃的提示。

先定义数据形状和重复规则,再写索引;集合无法表达顺序,因此报告标题时要回到原列表或排序后的列表;读取字典字段时区分必填字段和可选字段。不要把所有查询写在一个大循环里,至少拆出建立索引和生成报告两个步骤。

1.5.11 列表、字典和集合的选择题

假设你要保存一批按照出现顺序展示的标题,使用列表;要通过唯一编号快速找到标题,使用字典;要判断某个标签是否出现过,使用集合;要保存一个固定的经纬度或不可变的配置键,使用元组。一个真实项目通常同时使用多种容器,而不是只选一种。先把每个操作写成动词,再选择最适合它的数据结构。

数据结构的选择还影响错误表现。列表下标不存在会得到 IndexError,字典键不存在会得到 KeyError,集合查找不会返回位置。不要把异常看成需要隐藏的噪声;它们提示你对数据形状的假设没有成立。边界层可以把异常转成说明,核心逻辑仍然应该保持假设清楚。

学习资料中常见“列表里套字典,字典里又套列表”。处理这类结构时,用变量名表达层级:document、tag、student,不要全部叫 item。先用一个样例打印 repr() 和类型,再写循环。看清数据形状后,很多看似复杂的下标会自然变成字段名和普通遍历。

1.5.12 用数据结构表达不变量

如果一组标题必须保持顺序并允许重复,列表表达了这个不变量;如果 ID 必须唯一,字典键表达了唯一性;如果标签不能重复,集合表达了去重。好的数据结构会把一部分规则交给语言本身,减少额外的 if。相反,把所有数据都存成字符串,再在每个函数里重新解析,会让规则分散、错误增加。

在写入 JSON 前,要把复杂对象转换成稳定、可读的结构;在读取 JSON 后,要恢复为函数约定的形状。转换函数应该写清“丢失了什么”:集合变成列表后不再保证顺序,元组变成数组后无法区分原始语义。数据跨格式传递时,显式记录这种变化,比追求一行转换代码更重要。

完成容器小节后,请拿自己保存的学习笔记做一次建模:每条笔记有哪些字段?哪些字段必填?哪些需要去重?报告时是否需要原顺序?把答案写在纸上,再选择列表、字典、集合和元组。这个练习把抽象语法连接到真实数据,也会为后续 Pandas 和向量数据库的学习打基础。

CONTAINER REVIEW 05数据形状说明书

选择一个你熟悉的学习资料目录,写出它的 JSON 形状(顶层类型、字段、字段类型、是否允许缺失)。然后用 Python 构造 3 条样例,分别完成按 ID 查找、按标签筛选、去除重复标题和保持原顺序四个操作。

先写数据形状再写代码;索引字典不应破坏原列表;标签集合负责判断,报告标题时回到列表;重复标题的判定规则要说明是否忽略大小写和首尾空格。样例中至少包含一个空标签列表和一个缺失可选字段。

1.5.13 容器小结:先画形状,再写访问

遇到未知 JSON 时,先打印或画出它的形状:顶层是列表还是字典,每个元素有哪些键,键对应的值是标量还是另一个容器。访问表达式应该沿着这张形状图走,而不是靠连续尝试下标。形状一旦改变,验证函数应尽早报告,避免错误深入到后面的业务逻辑。

容器选择还要考虑数据的生命周期。临时的中间列表可以自由重建;需要跨函数、跨文件保存的数据,应使用稳定字段和明确格式;需要快速查找的数据应建立索引,但保留原始顺序以便展示和追溯。一个项目同时保存“原始列表”和“按 ID 的字典”是正常的,只要说明谁是事实来源。

把本节的容器练习与词频项目连接起来:词序可以用列表,词频映射用字典,停用词用集合,排名记录用元组列表。四种结构协作后,代码表达的关系比把所有内容塞进一个字符串清楚得多。

数据结构不是一次性选定后永远不变的答案。需求、规模和访问方式改变时,列表可能需要变成字典索引,临时集合可能需要保留原顺序。先用小案例验证关系,再根据证据调整结构;能够解释调整原因,比记住某个容器的“最佳用法”更重要。

如果容器选择仍然犹豫,先用最小样例记录一次查找、一次修改和一次去重的成本与结果。用证据比较结构,远比背诵“列表适合什么、集合适合什么”更容易形成长期记忆。

06 · FUNCTIONS, CLASSES & ERRORS

函数、类、模块与异常:把代码变得可复用、可解释

当一个文件越来越长,最危险的不是行数本身,而是同一段逻辑被复制到很多位置。函数封装一组有名字的步骤,类把一组相关状态与行为放在同一个对象中,模块组织可复用定义,异常处理则让程序在遇到可预期的问题时给出清楚回应。以后读取 Dataset、编写 nn.Module 和组织模型实验时,这些能力会反复出现。

1.6.1 函数:输入、处理和返回值

定义函数时使用 def,参数是函数需要的输入,return 把结果交还给调用者。没有显式 return 的函数会返回 None。把“打印结果”和“返回结果”分开:打印适合给人看,返回适合让另一个函数继续处理。

PYTHON · 函数的输入与输出
def completion_rate(done, total):
    """返回完成比例;总数不合理时给出明确错误。"""
    if total <= 0:
        raise ValueError("total 必须大于 0")
    return done / total

rate = completion_rate(3, 8)
print(f"完成比例:{rate:.1%}")

函数最好只承担一个清晰职责。completion_rate 负责计算比例,不负责读取输入、打印三段提示、写文件和发送网络请求。职责单一并不意味着函数只能有一行,而是意味着你可以用一句话说清它的作用,并能单独测试它。

参数顺序要稳定,默认值要谨慎。不要使用可变对象作为默认参数,例如 def add_item(item, items=[]),因为这个列表会在多次调用之间被复用。初学阶段可以使用 None 作为默认值,再在函数内部创建真正的列表。

PYTHON · 默认参数的安全写法
def collect(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

first = collect("Python")
second = collect("数学")
print(first)   # ["Python"]
print(second)  # ["数学"]
表 6.1 · 函数接口在定义前先写清楚
接口问题应该说明什么示例未说明的风险
输入类型接受字符串、数字、列表还是字典text: str调用者传入 None 时错误位置不明确
输入范围是否允许空值、负数或超长文本limit >= 0边界值在深层逻辑才暴露
返回类型返回新对象、原对象还是 None-> list[str]调用者误把打印结果当数据继续处理
副作用是否写文件、修改输入、发请求或打印read_notes() 只读取测试依赖环境,重复调用产生隐藏变化
失败方式返回空结果、错误对象还是抛异常raise ValueError上层不知道应该重试、提示还是停止

1.6.2 作用域:函数内部的名字不应随意泄漏

函数内部创建的局部变量通常只在函数内部有效。外部变量可以被读取,但不建议依赖大量隐式的全局状态。把依赖关系放在参数里,把结果放在返回值里,函数会更容易理解和测试。看到一个函数时,先找它的参数和返回值,再看内部细节。

1.6.3 模块:用文件组织可复用代码

一个 .py 文件就是一个模块。使用 import 可以把标准库或自己写的模块引入当前程序。把运行示例放进 if __name__ == "__main__": 代码块,可以让文件既能被导入,又能直接执行。模块边界是学习工程化的第一步。

PYTHON · module_tools.py
def normalize_text(text):
    return " ".join(text.lower().split())


if __name__ == "__main__":
    sample = "  Hello   Python  "
    print(normalize_text(sample))
PYTHON · main.py
from module_tools import normalize_text

text = "  A small   language model note  "
print(normalize_text(text))

导入错误时,先检查文件名、当前工作目录和虚拟环境是否正确。不要把自己的文件命名为 random.py、json.py 或 typing.py,否则可能遮蔽标准库模块,产生很难理解的导入问题。

1.6.4 异常:错误是程序提供的信息

语法错误通常在程序运行前被发现;异常则发生在执行过程中,例如把文本转换为整数失败、访问不存在的字典键、打开不存在的文件。异常信息包含类型和上下文,应该先读懂再处理,而不是一看到错误就写一个宽泛的 except: 把它吞掉。

PYTHON · try / except / else / finally
raw = input("请输入一个整数:").strip()

try:
    number = int(raw)
except ValueError:
    print("输入不是有效整数")
else:
    print(f"两倍是 {number * 2}")
finally:
    print("本次转换结束")

只捕获你能够处理的异常类型。ValueError 表示值的形式不适合当前操作,FileNotFoundError 表示文件路径没有找到,KeyError 表示字典中没有这个键。若你不知道如何修复,就让异常保留完整信息;调试时完整 traceback 比“程序没反应”有价值。

表 6 · 常见异常与处理边界
异常类型典型场景可以怎样处理不要怎样处理
ValueErrorint("十八") 或数值超出约定范围提示格式或范围,并让用户重新输入不要把无效值默默改成 0
TypeError对象类型不支持当前操作检查输入契约,必要时在边界拒绝不要为了“能运行”强行拼接不同类型
FileNotFoundError打开不存在的文件提示路径、当前目录和可选的修复方式不要直接吞掉错误并返回假数据
KeyError字典缺少必填键区分必填字段与可选字段,给出字段名不要用空字符串掩盖数据格式问题
json.JSONDecodeErrorJSON 文本格式损坏保留原文和位置,提示重新导出或修复不要只显示“解析失败”而丢掉上下文

异常处理的位置也有讲究。把整个程序包在一个巨大的 try 里,虽然看起来“不会崩”,却会把真正出错的那一行藏起来。更好的做法是只包住可能失败的最小操作:转换数字时处理 ValueError,打开文件时处理 FileNotFoundError,解析 JSON 时处理对应的解码错误。错误信息越靠近问题发生处,越容易给用户可行动的提示。

raise 不是“让程序故意报错”,而是函数在发现调用者违反约定时主动发出信号。比如完成数不能小于 0,总数不能等于 0,文件路径不能是目录。把这些约束写出来,能让错误尽早出现,也能帮助未来的自己理解函数的输入边界。不要用沉默的错误值替代所有异常,否则错误会继续向后传播,最后变得更难追踪。

函数还可以返回多个值,Python 会把它们打包成元组。例如 return notes, message 让调用者同时得到数据和说明。与其让函数修改外部列表、设置全局变量、再打印一句“完成”,不如明确返回结果。纯粹一点的函数更容易用几组输入反复测试,也更方便以后接入命令行、网页或模型服务。

模块化的意义不只是把一个大文件切成几个小文件,而是让每个文件有可解释的边界。可以按“文本清洗”“统计”“文件读写”拆分;文件名反映职责,函数名反映动作。模块之间通过参数和返回值交流,少依赖隐含的全局变量。未来的大模型项目中,数据、提示词、模型调用和评估也可以沿着相同思路拆开。

设计函数时,可以先写一句“它接收什么,返回什么”。例如“接收一段文本,返回清洗后的词列表”;这句话同时给出了参数和结果。若一句话里出现“还要打印、保存、发送、修改外部状态”,说明职责可能太多。先写最小函数,再在调用层组合,通常比一开始设计一个“万能函数”更稳。

函数名应该使用动作或明确的判断,例如 read_notes()、normalize_text()、is_valid_score()。不要用 do_work()、handle_data() 这类无法预测内容的名字。命名越具体,代码阅读者越少需要打开函数内部确认它做什么;调试日志也会更有意义。

当函数需要很多参数时,可以把相关参数组合成一个字典或数据类,但不要为了“少写几个参数”而把所有东西塞进一个无结构字典。参数越多,调用方越容易把位置弄错;键名越随意,运行时越晚才暴露问题。先保持接口小而清楚,只有确实出现重复组合时再抽象。

CHECKPOINT 06函数与异常练习

写一个函数 safe_divide(a, b):

  1. 当 b 为 0 时,不让程序直接崩溃,返回一条清楚的提示。
  2. 正常情况下返回结果。
  3. 为它写至少三次调用:正常、除数为 0、输入类型不合理。
def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return "除数不能为 0"
    except TypeError:
        return "a 和 b 必须支持除法"

1.6.5 参数设计:让函数的边界可预测

函数参数是调用者和实现者之间的约定。位置参数适合含义明显且顺序固定的值,关键字参数适合可选配置,默认值适合“绝大多数调用都使用同一个选择”的情况。参数名称应表达单位和范围,例如 limit=10 比 n=10 更容易理解;如果参数是秒、字节或比例,最好在名称或文档中写出来。

默认值会在函数定义时计算,而不是每次调用时重新计算。使用当前时间、随机数或可变列表作为默认值,可能产生让人意外的共享状态。安全的模式是用 None 表示“没有提供”,进入函数后再创建对象。这个规则不仅适用于列表,也适用于字典、集合和自定义配置对象。

PYTHON · 参数和关键字参数
def summarize(text, *, limit=80, suffix="…"):
    text = text.strip()
    if len(text) <= limit:
        return text
    return text[:limit - len(suffix)] + suffix

print(summarize("一段很长的学习笔记", limit=7))

上例中的 * 表示后面的参数必须使用关键字传入。关键字参数能避免调用者把多个数字的位置写反,也让调用代码更像一句说明。不是所有函数都需要关键字限制,但当参数含义相近、默认行为复杂或未来可能增加选项时,显式命名会让接口更稳。

1.6.6 返回值、None 与副作用

函数可以打印结果,也可以返回结果。打印是面向人的副作用,返回是面向调用者的数据接口。一个负责计算的函数通常应该返回值,把展示交给外层;一个明确叫作 show_ 或 print_ 的函数才适合把输出作为主要目的。这样做可以让同一逻辑被测试、保存、排序或交给模型调用,而不必解析屏幕文字。

没有显式 return 的函数返回 None。如果调用者写成 result = items.sort(),得到的就是 None,因为排序方法原地修改列表而不返回新列表。遇到这种问题,先查方法的契约:它是否修改原对象,返回什么,失败时抛什么异常。不要看到“没有结果”就随意加一条打印。

函数最好只承担一个可以用一句话描述的职责。读取文件、清洗文本、统计次数和打印报告可以串起来,但不一定要放在一个函数里。拆开后每层可以用不同输入测试,也可以替换实现。例如把读取文本改成读取 API 响应,不需要重写词频统计部分。职责清楚是复用的前提。

1.6.7 文档字符串、类型提示和断言

文档字符串位于函数体第一行,用来说明目的、参数、返回值和限制。它不是给解释器看的注释,而是可以被 help()、编辑器和文档工具读取的接口说明。学习项目不必使用复杂格式,但至少写清输入类型、空值行为和一个例子。未来回看自己的代码时,这段文字往往比变量名更能提醒你当初的设计。

类型提示让函数的形状更直观,例如 def load_titles(path: str) -> list[str]:。它不会自动阻止错误输入,仍需在边界检查。断言 assert 适合表达程序内部必须成立的不变量,例如排序前结果不应为空;不应把断言当作面向用户的输入验证,因为优化模式下断言可能被关闭。

PYTHON · 文档字符串与返回契约
def ratio(done: int, total: int) -> float:
    """返回完成比例;total 必须大于 0。"""
    if total <= 0:
        raise ValueError("total 必须大于 0")
    return done / total

print(ratio(3, 4))

1.6.8 作用域、global 与闭包的边界

函数内部创建的名称默认只在函数内部可见,外部同名变量不会自动被覆盖。Python 查找名称通常遵循局部、外层函数、全局和内置的顺序。初学阶段尽量通过参数和返回值传递数据,少用 global 修改全局状态。全局状态会让函数的结果依赖调用顺序,测试和并行运行都会更困难。

嵌套函数可以访问外层函数的变量,形成闭包。这是有用的工具,但也会让数据的生命周期不明显。先把逻辑写成普通函数和显式参数,只有在需要保存少量配置或创建函数工厂时再学习闭包。看到 nonlocal 时,先问自己是否可以用一个对象或返回值表达同样关系,通常会更容易维护。

1.6.9 模块:导入的是名称和对象,不是“复制一段代码”

把代码保存成 text_utils.py 后,其他文件可以使用 import text_utils 或 from text_utils import clean。导入时 Python 会执行模块顶层代码一次,并把名称放进模块对象。顶层只定义函数和常量,不要在导入时就读取用户文件、发网络请求或打印大量内容。这样模块才能安全地被测试和复用。

PYTHON · 模块入口保护
# text_utils.py
def clean(text):
    return text.strip().lower()

def demo():
    print(clean("  Python  "))

if __name__ == "__main__":
    demo()

if __name__ == "__main__": 只在文件被直接运行时成立,被其他模块导入时不成立。它把“库的定义”和“命令行演示”分开。项目逐渐变大后,可以把可复用函数放进 src/,把启动入口放在单独文件;即使只做学习练习,也尽早保持这条边界。

导入错误常见于三种情况:文件名拼错,当前工作目录不在模块可搜索路径中,或模块内部又导入了不存在的依赖。排查时先看 traceback 最后一行,再打印 module.__file__ 确认实际导入的是哪份文件。电脑上有同名文件时,Python 可能导入错误版本,名称清楚和目录清楚都能减少这种风险。

1.6.10 异常层次:捕获你能够处理的错误

异常是运行时传递失败信息的一种机制。ValueError 表示值的形式或范围不合适,TypeError 表示对象类型不支持某个操作,FileNotFoundError 表示路径不存在,KeyError 表示字典中缺少键。捕获异常时尽量写具体类型,并只处理你知道如何恢复的情况。裸 except: 会把键盘中断、系统退出等信号也吞掉,排错会变得困难。

异常处理要有边界。读取一个可选文件时,可以捕获文件不存在并给出提示;解析模型返回的 JSON 时,可以捕获解析错误并保存原始响应;如果磁盘已满或权限不足,可能没有安全的自动恢复方式,应让错误明确暴露。不要用 try 包住整段程序后统一输出“失败”,那会丢失真正的上下文。

PYTHON · 捕获、转换与重新抛出
def parse_count(raw):
    try:
        value = int(raw)
    except ValueError as exc:
        raise ValueError("章节数必须是整数") from exc
    if value < 0:
        raise ValueError("章节数不能为负数")
    return value

try:
    print(parse_count("abc"))
except ValueError as error:
    print(f"输入有误:{error}")

1.6.11 finally、清理和可恢复性

finally 中的代码无论是否发生异常都会执行,适合释放资源、删除临时文件或关闭连接。但对文件和网络资源,优先使用已经封装好生命周期管理的 with。清理动作本身也可能失败,重要资源要记录结果,不要假设“执行 finally 就一定成功”。

是否重试要根据错误类型决定。网络暂时超时可能适合有限次数的指数退避;输入格式错误重试没有意义;权限错误需要用户修正配置。每次重试都应有上限,并记录尝试次数,避免异常处理反而制造无限循环。把“可恢复”和“不可恢复”写在函数契约里,调用者才能做出正确选择。

本节完成标准是:你能写出有清晰输入输出的函数,解释默认参数和作用域,使用模块入口保护,把可预期异常转成行动提示,并保留无法处理的错误上下文。函数不是把代码切碎的工具,而是把责任、状态和失败方式明确下来的边界。

DEEP DIVE 06可测试的资料清洗模块

创建 text_utils.py,写三个函数:清洗标题、统计非空标题、生成编号报告。另建 run_demo.py 导入它们。要求导入模块时不自动打印;空输入返回清楚的结果;当编号起点小于 1 时抛出 ValueError。为每个函数准备至少两个正常案例和两个边界案例。

把清洗、统计和展示分开;函数通过返回值传递数据,不依赖全局列表;入口保护只出现在演示或命令行文件中。测试时同时检查返回值、异常类型和异常消息是否能指导修复。

1.6.12 函数的测试与重构顺序

测试函数时,优先检查三件事:给定输入是否得到预期返回值,输入是否被意外修改,错误输入是否抛出正确的异常。一个函数可以先用普通 assert 测试,不必等学会完整测试框架。测试内容应围绕契约,而不是围绕内部实现;这样重构函数内部时,外部行为仍然有保护。

重构时一次只改变一个维度。先把打印改成返回值,再把重复代码抽成函数,然后把函数移动到模块,最后添加类型提示和日志。每一步都运行原有案例。如果一次同时改命名、路径、异常和数据结构,出现错误时很难判断原因。小步重构更适合学习,也更接近团队工作。

PYTHON · 用简单断言测试函数
def clean_title(text):
    if not isinstance(text, str):
        raise TypeError("标题必须是字符串")
    return " ".join(text.split())

assert clean_title("  Python   基础 ") == "Python 基础"
assert clean_title("") == ""

try:
    clean_title(None)
except TypeError:
    pass
else:
    raise AssertionError("应该拒绝 None")

1.6.13 模块搜索路径和命名冲突

Python 会在若干目录中寻找导入模块,当前项目目录通常在其中。若你把文件命名为 json.py、random.py 或 typing.py,可能遮蔽同名标准库,导致极难解释的导入错误。文件名应该描述你的业务,并避开常见标准库名称。看到异常时,用 module.__file__ 检查到底导入了哪一份文件。

模块顶层代码应该尽量轻量。定义常量、函数和类不会产生副作用;读文件、创建目录或调用网络则会改变环境。把这些动作放在函数里,并由明确的入口调用,能让导入安全、测试快速。这个原则对大模型 SDK 尤其重要,因为导入模块不应自动消耗额度或发送请求。

1.6.14 异常链与用户提示

使用 raise ... from exc 可以在保留底层原因的同时,给上层一个更贴近业务的错误。例如 JSON 解码失败可以转成“学习记录格式损坏”,并保留原始 JSONDecodeError 便于排查。不要把异常链完全丢掉,也不要把内部堆栈直接展示给普通用户。开发日志和用户提示可以是两种不同粒度。

异常消息应包含动作和对象:读取哪个文件、解析哪个字段、使用哪个参数。避免“出错了”“请重试”这种无法行动的提示。若可以修复,就告诉用户怎么修;若只能停止,就告诉用户应保留哪些日志并联系谁。清楚的错误消息是程序接口的一部分,而不是最后才补的文字。

函数、模块和异常的共同目标是控制复杂度:函数控制职责,模块控制边界,异常控制失败传播。完成本节后,尝试把词频统计项目拆成可单独导入的模块,并为每个公共函数写一句文档字符串。你会发现代码行数可能增加,但理解和修改的成本下降了。

FUNCTION REVIEW 06模块化与异常契约

把一个“读取 JSON 学习记录并生成摘要”的脚本拆成 storage.py、summary.py 和 main.py。定义文件不存在、JSON 格式错误、必填字段缺失三种异常路径;要求导入模块不打印、不读文件,命令行入口才负责展示提示。

每个模块只承担一种主要职责;底层异常在合适的位置转换为包含路径或字段名的消息;入口捕获可预期错误,未预期错误保留 traceback。先用小字典和临时文件测试,再接入真实学习记录。

1.6.15 函数与异常小结:让失败也有接口

一个好函数不仅说明成功时返回什么,也说明失败时如何表现。它可以返回空列表、返回带错误字段的结果,或抛出具体异常;三种方式没有绝对优劣,关键是调用者能否预测并正确处理。不要让同一个函数有时打印、有时返回、有时静默失败,这会让上层无法组成稳定流程。

在模块边界处,把底层错误转换成业务可读的信息;在最外层入口处,决定如何展示和退出;在开发日志中保留必要的异常链。这样既不会把内部堆栈直接暴露给用户,也不会为了友好提示而丢失排错证据。

重构函数时保留旧的输入输出案例,先让测试保护行为,再改变内部实现。你会逐渐发现,函数、模块和异常并不是三个孤立主题,而是一套管理复杂度和失败传播的工具。

1.6.16 参数展开、lambda 与函数式组合

官方教程还会介绍任意参数和小型匿名函数。*args 把多余的位置参数收集成元组,**kwargs 把多余的关键字参数收集成字典;调用时反过来用 * 和 ** 展开列表或字典。它们适合编写通用包装器,但初学时不要为了显得灵活而隐藏函数真正需要的输入。能明确写出参数名称时,显式参数通常更容易读和检查。

先看调用方的数据怎样进入函数

PYTHON · *args、**kwargs 与展开
def make_report(title, *topics, level="入门", **meta):
    return {
        "title": title,
        "topics": list(topics),
        "level": level,
        "meta": meta,
    }

topics = ["变量", "列表", "函数"]
info = {"author": "小林", "edition": 1}
report = make_report("Python 基础", *topics, level="基础", **info)
print(report)
预期输出:topics 会收到一个元组,meta 会收到一个字典;调用处的星号只是把已有容器拆开传入,并不会改变原列表。

lambda 是只有一个表达式的匿名函数,常出现在 sorted(key=...)、map() 或 filter() 的短规则中。它不适合放多步逻辑、异常处理或副作用。只要规则需要注释或命名,直接写成普通 def 函数,读者会更容易理解和测试。

PYTHON · lambda 只承担一个排序规则
words = ["模型", "Python", "数据", "API"]
by_length = sorted(words, key=lambda word: (len(word), word))
print(by_length)

把函数组合起来时,先确认每个函数的输入输出类型:清洗函数返回字符串,切分函数返回列表,统计函数返回字典,排序函数返回列表。若一个函数同时接受字符串、列表和字典,通常说明边界还没有想清楚。函数式写法的价值不是减少行数,而是让每一步的转换关系可见。

FUNCTION LAB 1.6.16设计一个可组合的摘要器

分别写 clean(text)、split_words(text)、count_words(words) 和 top_n(counts, n=5) 四个函数,再用一个 make_summary(text, n=5) 组合它们。要求每个函数只做一件事,空文本返回空结果,n 不是正整数时抛出 ValueError。最后把 top_n 的排序规则写成一段自然语言。

先确定每个函数的输入和返回类型,再写实现;排序可以先按次数降序、再按词语升序保证同频稳定;不要在底层函数里打印。测试至少包含空文本、重复词、大小写差异和 n 大于词语种类的情况。

1.6.17 自定义异常:把领域错误说清楚

当程序的失败原因已经超出通用的 ValueError,可以定义自己的异常类型。例如学习资料导入器需要区分“文件缺失”“格式损坏”和“字段不完整”,上层就可以针对不同情况给出不同提示。自定义异常不必一开始就大量使用;只有当调用者需要按领域原因分别处理时,它才值得成为一个有名字的接口。

PYTHON · 自定义异常与边界转换
class InvalidRecordError(ValueError):
    """学习记录缺少必填字段。"""


def read_title(record):
    if not isinstance(record, dict):
        raise InvalidRecordError("记录必须是字典")
    title = record.get("title")
    if not isinstance(title, str) or not title.strip():
        raise InvalidRecordError("记录缺少非空 title")
    return title.strip()


try:
    print(read_title({"done": True}))
except InvalidRecordError as error:
    print(f"资料格式有误:{error}")

定义异常时,优先继承一个合适的内置异常,让已有调用者仍能按父类捕获;异常消息写清对象和修复动作,不要只写“无效”。底层函数负责发现问题,最外层入口负责决定是跳过、重试还是停止。这样错误分类和业务决策彼此分离,模块也更容易复用。

ERROR LAB 1.6.17为导入器设计错误分类

定义 SourceMissingError、SourceFormatError 和 RecordValidationError 三个异常。让读取函数分别在路径不存在、JSON 不能解析、必填字段缺失时抛出它们;入口按异常类型输出不同的行动提示,并确认未预期的 PermissionError 仍然保留 traceback。

异常类表达“发生了哪种领域问题”,消息表达“哪个对象出了问题以及怎么修复”;不要在底层函数里直接打印或退出。测试每种异常的类型、消息和触发输入,再测试一条正常记录。

1.6.18 类与对象:读懂 Dataset 和 nn.Module 的最低前置知识

前面的字典可以保存一条学习记录,函数可以处理这条记录;当很多函数都围绕同一组状态工作,并且这组状态需要满足固定规则时,类(class)提供了另一种组织方式。类是对象的定义,对象是按照这个定义创建出来的实例。这个说法不是要求你把所有字典都改成类,而是帮助你看懂后续框架代码:dataset = NotesDataset(rows) 创建一个对象,len(dataset)、dataset[0] 和 model(batch) 则是在使用对象公开的行为。

__init__ 在对象创建时接收初始数据,self 表示当前正在操作的那个对象。写成 self.title = title,意思是把参数 title 保存为这个对象的属性;另一个对象拥有自己的属性,不会自动和它共享。self 不是特殊关键字,但它是 Python 社区统一使用的惯例,阅读方法定义时应把第一个参数理解为“调用这个方法的实例”。

PYTHON · learning_record.py
class LearningRecord:
    """保存一条已经验证过的学习记录。"""

    def __init__(self, title: str, minutes: int, done: bool = False):
        title = title.strip()
        if not title:
            raise ValueError("title 不能为空")
        if minutes < 0:
            raise ValueError("minutes 不能为负数")
        self.title = title
        self.minutes = minutes
        self.done = done

    def mark_done(self) -> None:
        self.done = True

    def summary(self) -> str:
        status = "已完成" if self.done else "进行中"
        return f"{self.title} · {self.minutes} 分钟 · {status}"


python_note = LearningRecord(" Python 类与对象 ", 35)
python_note.mark_done()
print(python_note.summary())
assert python_note.title == "Python 类与对象"
assert python_note.done is True

方法调用 python_note.mark_done() 可以读成 LearningRecord.mark_done(python_note):点号左边的对象会作为 self 传入。这个等价关系能解释为什么定义方法时要写 self,调用时却不用手动传它。方法可以修改对象状态,也可以只读取状态并返回新结果;名称应让调用者看出是否会修改,例如 mark_done() 明显会改变记录,而 summary() 只生成文字。

表 6.18 · 面向对象语法与后续 PyTorch 代码的对应
Python 概念最小含义后续会在哪里遇到新手易错
class定义对象能保存什么、能做什么class Model(nn.Module)把类名当成已经创建的对象
__init__接收配置并建立初始状态创建层、保存词表和数据行忘记调用父类初始化
self当前实例self.linear、self.rows漏写或误用全局变量
继承复用父类约定并补充行为Dataset、nn.Module只继承名字,没有实现约定方法
协议方法让内置语法知道如何使用对象__len__、__getitem__、__iter__返回类型或边界不稳定
组合一个对象把别的对象保存为成员模型包含 Linear、Embedding 等层为了复用一行代码建立过深继承

继承表达“遵守一种接口”,组合表达“内部使用另一个对象”

继承写成 class Child(Parent)。子类会获得父类已有的行为,也可以重写方法。后续定义 PyTorch 模型时,class TextClassifier(nn.Module) 的重点不是少写几行,而是让框架知道这个对象遵守 Module 的生命周期:子类先调用 super().__init__() 建立父类状态,再把层保存到 self,最后实现 forward() 描述一次前向计算。平时应优先使用组合;只有确实存在稳定的“是一种”关系和父类协议时才使用继承。

Dataset 的核心协议很小:__len__ 回答有多少条样本,__getitem__ 根据索引返回一条样本。Python 的双下划线方法又叫特殊方法,它们会被内置语法间接调用:len(dataset) 调用 dataset.__len__(),dataset[0] 调用 dataset.__getitem__(0)。通常使用公开语法,不需要手动调用双下划线方法。

PYTHON · dataset_protocol.py
class NotesDataset:
    def __init__(self, rows):
        self._rows = list(rows)  # 复制容器,避免外部追加改变长度

    def __len__(self):
        return len(self._rows)

    def __getitem__(self, index):
        row = self._rows[index]
        return row["text"], row["label"]

    def completed(self):
        for row in self._rows:
            if row.get("done", False):
                yield row


rows = [
    {"text": "Python 类", "label": 1, "done": True},
    {"text": "异常处理", "label": 0, "done": False},
]
dataset = NotesDataset(rows)
print(len(dataset), dataset[0])
print([row["text"] for row in dataset.completed()])
assert len(dataset) == 2
assert dataset[0] == ("Python 类", 1)

yield 会创建生成器:函数每次产生一个值后暂停,下次迭代再从原位置继续。它适合逐条处理数据,不必先把所有结果装进新列表。生成器通常只能按顺序消费;如果需要随机索引、重复遍历或提前知道长度,就要选择列表、Dataset 协议或能够重新创建的迭代器。看到 for batch in loader 时,可以把它理解为“反复向可迭代对象请求下一批”,而不是某种只属于深度学习的特殊语法。

图 6.18 · 从普通类走到 PyTorch 对象自制流程:先读对象协议,再读框架功能
普通对象属性 + 方法
↓
Dataset 协议len + getitem
↓
Module 协议层 + forward
↓
训练循环batch → model → loss

框架类并没有改变 Python 语法;它们只是约定了对象应提供哪些方法,以及框架何时调用这些方法。

读框架类时按四步走

第一步看父类,确认对象遵守哪个框架协议;第二步看 __init__,列出保存了哪些配置、层和数据;第三步看公开方法或 forward,追踪输入和返回形状;第四步才看训练循环如何使用对象。不要从装饰器、设备参数或复杂类型提示开始读,那些细节会遮住最基本的数据流。

类也可能被滥用。如果一个对象只是保存三个字段且没有行为,普通字典、命名元组或 dataclass 可能更简洁;如果方法互不相关,只是为了减少文件数量而放在同一个类里,它没有形成清楚职责;如果所有方法都依赖可随意修改的全局状态,对象外表并不会自动带来可靠性。选择类的理由应是状态、行为和不变量确实属于同一个概念。

可运行实验包:下载 大模型基础衔接实验包,运行其中的 labs/python_oop.py。它用标准库实现 Dataset 风格对象、Module 风格调用和生成器,不需要安装 PyTorch 就能先理解对象协议。
OOP LAB 1.6.18把学习记录改造成可验证对象

实现 LearningDataset:构造函数接收记录列表,__len__ 返回记录数,__getitem__ 返回标题与完成状态,completed() 使用 yield 逐条返回已完成记录。再实现一个 ScoreModule,通过 __call__ 转交给 forward(),根据分钟数和完成状态计算分数。

至少验证空数据集、索引越界、两条不同记录和一次生成器遍历;每个实例必须保存自己的状态。__getitem__ 的返回形状要稳定,forward() 只计算分数,不读取文件或打印。完成后用一句话解释为什么 PyTorch 要求模型继承 nn.Module、为什么实际调用通常写 model(x) 而不是直接调用 forward(x)。

07 · FILES & RELIABILITY

文件读写:让程序能够接触真实资料

到这里,代码已经能计算和组织内存中的数据,但学习大模型时,你还要读取教材摘录、笔记、日志和数据集。文件读写的核心不是记住几个方法,而是建立三个意识:路径要清楚,编码要明确,资源要及时关闭。

1.7.1 用 with 管理文件生命周期

open() 打开一个文件对象;模式 "r" 表示读取,"w" 表示覆盖写入,"a" 表示追加,"x" 表示只在文件不存在时创建。文本文件建议显式指定 encoding="utf-8"。使用 with 可以在代码块结束时自动关闭文件,即使中间发生异常也更安全。

PYTHON · 读取与写入文本
from pathlib import Path

path = Path("notes") / "python.txt"
path.parent.mkdir(exist_ok=True)

path.write_text("变量\n循环\n函数\n", encoding="utf-8")
text = path.read_text(encoding="utf-8")

lines = [line.strip() for line in text.splitlines() if line.strip()]
print(lines)

pathlib.Path 比手动拼接字符串更适合处理路径,因为它能在不同操作系统上使用合适的路径分隔符。read_text() 和 write_text() 适合简单文本;需要逐行处理大文件时,可以使用 open() 和循环,避免一次性把全部内容读进内存。

1.7.2 JSON:保存结构化的学习记录

JSON 适合保存字典、列表、字符串、数字、布尔值和空值组成的结构。它不是 Python 专属格式,很多 Web API 和数据工具都能读写。写入 JSON 时指定 ensure_ascii=False 可以保留中文,指定 indent 可以让文件更适合人工阅读。

PYTHON · JSON 学习记录
import json
from pathlib import Path

progress = {
    "chapter": 1,
    "title": "Python 基础",
    "completed": ["变量", "条件判断"],
}

Path("progress.json").write_text(
    json.dumps(progress, ensure_ascii=False, indent=2),
    encoding="utf-8",
)

loaded = json.loads(Path("progress.json").read_text(encoding="utf-8"))
print(loaded["completed"])

1.7.3 可靠读取:先检查,再处理,再报告

真实资料不一定干净:文件可能不存在,编码可能不对,某一行可能为空,用户输入可能不符合格式。可靠程序不会假设所有输入都完美,而会在边界处做检查。检查不要写成一大团条件;把“路径是否存在”“文件是否为空”“这一行是否可转换”拆成可理解的步骤。

PYTHON · 读取时处理常见错误
from pathlib import Path

def read_notes(filename):
    path = Path(filename)
    if not path.exists():
        return [], f"找不到文件:{path}"

    try:
        text = path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return [], "文件不是 UTF-8 编码,暂时无法读取"

    notes = [line.strip() for line in text.splitlines() if line.strip()]
    return notes, f"读取到 {len(notes)} 条记录"

notes, message = read_notes("python.txt")
print(message)

文件路径有“相对路径”和“绝对路径”之分。相对路径通常相对于程序启动时的当前工作目录,而不是相对于脚本文件本身;这就是为什么你在编辑器里运行成功,换到终端却找不到文件。排查时可以打印 Path.cwd() 看当前目录,也可以让程序接收明确的输入路径。不要把自己的电脑路径硬编码进给别人使用的示例。

文本文件的编码问题在中文资料中尤其常见。UTF-8 是当前最通用的选择,但旧文件可能来自其他编码。遇到乱码时,先确认文件实际编码和读取时指定的编码;不要用“多试几个编码直到不报错”代替判断,因为错误解码有时不会报错,只会悄悄产生错误字符。保存原始文件、记录转换方式,才能在后续处理中追溯来源。

一次性读取全文适合小型笔记,处理大型语料时要考虑内存。用 for line in file 逐行处理,或使用生成器,可以让程序在需要时才取得下一段数据。以后学习数据集和模型训练时,“批量读取”“分块处理”“不要重复复制大对象”都会成为重要的工程意识。现在先把输入规模写在笔记里,就已经是在做有价值的性能判断。

写文件时还要考虑覆盖风险。"w" 会清空旧内容,调试阶段可以优先输出到新的临时文件,确认结果后再替换正式文件。对于重要数据,保留原始输入和带日期的输出文件;对于可重复生成的结果,记录生成脚本和参数。数据处理不是“跑完就算”,而是要能说明结果从哪里来、如何重新得到。

文件读写还涉及换行符、空行和最后一行是否带换行的问题。多数学习任务不必手动处理所有平台差异,但使用 splitlines() 通常比直接按 "\n" 分割更稳妥。输出多行文本时,明确是否需要在末尾加换行,避免重复运行后出现空行越来越多的情况。

JSON 也有边界:它不能直接保存 Python 的集合、元组、路径对象或自定义实例。保存前要把它们转换成字符串、列表或字典,读取后再恢复成程序需要的形状。理解“文件格式能表达什么”很重要,因为模型 API 返回的结构化数据最终也常常需要落到 JSON 文件中。

处理学习资料时,文件内容可能包含空白行、标题行、注释行或不同格式的日期。先明确哪些内容属于数据,哪些内容只是排版,再写过滤规则。不要把所有空白都删除后才发现段落边界丢失;也不要把文件名、创建时间和正文混在同一个字符串里。数据清洗的第一步永远是描述原始数据的形状。

如果程序会反复运行,给输出文件选择可预测的路径,并在 README 中写清“运行前会不会覆盖已有结果”。需要保留多个版本时,可以在文件名加入日期或实验编号;需要机器读取时,则把元数据写进 JSON。清晰的输出约定会让你更容易比较两次运行的差异,也为下一章的数据可视化准备好材料。

CHECKPOINT 07文件读写练习

创建一个 questions.txt,每行保存一个你对大模型的疑问,然后写程序:

  1. 读取文件并删除空行。
  2. 给每条疑问加上编号。
  3. 把结果写入 questions-numbered.txt。
  4. 文件不存在时,输出提示而不是 traceback。
先用 Path.exists() 检查;读取后用 splitlines() 得到列表,用 enumerate(..., start=1) 编号,再用 "\n".join(lines) 写回。把“读取”和“格式化”分别写成函数,会比全部塞进主程序更容易调试。

1.7.4 路径不是字符串:用 Path 描述文件位置

路径包含目录层级、文件名和扩展名,但它不是普通文本那么简单。不同系统使用不同的分隔符,用户目录和盘符也不同。pathlib.Path 把路径当作对象,让你可以使用 / 拼接、.name 取文件名、.suffix 取扩展名、.parent 取上级目录。这样写比手动拼接反斜杠更容易跨平台。

PYTHON · Path 的基本操作
from pathlib import Path

root = Path("llm-learning")
source = root / "data" / "notes.txt"

print(source)
print(source.name)
print(source.suffix)
print(source.parent)

相对路径依赖当前工作目录,绝对路径从盘符或根目录开始。学习代码优先使用相对路径,并在 README 说明从哪里启动;需要处理脚本旁边的固定资源时,可根据 Path(__file__).resolve().parent 构造基准目录。不要把自己电脑中的绝对路径硬编码进示例,也不要把路径中的用户名提交到公开仓库。

exists() 只能说明路径当前存在,不能说明它是文件、目录还是你有权限读取。分别使用 is_file() 和 is_dir(),并考虑路径在检查后被其他程序移动的情况。检查与打开之间仍可能发生竞态,因此最可靠的方式是在打开时捕获具体异常;预先检查更多是为了提供友好的提示。

1.7.5 文本编码、换行和 BOM

中文学习资料建议统一使用 UTF-8,并在读写时显式写 encoding="utf-8"。Windows 某些工具会生成带 BOM 的 UTF-8 文件,读取时可能把文件开头的 BOM 当作隐藏字符;遇到首个字段异常时,可以确认编码是否为 utf-8-sig。不要盲目把所有文件都换编码,先保留原始文件和转换记录。

换行符在 Windows、macOS 和 Linux 上可能不同。Path.read_text() 读取后,使用 splitlines() 通常比按 "\n" 切分更稳妥;写出多行内容时,用 "\n".join(lines) 并在需要时补上最后的换行。对模型训练语料而言,换行可能代表段落边界,清洗前要先确认它是否是有意义的信息。

空白字符不只有普通空格,还可能包含制表符、不换行空格和全角空格。strip() 会处理许多首尾空白,但不会自动判断正文内部的格式。学习资料清洗时,先用可视化调试或打印 repr(line) 查看真实字符,再决定替换规则。把所有空白压成一个空格,可能破坏代码块、表格和段落结构。

1.7.6 CSV、JSON 与数据形状

CSV 适合表格数据,JSON 适合层次结构。读 CSV 时不要手动用逗号分割,因为字段本身可能包含逗号、引号或换行;应使用标准库 csv。JSON 可以表达字典和列表,但键必须是字符串,不能直接保存集合、路径和自定义对象。选择格式前先描述数据形状,格式只是表达这种形状的载体。

PYTHON · 使用 csv 标准库
import csv
from io import StringIO

content = "title,done\nPython,true\n函数,false\n"
rows = list(csv.DictReader(StringIO(content)))

for row in rows:
    print(row["title"], row["done"])
表 7 · 文件格式与 Python 工具选择
格式数据形状推荐工具适合的学习资料场景注意事项
纯文本 .txt连续文字或逐行记录Path.read_text()
open()
课程笔记、问题清单、日志明确编码和换行;大文件不要一次性读入
CSV .csv行与列组成的表格csv.DictReader
csv.DictWriter
题目清单、评测结果、词频排名不要手动按逗号切分,字段可能包含引号或逗号
JSON .json字典、列表和嵌套结构json.load()
json.dump()
学习进度、配置、模型响应键通常是字符串;保存前确认数据可序列化
二进制字节序列或专用格式open(..., "rb")
对应格式解析库
图片、压缩包、模型权重不要当作 UTF-8 文本读取,先确认文件格式

从 JSON 读取后,先验证顶层类型和必填字段,再交给业务函数。不要假设外部文件永远符合你写下的示例;字段可能缺失、类型可能变化、数组可能为空。可以写一个小的 validate_record() 函数,把格式错误转换成包含字段名和行号的提示。验证越靠近输入边界,后面的核心逻辑越简单。

1.7.7 大文件:流式读取和分块处理

一次性 read_text() 很适合小笔记,但把几 GB 的语料全部读进内存会导致交换、变慢甚至崩溃。with path.open(encoding="utf-8") as file: 后逐行遍历,解释器会按需读取。需要固定大小的块时,可以调用 file.read(size),并处理块边界处被拆开的多字节字符或一条记录。

PYTHON · 逐行统计资料
from pathlib import Path

path = Path("notes.txt")
non_empty = 0

with path.open(encoding="utf-8") as file:
    for line in file:
        if line.strip():
            non_empty += 1

print(f"非空行:{non_empty}")

流式处理的代价是不能随时回到之前的内容,也不能直接知道总长度。需要进度显示时,可以先获取文件大小或分阶段统计;需要随机访问时,再考虑索引或数据库。不要为了“看起来高效”把所有代码都写成生成器,先根据数据规模和下游需求做选择。

1.7.8 写入安全:覆盖、临时文件与原子替换

write_text() 默认会覆盖旧文件,重复运行可能悄悄丢失结果。学习时可以输出到带实验编号的新文件,或在写入前明确提示;重要资料则应保留原始文件、生成备份并记录时间。更可靠的做法是先写临时文件,确认成功后再用 Path.replace() 替换目标,避免程序写到一半时留下损坏文件。

写文件失败可能来自权限、磁盘空间、路径不存在或文件被占用。捕获异常时,把目标路径和操作包含在消息中,例如“保存 progress.json 失败”,不要只输出“失败”。如果错误无法自动恢复,就让程序返回非零状态或抛出异常,方便调用者和自动化工具发现问题。静默失败比程序停止更危险。

1.7.9 文件权限、敏感信息和可追溯性

学习资料可能包含个人信息、课程账号、API 密钥或未公开数据。不要把敏感文件和代码放在同一个公开目录,也不要把密钥写进 JSON 示例。可以使用环境变量、操作系统凭据存储或本地未提交的配置文件;代码只读取变量名,README 说明如何准备它。即使项目只是个人练习,也应尽早区分“可公开输入”和“私密输入”。

输出文件应包含足够的元数据:来源文件、生成时间、脚本版本、清洗规则和参数。可以在 JSON 顶层添加 meta 字段,也可以在旁边保存 README。这样当结果被质疑时,你能回到原始输入和生成步骤,而不是只剩一份无法解释的最终文本。可追溯性是数据处理可信度的一部分。

1.7.10 文件发现、排序和重复运行

Path.glob("*.txt") 可以发现匹配文件,但文件系统返回的顺序不应被当作业务顺序。需要稳定输出时,使用 sorted() 排序,并明确按文件名、修改时间还是内容 ID 排序。批量处理前先打印将要处理的文件列表,确认没有把输出目录、临时文件或隐藏文件误当作输入。

重复运行时,尽量让程序具备幂等性:同一输入和同一参数应产生相同结果,或明确追加而不重复。给输出文件写实验编号、用清单记录处理过的输入,能够避免一次重跑把结果翻倍。大模型数据管道也需要这个意识,否则重复切分、重复嵌入和重复评估都会浪费时间与费用。

本节完成标准是:你能用 Path 构造跨平台路径,能以 UTF-8 安全读写文本,能区分 JSON 和 CSV 的数据形状,能为大文件选择逐行处理,能解释覆盖写入和临时文件的风险,并能为输入、输出和敏感信息建立清楚的目录边界。

DEEP DIVE 07可追溯的学习资料导入器

建立 data/、outputs/ 和 logs/ 三个目录。写程序扫描 data/ 下的文本文件,逐行去除空白但保留段落边界,把每个文件的非空行数和来源路径写入 JSON。要求输入目录不存在、文件编码错误和输出失败时都有清楚提示;再运行两次,确认不会把同一文件重复追加。

使用 Path.glob() 后排序;读取时显式指定编码并在边界捕获异常;输出结构中保留生成时间、源文件名和参数;重复运行采用覆盖或按文件 ID 更新,不能无条件 append。先用三个很小的样例文件验证,再扩大输入规模。

1.7.11 用目录和清单管理一批文件

批量导入资料时,先获取文件清单,再过滤扩展名、排序和打印数量,最后才开始读取。把“发现文件”和“处理文件”分开,能够在正式运行前发现路径写错、输出目录被误选或隐藏文件混入。对每个文件记录成功、跳过或失败状态,最终汇总而不是只显示最后一条消息。

PYTHON · 批量文件清单
from pathlib import Path

data_dir = Path("data")
sources = sorted(path for path in data_dir.glob("*.txt") if path.is_file())

print(f"发现 {len(sources)} 个文本文件")
for source in sources:
    print(source.name)

文件名不是可靠的业务 ID。重命名、大小写差异和同名目录都可能造成冲突。若资料要长期保存,给每份内容生成稳定 ID,或使用数据库记录来源;文件名只作为展示。初学阶段可以先用相对路径和文件名,等需求变复杂时再学习哈希、数据库和对象存储。

1.7.12 日期、大小和元数据

Path.stat() 可以取得文件大小和修改时间,但不同系统的时间精度和时区处理可能不同。展示时间前明确使用本地时间还是 UTC;比较实验结果时更依赖脚本版本和参数,而不是单看修改时间。输出元数据时,最好同时保存来源路径、文件大小和读取成功的记录数。

文件大小是字节数,不是字符数。中文 UTF-8 一个字符可能占多个字节,压缩文件和二进制模型更不能用文本方式读取。先根据扩展名和格式决定读写模式:文本用 "r" 和编码,二进制用 "rb"、"wb"。把二进制内容当字符串读取,通常会得到解码错误或损坏数据。

1.7.13 可靠导入的四步流水线

一条可靠的文件流水线可以拆成“发现—验证—转换—保存”。发现阶段得到候选文件;验证阶段检查类型、大小、编码和必填字段;转换阶段生成统一的数据结构;保存阶段写入临时文件并记录统计。每一步都返回可观察的数量和错误列表,最终报告才不会把失败的文件悄悄算进成功数。

验证失败时,保留原文件并把文件名、行号、字段名写入失败报告。不要在失败后自动删除原始资料,也不要把修复后的文件覆盖原文件。学习阶段可以把失败样例放入单独目录,作为回归测试;每次修改清洗规则都重新运行,确认旧问题确实解决且没有引入新问题。

如果程序需要读取用户提供的路径,防止路径穿越也很重要。不要无条件拼接用户输入后删除或覆盖文件;先限制输入目录,解析路径后确认它仍位于允许的根目录下。基础课程不要求你实现完整的安全框架,但要知道“文件路径来自外部输入”时,读写权限和范围都需要检查。

1.7.14 文件处理与大模型准备

大模型应用常要把 PDF、网页、Markdown、CSV 或数据库内容变成纯文本。不同格式的解析器会有不同失败模式,统一入口应保留来源和页码、行号或 URL。先用纯文本文件练习路径和编码,再逐步加入格式解析库;不要因为某个库“一行就能解析”而忽略版权、脚本内容、表格结构和图片文字等边界。

切分长文档时,保留标题、段落和来源信息,避免只保存孤立的片段。以后做向量检索时,命中片段需要能回到原文;如果清洗阶段丢掉文件名和位置,检索结果就无法解释。文件读写小节的重点因此不只是“打开文件”,而是建立从原始资料到可追溯中间结果的链路。

文件相关能力可以通过一个很小的验收标准判断:你能在不看答案的情况下读取 UTF-8 文本,处理空文件和不存在路径,保存可再次读取的 JSON,说明覆盖风险,按行处理大文件,并在输出中保留来源和参数。达到这个标准后,再学习 Pandas 或文档解析库会更有把握。

1.7.15 文件小结:把资料处理成可追溯证据

文件处理的最终产物不应只有一个数字或一张排名表,还应包含来源、参数、时间和失败清单。这样当结果异常时,你可以回到原文件和具体规则,而不是重新猜测当时做过什么。把元数据写入 JSON 或 README,是成本很低却价值很高的习惯。

本节也为词频项目准备了工程边界:输入和输出目录分开,原始文件只读保存,结果可以重复生成,错误文件单独记录。以后把文本换成 PDF、网页或模型响应时,仍然先遵守这四条边界,再考虑引入更复杂的解析库。

如果你的程序能在小文件和大文件之间选择合适的读取方式,能解释编码和路径问题,能在写入失败时保留原始资料并给出行动提示,就已经具备处理真实学习资料的基本能力。

文件处理的核心验收可以再浓缩为一句话:任何一个输出,都能回答“来自哪个文件、经过什么规则、用什么参数、是否完整”。如果还回答不了,就先补元数据和失败清单,再继续增加解析功能。

每个文件导入任务都应有一个小的“干运行”模式:只列出将要处理的路径和预计输出,不真正写文件。确认清单正确后再执行写入,可以避免路径写错时覆盖资料,也是批量模型任务常用的安全习惯。

1.7.16 输出格式:给人看的文字与给程序看的表示

官方教程在输入输出部分同时介绍格式化字符串、str.format() 和 repr()。它们解决的问题不同:f-string 适合把少量值嵌入句子,format() 适合已有模板或需要重复使用的格式规则,repr() 更接近对象的调试表示,能把换行、引号和不可见空格显示出来。不要用面向人的句子让下游程序再做字符串切片;需要机器读取时,优先保存 JSON、CSV 或明确的结构。

同一个值的三种展示方式

PYTHON · f-string、format 与 repr
name = "小林"
rate = 0.875
raw_line = "  Python\n"

print(f"{name} 已完成 {rate:.1%}")
print("{} 已完成 {:.1%}".format(name, rate))
print(repr(raw_line))
预期输出: 小林 已完成 87.5% 小林 已完成 87.5% ' Python\n'

最后一行的 repr() 把首尾空格和换行转义显示出来,适合定位“看不见的字符”。真实报告中可以用中文句子,调试日志中则保留字段名和结构,二者不要混为一种输出。

格式化数字时要记录单位和舍入规则。{rate:.1%} 把 0.875 展示为 87.5%,但原变量仍然是浮点数;{count:,} 可以给大整数加千位分隔符;日期、时区、字节数也应在字段名或说明中写清。显示层可以友好,数据层必须保持可计算和可验证。

写日志时,推荐使用稳定的键值格式,例如 status=ok file=notes.txt lines=18,而不是只写“处理完成”。当任务失败时,输出文件名、操作、异常类型和下一步;当任务成功时,输出处理数量和参数。可读日志既方便人排查,也方便以后用脚本再次统计。

OUTPUT LAB 1.7.16生成一份双层报告

读取一份文本,生成两种结果:终端摘要包含文件名、非空行数和字符数;JSON 文件包含 source、stats、generated_at 和 rules。故意在文本首尾加入空格和空行,用 repr() 检查清洗前后的差异,再验证 JSON 能被下一次程序重新读取。

终端摘要可以使用 f-string,JSON 不要把整段摘要当作唯一数据;把统计值保存为数字,把规则保存为字符串或列表。生成时间要明确时区,输入路径要保留相对路径或标准化后的路径,方便结果追溯。

写入前先做“干运行”,写入后再做回读

批量处理资料时,最危险的错误往往不是语法错误,而是把输出写到了错误目录,或者覆盖了原始文件。可以先提供一个只展示计划、不写入磁盘的步骤:列出将要读取的文件、预计生成的字段和目标路径;确认清单正确后再执行真正的写入。写入结束后,用同一个编码回读 JSON,并检查关键字段是否存在。这样把风险拆成“路径正确”“内容正确”“格式正确”三次检查。

PYTHON · dry_run_then_read.py
from pathlib import Path
import json

source = Path("notes.txt")
target = Path("output/report.json")

print("将读取:", source.resolve())
print("将写入:", target.resolve())

if source.is_file():
    text = source.read_text(encoding="utf-8")
    report = {"source": str(source), "characters": len(text)}
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(
        json.dumps(report, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    checked = json.loads(target.read_text(encoding="utf-8"))
    print("回读成功:", checked["characters"])
else:
    print("干运行发现输入不存在,未创建输出文件")

注意这里用 is_file() 做了明确判断,但仍然保留异常处理的必要性:检查和真正读取之间可能发生权限变化,写入也可能遇到磁盘空间不足。干运行不能替代异常处理,它的作用是让路径和计划先被人看见。把原文件、输出文件和失败清单分开,今后接入网页抓取、PDF 解析或模型响应时也能沿用这条安全边界。

08 · CAPSTONE PROJECT

综合项目:做一个词频统计器

现在把前面学过的内容串起来:输入一段文本,清洗它,切分成词,统计每个词出现的次数,最后输出排名。这个项目看起来简单,却包含了大模型学习中反复出现的完整骨架:读取数据、规范化、遍历、聚合、排序、输出。

项目目标

完成后,你应该能够解释每一行代码为什么存在,并能在不看答案的情况下改出一个“统计字符”或“统计中文词语”的版本。

INPUT文本文件或多行字符串
PROCESS清洗 · 切分 · 计数 · 排序
OUTPUT出现次数最多的 10 个词
PYTHON · word_frequency.py
from collections import Counter
from pathlib import Path
import re


def clean_and_split(text):
    """把文本转成适合统计的英文小写词列表。"""
    text = text.lower()
    words = re.findall(r"[a-z]+(?:'[a-z]+)?", text)
    return words


def count_words(text, limit=10):
    words = clean_and_split(text)
    counts = Counter(words)
    return counts.most_common(limit)


def main():
    source = Path("sample.txt")
    if not source.exists():
        print(f"找不到输入文件:{source}")
        return

    text = source.read_text(encoding="utf-8")
    ranking = count_words(text)

    if not ranking:
        print("文件中没有找到英文单词")
        return

    print("词频排名")
    for index, (word, count) in enumerate(ranking, start=1):
        print(f"{index:02d}. {word:<15} {count:>4} 次")


if __name__ == "__main__":
    main()
sample.txt: Python is readable. Python is useful for data. Readable code is useful code. 运行: python word_frequency.py 输出示例: 词频排名 01. code 2 次 02. is 2 次 03. python 2 次 04. readable 2 次 05. useful 2 次

1.8.1 先拆需求,再读代码

第一步是定义输入和输出。这里的输入是 sample.txt,输出是一个按次数从高到低排列的列表。第二步是拆中间步骤:读取文本、统一大小写、用正则表达式找到英文单词、使用 Counter 聚合次数、调用 most_common() 排序。每一步都能独立测试,出了问题就能知道是哪一层。

clean_and_split() 只负责清洗和切分,所以它没有读取文件,也没有打印结果。count_words() 只负责统计,main() 负责把程序步骤串起来。这就是函数设计带来的价值:需求变化时,你可以替换某一层,而不用重写全部程序。

参考实现:把读取、统计和保存分成三种职责

下面的版本刻意不追求“最短代码”,而是让每个函数只做一件能说清楚的事。read_titles() 处理文件和清洗;count_keywords() 处理大小写策略和关键词命中;save_json() 只负责建立输出目录并写入 JSON。重复标题的规则也单独写在组装报告的地方,之后想改成“重复次数”或“重复行号”时,不必把文件读取逻辑一起改掉。

PYTHON · three_steps.py
from collections import Counter
import json
from pathlib import Path

def read_titles(path):
    text = path.read_text(encoding="utf-8")
    return [line.strip() for line in text.splitlines() if line.strip()]

def count_keywords(titles, keywords):
    counts = Counter()
    for title in titles:
        normalized = title.casefold()
        for keyword in keywords:
            if keyword.casefold() in normalized:
                counts[keyword] += 1
    return dict(counts)

def save_json(path, data):
    path.parent.mkdir(parents=True, exist_ok=True)
    content = json.dumps(data, ensure_ascii=False, indent=2)
    path.write_text(content, encoding="utf-8")

source = Path("titles.txt")
titles = read_titles(source)
report = {
    "titles": titles,
    "duplicates": [title for title, count in Counter(titles).items()
                   if count > 1],
    "keywords": count_keywords(titles, ["Python", "模型", "数据"]),
}
save_json(Path("output/titles.json"), report)
print(f"已保存 {len(titles)} 条标题")
示例输出:已保存 6 条标题。再次打开 output/titles.json 时,文件仍然是标准 JSON,而不是只能给人看的终端文字。

关键词统计在这里采用“一个标题对一个关键词最多记一次”的规则:标题里出现两次“Python”,仍算一个 Python 标题。如果需求是统计出现总次数,就要把条件改成 normalized.count(keyword.casefold()),并在报告字段名中明确写成“occurrences”。casefold() 比只对英文做 lower() 更适合一般文本,但它同样不会替你完成中文分词。规则写在函数里、字段名里和 README 里,结果才不会被误读。

1.8.2 逐行理解几个关键点

from collections import Counter 引入标准库里的计数工具;它本质上像一个字典,但专门为“元素 → 次数”这种任务提供了更清晰的方法。re.findall() 根据模式找到所有符合条件的英文词;正则表达式不是魔法,只是一种描述文本模式的语言。当前版本为了保持入门重点,只统计拉丁字母词,中文分词会在后续 NLP 章节单独处理。

counts.most_common(limit) 返回按次数排序的前若干项。enumerate(ranking, start=1) 为每一项提供从 1 开始的序号。f-string 里的 {index:02d} 把数字格式化成两位,{word:<15} 左对齐文本,{count:>4} 右对齐数字。格式化不是必须,但能让输出更容易阅读。

正则表达式的模式 [a-z]+(?:'[a-z]+)? 是一个有意的取舍。它能把 model、models 和 don't 识别为词,但不会自动理解中文分词、连字符或专业缩写。学习项目最重要的不是假装解决所有语言问题,而是明确当前版本支持什么、不支持什么。一个边界写在说明里的程序,比一个“看起来万能”却结果不稳定的程序更可信。

统计结果还需要经过人工抽查。拿一段很短的文本,自己数一遍,再和程序输出比较;故意加入大小写、重复空格、标点和空文件,观察结果是否符合预期。测试不需要一开始就使用复杂框架,几个清楚的输入和预期输出就能发现很多逻辑错误。等项目变大,再把这些案例整理成自动化测试。

项目中的 main() 入口让“被导入”和“直接运行”两种场景分开。别人导入 count_words 时,不应该自动读取你电脑上的 sample.txt 并打印一大段结果;只有直接执行文件时,才调用 main()。这是模块化的一个小但关键的习惯,后面把代码接入 Notebook 或 Web API 时会明显省力。

如果要让项目支持中文,不能只把正则里的 a-z 换成一个看似神奇的字符范围。中文文本通常需要分词工具,英文和中文还可能混合出现,数字、链接、医学缩写也有不同处理策略。下一章学习数据处理时,可以先把“字符统计”和“按行统计”做出来,再在 NLP 章节学习真正的分词。循序渐进比一开始引入很多库更适合建立基础。

最后给程序写一段简短的 README:它解决什么问题、输入文件放在哪里、使用哪个 Python 版本、如何运行、输出代表什么、有哪些已知限制。README 不是项目结束后的装饰,而是你检验自己是否真正理解的工具。如果连运行步骤都写不清楚,往往说明目录、输入和输出之间的关系还需要再梳理一次。

项目验收时可以采用“先猜、再跑、后解释”的顺序。先看输入文本,猜测最常见的词和次数;运行程序,确认输出;最后逐行解释为什么结果如此。若结果和猜测不同,不要立刻修改代码,先判断是你的预期错了、清洗规则不同,还是统计逻辑有 bug。这个顺序能训练你用证据而不是直觉调试。

一个项目完成后,最好保留一份失败版本的记录。例如第一次可能忘记统一大小写,导致 Python 和 python 被分成两个词;第二次可能把空文件当成有数据,输出一个误导性的排名。记录这些失败案例,会让你在后续处理更复杂的文档、日志和模型响应时,主动想到相同的边界情况。

词频统计并不是为了把一个玩具项目包装成大模型。它真正提供的是数据处理的基本肌肉:先定义数据,再清洗数据,之后聚合和验证,最后把限制写清楚。未来做分词、向量检索或模型评测时,工具会更复杂,但这条思考链不会改变。

把项目与大模型联系起来时,可以先观察“统计”这个动作。词频统计关心某个词出现了多少次;分类模型关心一个输入属于哪个类别;语言模型关心在上下文中下一个 Token 的概率。它们都需要从输入中提取表示、计算某种结果、再用结果做判断。现在把基础流程做扎实,后面学习概率和向量时会有一个具体参照,而不会只面对抽象公式。

项目还可以成为你的第一个实验记录。每次只改一个变量:是否统一大小写、是否过滤停用词、排名取前 5 还是前 10。记录改动、输入和输出,比较变化来自哪里。这样的单变量实验习惯会直接迁移到模型训练:改变学习率、批大小或数据预处理后,必须知道是哪一项改变影响了指标。

1.8.3 给项目加三个升级任务

升级 A · 统计字符

把切分函数改成逐字符遍历,忽略空格和标点,比较英文文本中最常见的字符。

升级 B · 排除停用词

建立一个集合,放入 the、is、for 等常见功能词,统计时跳过它们。

升级 C · 输出 JSON

把词频排名转换为字典列表,用 json.dumps() 保存,方便下一个程序读取。

升级 D · 接受命令行参数

让用户通过命令行传入文件路径和排名数量,下一章再学习 argparse。

CAPSTONE CHECK项目验收清单

五项都完成后,把代码、示例输入、输出截图和自己的复盘写进一个 README。这份 README 就是你进入下一章前留下的第一个可展示成果。

1.8.4 把需求写成验收标准

一个项目开始前,先用自然语言写清楚“什么算完成”。词频统计至少要回答:统计的单位是英文词、中文字符还是分词结果?大小写是否统一?标点和数字是否保留?同频词如何排序?空文件输出什么?文件不存在时程序返回什么状态?这些问题不写清楚,代码即使运行成功,也无法判断结果是否符合预期。

把验收标准改写成小案例:输入 "Python python",预期 python: 2;输入空字符串,预期空排名;输入带撇号的 don't,说明是否算一个词;输入中文,明确当前版本暂不分词或使用指定分词器。案例越具体,函数越容易设计,测试也越有针对性。

表 8 · 词频项目的最小测试矩阵
维度最小案例要观察的结果发现问题后先查
内容空文本、一个词、重复词是否返回空列表、正确次数切分函数和计数边界
大小写Python python PYTHON是否按规则合并lower() 的位置和开关
符号标点、撇号、连字符、数字哪些字符被保留或忽略正则表达式和 README 限制
文件不存在、空文件、乱码文件提示是否具体且不生成假结果路径、编码和异常边界
排名同频词、limit=0、超过总数顺序是否稳定、数量是否合理排序键和参数校验
PYTHON · 用断言固定最小预期
assert clean_and_split("Python python") == ["python", "python"]
assert count_words("") == []
assert count_words("readable readable") == [("readable", 2)]

断言失败时,先判断是测试预期写错,还是实现不符合需求。不要为了让断言通过而修改测试数据。测试案例是项目对外承诺的一部分,修改它应该有明确理由,并在 README 中留下变更记录。学习项目中哪怕只有三条断言,也比完全依赖人工观察更可靠。

1.8.5 清洗规则:明确支持范围和损失

正则表达式 [a-z]+(?:'[a-z]+)? 适合本项目的英文演示,但它不是通用语言学方案。它会忽略中文、重音符号、连字符、网址和代码标识符。清洗的每个规则都会损失某些信息,因此 README 要写出支持范围。后续扩展时,不要直接替换正则并宣称“支持所有语言”,而应增加案例、工具和边界说明。

大小写统一也有取舍。统计普通英文词时,小写化能把 Python 和 python 合并;统计专有名词或代码时,大小写可能是有意义的。可以把 case_sensitive 作为显式参数,或提供两个不同函数。让调用者看见这个选择,比在函数内部偷偷转换更可审计。

空白处理、标点过滤和停用词过滤应分成独立步骤。这样你可以分别比较每一步造成的数量变化,并在出现结果异常时定位。对大模型语料而言,保留一份原文、清洗中间结果和最终 Token 列表,能帮助你判断指标变化来自数据规则还是模型本身。

1.8.6 计数与排序:同频结果也要稳定

Counter.most_common() 会按次数降序排列;同次数项目的相对顺序可能受首次出现顺序影响。若要得到跨运行完全稳定的报告,可以显式使用 sorted(counts.items(), key=lambda item: (-item[1], item[0])),先按次数降序,再按单词字母序升序。稳定排序方便测试、版本比较和后续可视化。

PYTHON · 稳定排序与限制数量
def ranking_from_counts(counts, limit=10):
    ordered = sorted(counts.items(), key=lambda item: (-item[1], item[0]))
    return ordered[:limit]

print(ranking_from_counts({"b": 2, "a": 2, "c": 1}, limit=2))
# [('a', 2), ('b', 2)]

排名数量也要验证。limit=0 是返回空列表还是提示错误?负数是否代表“全部”?最简单的规则是要求它为非负整数,并在函数边界检查;如果业务确实需要特殊含义,就把它写在文档里。不要让 Python 切片的隐式行为替你决定产品规则。

1.8.7 命令行参数、配置和退出状态

把文件路径写死在 main() 里适合第一次演示,但真正使用时应该允许用户传入路径和排名数量。可以先手动读取 sys.argv,理解参数来自哪里,再学习标准库 argparse 生成帮助信息。命令行工具应在错误时返回非零退出状态,让脚本和自动化任务知道它失败了。

配置项要区分“代码规则”和“运行参数”。正则模式、默认编码属于代码的一部分;输入路径、排名数量和是否区分大小写更像运行参数。把两者混在一起会让每次实验都修改源代码,难以比较。将参数打印或保存到输出元数据中,才能知道一次结果采用了哪些设置。

1.8.8 从英文词频扩展到中文和模型数据

中文没有天然的空格分词,按字符统计、按词统计和按句统计是不同任务。可以先实现字符频率,认识“遍历字符串”的过程;再选择成熟分词工具,并测试专有名词、数字和中英文混合。不要把一个英文正则替换成中文字符范围就宣称完成中文分词,那只是在改变匹配字符,不是在理解词边界。

如果把词频统计连接到大模型语料,新的问题会出现:如何去除 HTML 标签,如何处理 Markdown 代码块,如何识别重复文档,如何截断过长文本,如何保存来源和许可证。词频统计本身不会回答这些问题,但它提供了一个检查入口:清洗前后词数变化异常,可能提示解析规则或数据质量存在问题。

模型响应也可以作为项目输入。先保存原始响应,再提取文本字段,最后统计词频;若解析失败,记录请求 ID、状态码和原始片段,不要只写“模型出错”。不要把真实用户内容放进公开样例,使用脱敏或人工编写的文本。数据隐私和版权边界与代码正确性同样重要。

1.8.9 测试矩阵、性能和可重复实验

为项目建立一个小型测试矩阵:输入规模(空、小、中)、语言(英文、中文、混合)、格式(标点、换行、重复空格)、文件状态(不存在、空、正常、无权限)。每次改动只改变一个维度,记录运行时间、词数、排名和错误。这样你能判断改动是否改善了目标,而不是被一份偶然的示例结果误导。

性能测试要用接近实际的数据规模,但不要一开始就下载未经许可的巨大语料。先生成可公开的重复样例,测量读取、正则匹配、计数和排序各阶段。若发现瓶颈,再考虑流式读取、分块、缓存或更合适的数据结构。记录 Python 版本、机器规格和参数,避免把不同环境的时间直接比较。

一个可重复实验至少包含四样东西:输入快照或来源说明、代码版本、运行参数、输出结果。输出中加入生成时间有助于追踪,但如果要比较文件内容,应同时保存不含时间的稳定结果。把实验编号放在目录名中,能让你保留失败尝试而不覆盖成功版本。失败记录不是浪费,它解释了为什么最终选择当前规则。

1.8.10 README 和项目展示

README 可以按“目的、环境、安装、运行、输入格式、输出示例、已知限制、测试方法、许可证”组织。每项写两三句话即可,但要让没有看过源代码的人知道如何复现。截图很直观,却不能替代文本命令;终端输出复制成代码块,别人可以直接比较差异。

项目展示时不要只展示排名表,也展示一条原始输入、一条清洗结果和一条错误处理。读者需要看到程序如何对待异常情况。解释为什么选择英文正则、为什么使用 Counter、为什么按两个键稳定排序,比展示一个漂亮界面更能证明你理解了程序。

本项目的终点不是“能数出几个词”,而是完成一次小型工程闭环:定义需求,准备环境,组织目录,读取输入,清洗和聚合,验证结果,保存输出,记录限制。未来把词频统计替换成文本分类、向量索引或模型评测时,步骤仍然相似,只是处理函数和数据规模变复杂了。

DEEP DIVE 08项目第二版规格书

请把当前词频统计器升级为一个可复现实验工具:使用命令行参数传入文件和排名数量;输出稳定排序的 JSON;保存来源、参数和生成时间;为不存在、空文件、编码错误、无效排名分别设计提示;补充至少 10 个断言案例,并在 README 中写明英文和中文支持范围。

验收时检查“同一输入和参数是否产生同一稳定排名”“失败是否包含可行动信息”“输出能否被 JSON 再次读取”“README 是否能让别人从零复现”。如果加入第三方中文分词器,要记录版本和许可证,不要把下载的模型或私有语料提交到公开仓库。

1.8.11 项目复盘模板

项目结束后,按“目标—输入—处理—输出—证据—限制—下一步”写一页复盘。目标说明为什么做,输入说明来源和格式,处理说明每一步规则,输出说明字段和排序,证据包括测试案例与截图,限制说明语言和规模边界,下一步写一个可验证的升级任务。这个模板可以直接迁移到后续的模型微调、检索和评测项目。

复盘时还要记录一次失败。比如忘记统一大小写、错误处理覆盖太宽、输出文件被覆盖或路径依赖当前目录。说明失败输入和修复方式,下一次就能把它加入自动化测试。只展示成功结果会让项目看起来顺利,却无法证明你理解了风险;能够解释一次失败,反而更能体现工程能力。

当词频项目稳定后,选择一个升级方向深入,不要同时堆十个功能。你可以做中文字符统计、加入命令行参数、输出 CSV、按文件批量处理,或把统计结果接到一个简单的可视化页面。每次升级都保留旧测试和 README,确保新增功能没有破坏已有行为。小步迭代比一次性重写更适合学习。

1.8.12 项目小结:从玩具脚本到可复用工具

词频统计器的每个升级都应有一个明确问题:要不要支持中文?要不要接受命令行路径?要不要保存 JSON?要不要按文件批量处理?先写问题和验收案例,再修改代码。这样功能增长会带来理解,而不是带来一堆彼此冲突的开关。

为项目保留一个最小版本和一个实验版本。最小版本只包含已经测试的规则,实验版本可以尝试新正则、新排序或新输入格式。两者分开后,失败实验不会破坏可靠结果,也能让你比较改动到底带来了什么。

最后检查项目是否能被“未来的自己”运行:目录清楚,命令完整,输入样例可公开,输出字段有说明,限制写在 README,错误有可行动提示。能让别人复现,通常也意味着你自己真正理解了。

项目交付前再做一次冷启动测试:关闭当前终端,打开新的终端窗口,按 README 从零创建环境并运行。冷启动能暴露隐藏的当前目录、旧变量和未记录依赖,是判断项目是否真正可复现的最有效检查之一。

词频项目的最后一个质量问题是结果解释:排名靠前不一定代表主题重要,可能只是停用词、模板文字或重复段落很多。把统计结果与抽样阅读结合起来,说明它能回答什么、不能回答什么,才能避免把简单指标过度解读成模型结论。

1.8.13 把脚本变成有帮助信息的命令行工具

官方教程强调“先写能运行的脚本,再逐步组织成模块”。对本项目来说,下一步不是立刻引入框架,而是让命令行告诉用户它需要什么。argparse 可以根据参数定义自动生成 --help,把路径、排名数量和大小写选项从源代码中分离出来。即使暂时不完整实现统计,也可以先运行帮助页面,理解命令行工具的入口、参数和退出边界。

参数定义本身就是接口文档

PYTHON · argparse 入门骨架
import argparse

def parse_args():
    parser = argparse.ArgumentParser(description="统计文本中的英文词频")
    parser.add_argument("source", help="输入文本路径")
    parser.add_argument("-n", "--limit", type=int, default=10,
                        help="显示前几项,默认 10")
    parser.add_argument("--case-sensitive", action="store_true",
                        help="保留大小写,不默认合并")
    return parser.parse_args()

if __name__ == "__main__":
    args = parse_args()
    print(args.source, args.limit, args.case_sensitive)
试运行:python word_frequency.py --help 应显示位置参数、可选参数和帮助文字;--limit abc 会在参数边界直接报告错误,而不是让错误深入统计函数。

参数解析函数只负责把命令行文字变成结构化配置,统计函数仍然只接收文本和明确的数值。这样测试时可以直接构造参数对象或调用核心函数,不必每次模拟终端输入。错误发生在入口时,给出“哪个参数不正确、应该怎样写”;错误发生在文件读取时,给出路径和编码;错误发生在统计逻辑时,保留 traceback 供开发者排查。

命令行工具还要约定退出状态:成功通常返回 0,输入错误或文件不存在时返回非零值。初学阶段可以先用 main() 返回整数,再在入口处调用 raise SystemExit(main())。这一步看似比 print() 多写一行,却能让脚本被批处理、测试工具和后续自动化准确判断成功还是失败。

CLI LAB 1.8.13为词频统计器补上接口

给项目增加三个命令行参数:输入路径、排名数量和是否区分大小写。完成 --help 页面;输入文件不存在时返回非零状态;排名数量不是正整数时给出参数错误;成功时同时输出终端摘要和 JSON 文件。最后把 README 中的运行命令复制到一个全新的终端窗口进行冷启动测试。

参数解析和核心统计分开;limit 在入口或函数边界校验;文件路径使用 Path;输出 JSON 保留参数和来源;不要让 --help 触发读取文件或网络请求。测试至少覆盖帮助、成功、缺参、文件不存在和空文件。

09 · REVIEW & REFERENCES

复盘:从“会写语法”到“会解决问题”

Python 基础不是把关键字背完,而是能把一个模糊任务拆成输入、状态、处理和输出。变量保存当前状态,数据结构组织一批状态,条件和循环决定处理路径,函数把重复逻辑封装起来,模块把文件分工,异常和文件处理让程序面对真实世界。

复盘时不要只问“我看完了吗”,而要问“我能否在没有提示的情况下完成一个相邻任务”。如果示例是统计英文单词,就尝试统计字符;如果示例读取文本,就改为读取 JSON;如果示例用列表保存结果,就思考字典或集合是否更合适。迁移任务会暴露真正的理解边界,也会让你更快发现自己只是记住了某一段代码的外形。

遇到错误时,把错误记录成三行:输入是什么、程序实际做了什么、你期望它做什么。再补一行“下一次先检查哪里”。例如输入 "18" 导致加法失败,实际类型是字符串,期望类型是整数,下一步就是在转换边界检查 int()。这样记录比简单写“今天遇到 TypeError”更容易在下次复用。

本章的知识还没有涉及对象、类、迭代器、生成器和装饰器等进阶主题。暂时不需要因为没有学完它们而停下。你已经能够用函数和基础容器完成很多数据准备任务;等遇到真实需求,再把进阶概念接到已有的输入—处理—输出链路上。学习路线不是一次性解锁所有术语,而是让新概念在需要时有落脚点。

你也可以把本章内容做成一张自己的“错误地图”。左栏记录语法和运行错误,例如漏冒号、缩进不一致、变量未定义;中栏记录数据错误,例如字符串没有转换、列表顺序被改变、字典键不存在;右栏记录设计错误,例如函数职责太多、路径依赖当前目录、错误被宽泛捕获。每次遇到同类问题,就在对应栏补一个最小案例。几周后,这张地图会比一份泛泛的语法摘要更贴近你的真实需要。

判断是否可以进入下一章,不看你能否背出所有方法名,而看你能否独立完成一个小闭环:拿到输入、做出判断、组织数据、封装步骤、处理一个错误、输出可验证的结果。如果中间还需要查文档,这是正常的;专业程序员也会查文档。真正需要避免的是不知道要查什么,或者复制代码后无法解释它对输入做了哪些改变。

下一章会把这些基础能力接到 NumPy、Pandas 和可视化上。届时数据量会变大,表格会有缺失值,结果需要用图表观察,但核心问题仍然是本章反复练习的四个词:输入是什么、状态如何、处理发生在哪里、结果怎样验证。带着这四个问题进入下一章,你会发现新工具只是新的容器和函数,而不是完全陌生的世界。

如果你暂时还不能完成所有升级任务,也不需要把整章推倒重学。把卡点分成“符号问题”“概念问题”和“设计问题”:符号问题通过最小示例解决,概念问题回到对应小节重读,设计问题则把程序拆小并写出预期输入输出。不同类型的问题需要不同的解决方式,混在一起只会让复习变得漫无目的。

建议在笔记末尾留下三句话:我现在能独立完成什么;我仍然会在哪里出错;下一次遇到真实资料时先做哪一个检查。它们比“本章已完成”更能帮助你进入下一章。学习编程的进度不是直线上升的,能持续缩小问题范围,就已经是在建立真正可迁移的能力。

当你再次打开本章时,可以跳过已经熟练的示例,直接从错误记录和迁移练习开始。先回忆,再查资料,再运行验证,这个顺序会把复习从被动阅读变成主动检索。能够在需要时找回知识,比一次性记住所有细节更接近真实工作。

图 2 · 本章知识如何连成一条程序链自制学习地图
输入文本 / 文件 / 用户状态变量 / 容器处理分支 / 循环 / 函数结果屏幕 / 文件 / 项目

任何一个小项目都可以沿着这条链回看:输入是否正确?状态是否符合预期?哪一步处理错了?结果是否可验证?

本章完成标准

  • 能在虚拟环境中创建并运行一个 Python 脚本。
  • 能区分字符串、数字、布尔值和空值,并在输入后正确转换。
  • 能用 if、for 和 while 表达选择与重复。
  • 能根据顺序、键、重复和可变性选择列表、字典、元组或集合。
  • 能把逻辑拆成函数,使用模块,并捕获具体的可预期异常。
  • 能使用 UTF-8 读写文本或 JSON,并完成词频统计项目。

课后小练习:不看资料完成一次迁移

FINAL PRACTICE学习资料索引器

请写一个小程序,读取一个文本文件,把每行当作一个学习资料标题,完成下面要求:

  1. 删除空行和首尾空白。
  2. 用集合找出重复标题,并报告重复数量。
  3. 用字典按关键词统计标题数量,例如包含“Python”“模型”“数据”的标题。
  4. 把清洗后的标题和统计结果保存为 JSON。
  5. 为文件不存在、空文件和 JSON 写入失败设计清楚的提示。

评分重点不是代码长短,而是职责是否清楚:至少拆出“读取标题”“统计关键词”“保存 JSON”三个函数;不要把所有逻辑写在一个循环里。重复标题可以用 Counter 或“列表长度减去集合长度”的方式判断;关键词统计应明确大小写策略。完成后检查 JSON 是否能被再次读取。

下面是一份可以直接保存为 title_indexer.py 的参考实现。它把错误处理放在靠近输入输出的边界,核心函数仍然可以单独测试。这里约定:重复标题按清洗后的完整字符串判断;关键词按“一个标题命中一次”统计;空文件是合法输入,但报告中明确写出标题数量为 0。

PYTHON · 参考答案:title_indexer.py
from collections import Counter
import json
from pathlib import Path

def read_titles(path):
    """读取 UTF-8 标题,去掉空行和首尾空白。"""
    text = path.read_text(encoding="utf-8")
    return [line.strip() for line in text.splitlines() if line.strip()]

def find_duplicates(titles):
    """返回出现次数超过一次的标题及其次数。"""
    counts = Counter(titles)
    return {title: count for title, count in counts.items()
            if count > 1}

def count_keywords(titles, keywords):
    """每个标题对每个关键词最多贡献一次,比较时忽略大小写。"""
    result = Counter()
    folded_keywords = {keyword: keyword.casefold() for keyword in keywords}
    for title in titles:
        folded_title = title.casefold()
        for keyword, folded in folded_keywords.items():
            if folded in folded_title:
                result[keyword] += 1
    return dict(result)

def save_json(path, payload):
    """创建父目录,保存可再次读取的 UTF-8 JSON。"""
    path.parent.mkdir(parents=True, exist_ok=True)
    text = json.dumps(payload, ensure_ascii=False, indent=2)
    path.write_text(text, encoding="utf-8")

def build_report(titles):
    return {
        "title_count": len(titles),
        "duplicate_count": len(find_duplicates(titles)),
        "duplicates": find_duplicates(titles),
        "keyword_titles": count_keywords(
            titles, ["Python", "模型", "数据"]
        ),
        "titles": titles,
    }

def main():
    source = Path("titles.txt")
    target = Path("output/title-index.json")
    try:
        titles = read_titles(source)
        report = build_report(titles)
        save_json(target, report)
        # 写完立即回读,验证输出不是“看起来像 JSON”。
        json.loads(target.read_text(encoding="utf-8"))
    except FileNotFoundError:
        print(f"找不到输入文件:{source},请先创建它。")
        return 1
    except UnicodeDecodeError:
        print(f"文件不是 UTF-8 编码:{source},请转换后再试。")
        return 1
    except OSError as error:
        print(f"文件读写失败:{error}")
        return 1
    except json.JSONDecodeError as error:
        print(f"JSON 回读失败:{error}")
        return 1
    print(f"已保存 {report['title_count']} 条标题到 {target}")
    print(f"重复标题:{report['duplicate_count']} 个")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())
准备一份最小的 titles.txt: Python 基础 模型入门 Python 基础 数据清洗 运行 python title_indexer.py 后,预期会报告 3 个非空标题、1 个重复标题,并在 output/title-index.json 中保留清洗后的列表、重复明细和关键词统计。

这份答案里有三个值得检查的设计决定。第一,find_duplicates() 返回明细而不是只返回一个数字,所以读者能知道究竟是哪一行重复;若只关心重复条目数量,也可以用 len(titles) - len(set(titles)),但那表示“多出来的行数”,不等于“有多少种标题重复”。第二,关键词使用 casefold() 后再做包含判断,且同一标题对同一关键词只加一次;这避免了标题写成 Python Python 时把“标题数量”误当成“出现次数”。第三,保存后用 json.loads() 回读,验证文件真的能被下一个程序使用。若把所有工作塞进 main(),这些规则会混在文件循环、打印和异常里,修改一个需求就容易破坏另外三个需求。

表 9.1 · 参考答案的验收证据
要求代码位置应观察的证据常见误解
清洗标题read_titles()空行消失,首尾空白消失,内部文字不被截断把整份文件当成一个标题
识别重复find_duplicates()报告具体标题和次数用集合后就丢失重复次数
统计关键词count_keywords()大小写策略稳定,每个标题最多计一次没有说明“标题数”还是“出现次数”
保存并回读save_json() 与 json.loads()输出目录存在,JSON 可被再次解析只看文件扩展名就认为格式正确

课后可以再做三次有目的的变更:把关键词改成命令行参数;把重复规则改成忽略大小写;把 JSON 增加来源文件和生成时间。每次只改一项,并保留原来的最小输入和预期输出。这样练习的是“需求变化时如何保护职责边界”,而不是把代码越写越长。

参考资料与继续学习

本章的概念顺序参考 Python 官方教程中关于速览、控制流、数据结构、模块、输入输出、错误与异常、虚拟环境的章节;编码风格部分参考 PEP 8。正文的中文解释、练习、示例代码和 SVG 图示由本站重新编写或绘制,不复制外部页面的排版与插图。

参考页面访问日期:2026-08-27。外部文档的版本和内容可能更新;遇到差异时,以 Python 官方文档为准。

1.9.1 按官方目录回看知识关系

可以把本章内容重新排成一张目录,而不是按阅读顺序死记。第一层是安装和解释器:你要知道如何唤出 Python、传入脚本、进入交互模式,以及源文件采用什么编码。第二层是速览:把解释器当作计算器,认识数字、文本和列表,再写出第一个带循环的程序。第三层是控制流、数据结构、模块、输入输出和异常,最后用项目把它们串起来。

表 9 · 本章与 Python 官方教程的对照
官方教程主题本站对应章节本章补充的零基础内容完成证据
课前甜点1.1.2、1.8.8软件来源、用途边界与练习方式能说明 Python 适合做什么
使用解释器1.1.1、1.1.7–1.1.13、1.2.1、1.2.6–1.2.7命令行、PATH、虚拟环境、编码和排错能在新终端运行脚本并读懂参数
Python 速览1.3.1–1.3.17输入验证、可变性、输出契约与迁移任务能独立完成一个输入—处理—输出小程序
控制流工具1.4.1–1.4.15、1.6.1、1.6.16状态机、路径覆盖、命令行和组合函数能解释每个分支和循环怎样结束
数据结构1.5.1–1.5.13列表方法、容器语义、嵌套形状和可追溯性能按顺序、键、重复和修改需求选容器
模块1.6.2–1.6.3、1.6.9–1.6.13、1.6.16–1.6.17目录边界、入口保护、导入冲突与测试能把函数拆到模块并安全导入
输入与输出1.3.4、1.7.1–1.7.16UTF-8、Path、JSON、CSV、大文件和日志能生成可再次读取、可追溯的输出
错误与异常1.6.4、1.6.10–1.6.17异常边界、异常链、用户提示和恢复策略能捕获可处理错误并保留上下文

官方教程还继续介绍类、标准库、虚拟环境、交互式编辑和浮点数限制等主题。它的分类值得借鉴,但它明确面向已经有编程总体概念、刚开始接触 Python 的读者。对真正零基础的人,本页面增加了命令行、路径、编辑器、输入验证和逐步排错;遇到官方章节读不懂时,先回到这些前置内容,不需要强行跳过。

复习时不要把目录当作待打勾的清单。每个条目都要有一个可运行证据:解释器条目对应一次版本和路径检查,数字条目对应一个算术实验,文本条目对应一次切片和格式化,列表条目对应一次追加和排序,控制流条目对应边界测试,模块条目对应一次导入,输入输出条目对应一个可再次读取的文件,异常条目对应一次可行动的错误提示。

1.9.2 零基础自测:不用术语也能说明白

尝试不用“动态类型”“可迭代对象”“异常链”这些术语,向别人解释一段三十行的小程序。你能说清它从哪里拿到输入、每个名称代表什么、哪一行改变了数据、程序如何决定走哪条路径、结果在哪里输出、发生问题时会告诉谁什么,就说明理解已经超过背诵。术语是压缩表达,不能替代具体解释。

再做一次反向练习:看到一个熟悉的程序,遮住变量名和注释,只看结构,猜测它的输入输出;然后恢复名称,检查自己的猜测。这个练习能训练你识别函数职责、循环边界和容器形状。真正工作时,你经常需要先读别人的代码,再决定是否修改,阅读能力和编写能力同样重要。

如果你卡在一个概念上,把问题改写成三个更小的问题。例如“不懂字典”可以拆成“键和值如何配对”“键不存在时怎么办”“为什么不用两个列表”;“不懂异常”可以拆成“哪一行可能失败”“我能否恢复”“如何把错误交给调用者”。小问题更容易用实验回答,也更容易在官方文档中定位。

1.9.3 复习间隔与错误地图

建议在当天、两天后、一周后各复习一次,但每次不要从头重读。当天运行所有最小示例;两天后不看答案重写一个相邻任务;一周后只看错误记录和迁移练习。间隔复习的目标是让你在需要时找回知识,而不是把页面读得越来越熟却无法独立写出代码。

错误地图可以分成四栏:环境错误、语法错误、数据错误和设计错误。环境错误包括解释器路径和依赖不一致;语法错误包括漏冒号、括号和缩进;数据错误包括字符串未转换、键缺失和空输入;设计错误包括函数职责过多、路径硬编码和错误被吞掉。每次只把错误放进最合适的一栏,并附上最小复现代码。

错误地图还应记录“我原来以为会发生什么”。例如你以为 input() 会给出整数,实际它给出字符串;你以为 sort() 返回排序后的列表,实际它原地修改并返回 None;你以为相对路径相对于脚本,实际相对于当前工作目录。把误解写出来,比只抄正确结论更能防止下次重复。

1.9.4 从本章走向大模型学习

后续学习大模型时,Python 基础会以四种方式出现。第一是数据:读取、清洗、切分、去重和保存语料。第二是实验:用循环批量调用模型,记录参数、响应和指标。第三是接口:把函数组织成命令行工具、Notebook 或 Web 服务。第四是可靠性:处理超时、格式错误、权限、成本和隐私。

模型并不会替你解决基础程序问题。API 返回 JSON 仍然要用字典和列表读取;模型输出不符合格式仍然需要条件判断和异常处理;长文档仍然需要路径、编码、分块和可追溯的文件;评测指标仍然需要数字、排序和测试。Python 基础越扎实,你越能把注意力放在模型和任务本身,而不是被环境和数据细节反复打断。

进入下一章前,可以完成一个小型“模型前置项目”:读取一份公开、许可清楚的文本;保存原始文件和来源;统计字符、行数和词数;把清洗规则写成函数;把结果保存为 JSON;为不存在文件、空文件和编码错误写测试。暂时不调用模型,先把数据管道闭环。这个项目会为后面的向量、检索和提示词实验准备可靠输入。

1.9.5 继续学习的顺序与边界

建议下一步先学习更完整的命令行参数和标准库,再进入 NumPy、Pandas 和可视化。数据处理工具会提供新的对象和方法,但仍然建立在本章的变量、容器、函数和文件之上。不要在基础概念还不稳定时同时学习十个框架;选择一个小项目,持续使用同一套输入、输出和测试。

当你开始接触类和对象时,先回想本章的“名称指向对象”模型。类只是把数据和相关行为组织成新的对象类型,并没有抛弃变量、函数和异常。学习生成器、装饰器或异步代码时,也先寻找输入、状态、处理和输出,复杂语法就会有落脚点。每个新概念都应该连接到一个已经能运行的例子。

阅读官方文档时,先看页面的目标、输入输出和最小示例,再看参数细节和边界。不要从第一行术语定义开始逐字背诵。遇到版本差异,记录文档版本和本机版本,以官方文档为准。第三方教程可以帮助理解,但要核对来源、发布日期、许可证和是否仍然维护,尤其是涉及模型 SDK、依赖安装和安全配置时。

1.9.6 本章最终验收

请在一个全新的终端窗口中完成验收,而不是依赖旧的交互式状态。创建或激活虚拟环境;运行一个不含第三方库的脚本;读取一个 UTF-8 文本;用列表、字典和集合保存中间数据;把核心逻辑拆成函数和模块;故意制造一次输入错误并给出提示;运行词频项目并保存 JSON;最后把命令、版本、路径、输出和限制写入 README。

如果其中一项需要查文档,不代表你没有学会。专业开发也会查文档,区别在于你知道要查哪个主题、要提供什么输入、要检查什么输出。把查到的页面和关键结论记入笔记,下一次先尝试回忆再打开链接。可检索、可验证、可复现,才是长期有效的学习方式。

完成验收后,你可以在本页顶部的学习地图中随机选择一个条目,写一个不超过二十行的新例子。若能在没有复制原代码的情况下完成,并且为至少一个边界写测试,就可以进入下一章。若不能,回到对应的独立卡片,只复习缺失的那一块,不必把整章全部重读。

FINAL REVIEW 09零基础迁移挑战

写一个“本地学习资料检查器”:扫描一个目录中的 Markdown 文件,统计每个文件的非空行数和字符数,找出重复标题,把结果保存为 JSON,并生成一份人类可读的摘要。要求支持空目录、文件不存在、UTF-8 解码失败和输出路径不可写四种情况;README 写明安装、运行、测试和已知限制。

评分不以代码行数为准:目录和文件路径是否清楚,读取是否使用 UTF-8,统计和展示是否拆成函数,重复规则是否明确,错误提示是否包含文件名和下一步,JSON 是否能再次读取,测试是否覆盖空输入和异常输入。完成后把一次失败运行也记录在复盘中。

下面给出一个“最小但完整”的参考骨架。它没有把所有功能压成一个循环,而是分别处理扫描目录、统计单个文件、提取标题、保存 JSON 和生成摘要。你可以把它保存为 check_materials.py,再用自己的资料目录替换命令行参数。

PYTHON · 参考答案:check_materials.py
from collections import Counter
import json
import sys
from pathlib import Path

def read_markdown(path):
    """读取一个 Markdown 文件,并返回原文。"""
    return path.read_text(encoding="utf-8")

def summarize_text(text):
    lines = text.splitlines()
    non_empty = [line for line in lines if line.strip()]
    headings = [
        line.removeprefix("#").strip()
        for line in lines
        if line.lstrip().startswith("#")
    ]
    return {
        "non_empty_lines": len(non_empty),
        "characters": len(text),
        "headings": headings,
        "first_heading": headings[0] if headings else "",
    }

def scan_directory(source):
    if not source.exists():
        raise FileNotFoundError(source)
    if not source.is_dir():
        raise NotADirectoryError(source)
    records = []
    for path in sorted(source.rglob("*.md")):
        text = read_markdown(path)
        record = summarize_text(text)
        record["file"] = str(path)
        records.append(record)
    return records

def find_duplicate_titles(records):
    titles = [
        record["first_heading"]
        for record in records
        if record["first_heading"]
    ]
    counts = Counter(titles)
    return {title: count for title, count in counts.items()
            if count > 1}

def save_report(target, records):
    target.parent.mkdir(parents=True, exist_ok=True)
    report = {
        "file_count": len(records),
        "files": records,
        "duplicate_titles": find_duplicate_titles(records),
    }
    target.write_text(
        json.dumps(report, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    # 立即回读,确认下游程序可以使用这个文件。
    json.loads(target.read_text(encoding="utf-8"))
    return report

def print_summary(report):
    print(f"扫描文件:{report['file_count']} 个")
    for record in report["files"]:
        print(
            f"- {record['file']}: "
            f"{record['non_empty_lines']} 行,"
            f"{record['characters']} 字符"
        )
    duplicates = report["duplicate_titles"]
    print(f"重复标题:{len(duplicates)} 种")
    for title, count in duplicates.items():
        print(f"  {title!r}({count} 个文件)")

def main(argv=None):
    args = argv if argv is not None else sys.argv[1:]
    if len(args) != 2:
        print("用法:python check_materials.py <资料目录> <输出 JSON>")
        return 2
    source = Path(args[0])
    target = Path(args[1])
    try:
        records = scan_directory(source)
        report = save_report(target, records)
    except FileNotFoundError:
        print(f"目录不存在:{source},请检查路径后重试。")
        return 1
    except NotADirectoryError:
        print(f"这不是目录:{source},请传入资料目录。")
        return 1
    except UnicodeDecodeError as error:
        print(f"UTF-8 解码失败:{error.filename},请转换文件编码。")
        return 1
    except OSError as error:
        print(f"读写失败:{error};请检查权限和输出路径。")
        return 1
    print_summary(report)
    print(f"JSON 已保存并验证:{target}")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())
运行示例:python check_materials.py notes output/report.json 扫描文件:2 个 - notes\python.md: 18 行,436 字符 - notes\model.md: 24 行,712 字符 重复标题:0 种 JSON 已保存并验证:output/report.json

这份实现把“字符数”定义为 Python 读入字符串的字符数量,把“非空行”定义为去掉首尾空白后仍有内容的行,把“标题”定义为以 # 开头的第一行。定义必须写出来,因为换成 Markdown 的 Setext 标题、代码块里的井号或 YAML 标题后,结果可能不同。对真实项目而言,可以继续扩展解析规则,但每次扩展都应该先增加一个输入案例,而不是悄悄改变旧字段的含义。

四种异常也各有边界。目录不存在属于路径输入错误;传入普通文件而不是目录属于类型错误;某个文件不是 UTF-8 属于数据编码错误;输出路径不可写属于环境或权限错误。代码给用户的提示包含“哪个对象出错”和“下一步做什么”,开发者仍可在测试环境中保留 traceback。不要使用一个裸的 except Exception 把所有问题都变成“失败”,那样看似友好,实际会丢失排错证据。

表 9.2 · 最终项目的测试矩阵
场景准备方式预期结果验收重点
正常目录两个 UTF-8 Markdown 文件记录文件数、非空行数和字符数,生成可回读 JSON字段和摘要一致
空目录目录存在但没有 *.md文件数为 0,仍能生成空报告空输入不是异常
目录不存在传入不存在的路径非零退出状态和可行动提示不生成伪造的成功 JSON
编码错误放入非 UTF-8 文件指出具体文件,提示转换编码没有吞掉 UnicodeDecodeError
输出不可写目标位于无权限位置报告写入失败并说明检查权限读取成功不应被误报为整体成功

提交前再运行一次冷启动测试:关闭旧终端,在项目根目录新开终端,按 README 创建环境、运行帮助、运行正常案例和异常案例。把一次失败的命令、完整提示和修复动作写进复盘。这样验收的对象不只是“代码今天能跑”,而是一个别人能理解、能复现、失败时知道下一步的学习项目。

这份章节资料会继续沿用同一原则:先给出可理解的目录,再用独立卡片承载一个完整小节;每个小节达到足够的篇幅,包含解释、图示、示例、练习和答案;外部资料只作为概念和规范的参考,正文、代码和图示由本站重新组织。你可以把它当作一份会持续更新的个人教材,而不是一次读完就丢开的网页。

1.9.7 学习资料的使用方式

这不是一篇需要从上到下背诵的长文章。第一次阅读按目录顺序运行示例;第二次只重做练习和错误案例;第三次随机挑一个小节,用自己的项目数据改写。每次都留下运行证据和一句“我还不清楚什么”,网页才会变成主动学习工具。

章节之间的独立卡片是为了让你可以暂停和返回。读完一张卡片后,把代码复制到自己的目录,改一个变量、换一种输入或故意制造一个错误。卡片里的文字提供背景,真正的理解来自你对输入和输出的预测、运行与解释。

如果学习时间有限,优先保证环境、变量、控制流、容器、函数、文件和项目各完成一个闭环,而不是只看完所有标题。基础能力形成后,后面的 NumPy、Pandas、向量、检索和模型 API 都可以在同一条输入—处理—输出链上继续扩展。

建议每完成一个小节就写三行笔记:一个我能解释的概念、一个我亲手运行的例子、一个下次要验证的边界。三行笔记比复制整页文字更容易复习,也能持续暴露真正的理解缺口。

复盘的最终产物可以很短,但必须具体:我能独立完成的任务、我仍会犯的错误、下一次要验证的边界,以及一个可运行的代码链接。把这些内容放在章节末尾,下一次学习就有明确入口,而不是重新从标题开始浏览。

复习时还可以进行一次“口述代码”:不看屏幕,按顺序说出程序会创建哪些对象、每个循环处理多少项、哪个函数可能失败以及结果保存在哪里。口述卡顿的位置就是下一次应该运行最小实验的位置。能把代码讲给别人听,通常也能在真实项目中更快定位问题。

如果你准备把本章资料分享给同学,请把本页的官方参考链接、示例代码许可证和原创图示说明一起保留。学习资料不仅要内容丰富,也要让读者知道哪些是规范依据、哪些是本站的解释和练习。清楚的来源边界有助于长期维护和放心使用。

把本章最后留下的命令、版本、路径、输入、输出和错误案例保存到一个小型学习记录中,它就是下一章的起点。以后不论学习数据处理、神经网络还是模型 API,都先从这份记录确认环境,再开始新的实验。