Class 3 · Chapter 2:函数、异常、文件读写(小白版)
预计 40 分钟|前置:Chapter 1|产物:
03_functions.py
0. 本章目标
把上一章散着的代码变成可复用函数 ,并学会两件保命技能:
文件不存在不崩溃、除零不崩溃。
1. 运行
powershell
chcp 65001 | Out-Null
$env:PYTHONUTF8="1"; $env:PYTHONIOENCODING="utf-8"
[Console]::OutputEncoding=[Text.Encoding]::UTF8
& "D:\miniconda3\envs\llm\python.exe" "D:\myAImodle\Class03\Chapter02\03_functions.py"
2. 预期输出
=== 1. 函数 ===
行数: 3
不存在的文件: None
=== 2. 异常 ===
10 / 2 = 5.0
10 / 0 = None
=== 3. 文件读写 ===
内容:
第一行
第二行
第三行
字符数: 12
=== 4. 模块 ===
math.sqrt(16) = 4.0
3. 完整代码
python
from pathlib import Path
def count_lines(path):
"""返回文件行数;文件不存在时返回 None。"""
p = Path(path)
if not p.exists():
return None
with p.open("r", encoding="utf-8") as f:
return sum(1 for _ in f)
def safe_divide(a, b):
"""安全除法,除数为 0 时返回 None。"""
try:
return a / b
except ZeroDivisionError:
return None
def main():
demo = Path(r"D:\myAImodle\Class03\Chapter02\demo.txt")
demo.write_text("第一行\n第二行\n第三行\n", encoding="utf-8")
print("=== 1. 函数 ===")
print("行数:", count_lines(demo))
print("不存在的文件:", count_lines(demo.with_name("nope.txt")))
print("\n=== 2. 异常 ===")
print("10 / 2 =", safe_divide(10, 2))
print("10 / 0 =", safe_divide(10, 0))
print("\n=== 3. 文件读写 ===")
content = demo.read_text(encoding="utf-8")
print("内容:")
print(content, end="")
print("字符数:", len(content))
print("\n=== 4. 模块 ===")
import math
print("math.sqrt(16) =", math.sqrt(16))
if __name__ == "__main__":
main()
4. 逐块拆解
第 1 块:定义函数
python
def count_lines(path):
"""返回文件行数;文件不存在时返回 None。"""
p = Path(path)
if not p.exists():
return None
with p.open("r", encoding="utf-8") as f:
return sum(1 for _ in f)
def 函数名(参数)::定义函数;函数体必须缩进。- 三引号字符串是 docstring ,写函数用途,
help(count_lines)能看到。 return:把结果交回去;执行到 return 函数立即结束。Path(path):把字符串变成路径对象,方便判断和打开。p.exists():文件/目录是否存在。with p.open(...) as f::打开文件,离开 with 自动关闭 ,不用手动close()。encoding="utf-8":不写就可能按 GBK 读,中文出错。sum(1 for _ in f):每读一行产生一个 1,最后求和 = 行数;_表示「这个值不用」。
第 2 块:异常处理
python
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
try:尝试执行可能出错的代码。except ZeroDivisionError:只捕获「除以 0」这一种错误。- 出错时执行 except 分支,程序不会崩溃。
- 捕获范围要小,不要写裸
except:把所有错误都吞掉。
第 3 块:写文件与读文件
python
demo.write_text("第一行\n第二行\n第三行\n", encoding="utf-8")
content = demo.read_text(encoding="utf-8")
write_text:一次写入整个字符串,覆盖原内容。\n:换行符。read_text:一次读回整个文件。- 大文件(几 GB)不要用
read_text,要像第 1 块那样逐行读。
第 4 块:模块
python
import math
print(math.sqrt(16))
- 模块 = 一个
.py文件或标准库。 import math后,用math.xxx调用里面的函数。- 后面会用到
import json、import pandas as pd,都是同一套用法。
第 5 块:main 保护
python
if __name__ == "__main__":
main()
- 直接运行本文件时,
__name__是"__main__",于是调用main()。 - 被别的文件
import时,不会自动执行,方便复用函数。 - 这是 Python 项目的标准写法,后面训练脚本都这么写。
5. 常见错误
| 现象 | 原因 | 解决 |
|---|---|---|
FileNotFoundError |
路径写错/文件不存在 | 先 Path.exists() 判断 |
IndentationError |
缩进不一致 | 统一 4 个空格 |
读取中文报 UnicodeDecodeError |
没指定编码 | 加 encoding="utf-8" |
ZeroDivisionError |
除数为 0 | 用 try/except 或先判断 |
函数返回 None 却参与计算 |
忘了 return | 检查每个分支都有返回值 |
| 文件被占用 | 没关闭文件 | 用 with open |
6. 练习
- 写一个
count_chars(path),返回文件字符数。 - 把
safe_divide改成除零时返回字符串"不能除以 0"。 - 为什么推荐
with open而不是f = open(...)?
7. 验收清单
- 能独立写出带参数和返回值的函数
- 会用
try/except捕获指定异常 - 会用
with open(..., encoding="utf-8") - 知道
if __name__ == "__main__"的作用
8. 下一章
Class 3 Chapter 3:JSON / JSONL------训练数据的标准格式。
附:练习答案
python
def count_chars(path):
p = Path(path)
if not p.exists():
return None
return len(p.read_text(encoding="utf-8"))
- 把
return None改成return "不能除以 0"。 with会在代码块结束时自动关闭文件,即使中途报错也会关,避免文件被占用和资源泄漏。
Class 3 · Chapter 3:JSON / JSONL 读写(小白版)
预计 40 分钟|前置:Chapter 2|产物:200 条样例数据
datasets\sample\sft_sample.jsonl
0. 本章目标
掌握训练数据的标准格式 JSONL :一行一条 JSON,能写、能读、能统计。
后面所有数据采集、清洗、训练都围绕它。
1. JSON 和 JSONL 的区别
JSON(一个数组):
json
[
{"instruction": "问题1", "output": "回答1"},
{"instruction": "问题2", "output": "回答2"}
]
JSONL(一行一条):
json
{"instruction": "问题1", "output": "回答1"}
{"instruction": "问题2", "output": "回答2"}
为什么训练都用 JSONL:
| 优点 | 说明 |
|---|---|
| 可流式读取 | 不用一次把几十 GB 读进内存 |
| 可追加 | 新数据直接往文件末尾加一行 |
| 容错 | 坏一行只影响一行,不会整个文件解析失败 |
| 工具友好 | HuggingFace、LLaMA-Factory 默认支持 |
2. 运行
powershell
chcp 65001 | Out-Null
$env:PYTHONUTF8="1"; $env:PYTHONIOENCODING="utf-8"
[Console]::OutputEncoding=[Text.Encoding]::UTF8
& "D:\miniconda3\envs\llm\python.exe" "D:\myAImodle\Class03\Chapter03\04_jsonl.py"
3. 预期输出
写入: D:\myAImodle\datasets\sample\sft_sample.jsonl
条数: 200
首个样例: {"instruction": "请解释显存的第0个要点", "input": "", "output": "太短"}
读回条数: 200
output 平均长度: 36.4
文件大小: 32445 字节
打开文件看一行:
powershell
Get-Content D:\myAImodle\datasets\sample\sft_sample.jsonl -TotalCount 2 -Encoding UTF8
4. 完整代码
python
import json
import random
from pathlib import Path
random.seed(42)
OUT_DIR = Path(r"D:\myAImodle\datasets\sample")
OUT_DIR.mkdir(parents=True, exist_ok=True)
OUT_FILE = OUT_DIR / "sft_sample.jsonl"
topics = ["微调", "LoRA", "量化", "数据清洗", "推理", "显存"]
rows = []
for i in range(200):
topic = random.choice(topics)
instruction = f"请解释{topic}的第{i % 20}个要点"
if i % 17 == 0 and rows:
instruction = rows[-1]["instruction"]
output = f"{topic}是训练中的关键概念。这是第{i}条示例,联系电话13800138000。"
if i % 23 == 0:
output = "太短"
rows.append({"instruction": instruction, "input": "", "output": output})
with OUT_FILE.open("w", encoding="utf-8") as f:
for row in rows:
f.write(json.dumps(row, ensure_ascii=False) + "\n")
print("写入:", OUT_FILE)
print("条数:", len(rows))
print("首个样例:", json.dumps(rows[0], ensure_ascii=False))
count = 0
total = 0
with OUT_FILE.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
obj = json.loads(line)
count += 1
total += len(obj["output"])
print("读回条数:", count)
print(f"output 平均长度: {total / count:.1f}")
print("文件大小:", OUT_FILE.stat().st_size, "字节")
5. 逐块拆解
第 1 块:固定随机种子
python
random.seed(42)
- 让「随机」每次运行结果相同,方便复现。
- 42 只是习惯用法,任何整数都行。
第 2 块:准备目录
python
OUT_DIR = Path(r"D:\myAImodle\datasets\sample")
OUT_DIR.mkdir(parents=True, exist_ok=True)
OUT_FILE = OUT_DIR / "sft_sample.jsonl"
mkdir(parents=True, exist_ok=True):父目录不存在就一起建,已存在也不报错。OUT_DIR / "文件名":路径拼接。
第 3 块:造数据
python
for i in range(200):
topic = random.choice(topics)
instruction = f"请解释{topic}的第{i % 20}个要点"
if i % 17 == 0 and rows:
instruction = rows[-1]["instruction"]
output = f"{topic}是训练中的关键概念。这是第{i}条示例,联系电话13800138000。"
if i % 23 == 0:
output = "太短"
rows.append({"instruction": instruction, "input": "", "output": output})
range(200):0~199。random.choice(topics):从列表里随机取一个。i % 20:取余,让问题有 20 种变化。i % 17 == 0:每 17 条制造一条重复,故意留脏数据给 Chapter 7 清洗。i % 23 == 0:每 23 条制造一条过短样本。- 每条是一个字典,字段固定为
instruction / input / output。
第 4 块:写 JSONL
python
with OUT_FILE.open("w", encoding="utf-8") as f:
for row in rows:
f.write(json.dumps(row, ensure_ascii=False) + "\n")
open("w"):覆盖写入。json.dumps(字典):字典 → JSON 字符串。ensure_ascii=False:必须加 ,否则中文会变成\u4e2d\u6587。+ "\n":每条一行,这是 JSONL 的关键。
第 5 块:按行读
python
for line in f:
line = line.strip()
if not line:
continue
obj = json.loads(line)
- 逐行读,内存占用恒定。
strip():去掉行尾换行和空白。continue:跳过空行,避免json.loads("")报错。json.loads(字符串):JSON 字符串 → 字典。
第 6 块:统计
python
count += 1
total += len(obj["output"])
- 条数和总长度累加。
- 最后算平均长度,这是最简单的数据体检。
6. 常见错误
| 现象 | 原因 | 解决 |
|---|---|---|
JSONDecodeError |
某行不是合法 JSON | 打印行号,检查该行 |
中文变成 \uXXXX |
没加 ensure_ascii=False |
加上即可 |
| 文件开头有 BOM 字符 | 被记事本存成 UTF-8 BOM | 用 Python 重写,或 utf-8-sig 读 |
| 读到的条数偏少 | 空行/最后一行没换行 | strip() + 跳过空行 |
| 写文件覆盖了旧数据 | 用了 "w" |
追加用 "a" |
| Windows 路径报错 | 用了普通字符串 | 用 r"..." |
7. 练习
- 把
range(200)改成 500,重新运行,条数是多少? - 给每条数据加一个字段
"source": "demo",写进 JSONL。 - 如果把
ensure_ascii=False去掉,文件内容会变成什么样?
8. 验收清单
- 能说出 JSON 和 JSONL 的区别
- 会写
json.dumps(..., ensure_ascii=False) - 会逐行读取并跳过空行
- 能统计条数和平均长度
9. 下一章
Class 3 Chapter 4:用 pandas 做长度分布统计。
附:练习答案
- 500 条(重复和过短样本也一起变多)。
- 在字典里加
"source": "demo",其余不变。 - 中文会变成
\u8bf7\u89e3\u91ca...这样的转义序列,程序仍能读,但人看不懂。