阶段 10 讲义
数学 11h:线代 · 向量组与线性方程组
编程 9h:文件读写与异常处理
第一部分 · 数学
阶段 9 我们学了"一个矩阵怎么求逆"。这一阶段回答更根本的问题:方程组到底有几个解?
答案来自一个词,就是秩。秩告诉你"这组方程真正提供了多少独立信息"。理解了秩,你就能不解方程直接判断解的情况。
1 向量是什么
向量的两种身份(都要有):
| 身份 | 说法 | 用途 |
|---|---|---|
| 几何 | 有方向的箭头 | 直观理解"方向""共线" |
| 代数 | 一列数 $\begin{pmatrix}1\\2\\3\end{pmatrix}$ | 能计算 |
记法:$\alpha = (1, 2, 3)$ 是行向量,$\alpha = \begin{pmatrix}1\\2\\3\end{pmatrix}$ 是列向量。
默认"向量"指列向量。
向量的基本运算:
$$\alpha + \beta = (a_1+b_1,\ a_2+b_2,\ a_3+b_3), \qquad k\alpha = (ka_1,\ ka_2,\ ka_3)$$
2 线性组合与线性表示
线性组合:把几个向量各乘一个数再加起来。
$$k_1\alpha_1 + k_2\alpha_2 + \cdots + k_m\alpha_m$$
线性表示:如果向量 $\beta$ 能写成上面这种形式,就说 $\beta$ 可由 $\alpha_1,\dots,\alpha_m$ 线性表示。
几何直觉(二维):
| 情况 | 能表示出什么 |
|---|---|
| 两个向量不共线 | 整个平面上的所有向量 |
| 两个向量共线 | 只有那条直线上的向量 |
⭐ 这就是"张成"(span)的意思:一组向量"张开"的空间有多大。
阶段 16 的 SVD,本质上就是在问"这个矩阵把空间张成了什么样"。
3 线性相关与线性无关 ⭐(本阶段核心概念)
3.1 定义
对一组向量 $\alpha_1, \dots, \alpha_m$,看方程
$$k_1\alpha_1 + k_2\alpha_2 + \cdots + k_m\alpha_m = 0$$
- 只有全零解($k_1 = k_2 = \cdots = 0$)→ 线性无关
- 存在不全为零的解 → 线性相关
3.2 直觉(这是关键)
| 术语 | 直觉 | 二维例子 |
|---|---|---|
| 线性无关 | 每个向量都贡献了新方向,谁也不能由别人凑出来 | $(1,0)$ 和 $(0,1)$ |
| 线性相关 | 有人是多余的,能被别人线性表示 | $(1,0)$ 和 $(2,0)$(后者是前者的 2 倍) |
线性相关 = 队伍里有"冗余的人"。
3.3 完整例子
判断 $\alpha_1 = (1,2,3)$,$\alpha_2 = (2,4,6)$,$\alpha_3 = (1,0,1)$ 是否线性相关。
第一步:观察
$$\alpha_2 = (2,4,6) = 2\times(1,2,3) = 2\alpha_1$$
$\alpha_2$ 是 $\alpha_1$ 的 2 倍,它没提供任何新方向。
第二步:写出关系式
$$-2\alpha_1 + 1\cdot\alpha_2 + 0\cdot\alpha_3 = 0$$
系数 $(-2, 1, 0)$ 不全为零,等式却成立 → 线性相关 ✓
第三步(通用方法):用秩验证
把三个向量拼成矩阵,算秩:
$$M = \begin{pmatrix} 1 & 2 & 1 \\ 2 & 4 & 0 \\ 3 & 6 & 1 \end{pmatrix}, \qquad \text{rank}(M) = 2$$
秩 2 < 向量个数 3 → 线性相关 ✓(有一个是冗余的)
💡 为什么秩能判定:秩 = "独立的向量个数"(下面第 4 节详讲)。
秩 < 向量个数,说明有向量没能贡献新方向,就是冗余,就是线性相关。
3.4 两条特殊规则(考试很常用)
规则一:含有零向量的向量组一定线性相关。
因为 $1\cdot 0 + 0\cdot\alpha_1 + \cdots = 0$,系数 $(1,0,\dots)$ 不全为零。
规则二:向量个数 > 向量维数 → 一定线性相关。
直觉:在二维平面上,给你 3 个向量,必然有多余的,因为平面只有 2 个独立方向。
⭐ 这条规则很深刻:它说的是"你不能凭空造出新方向"。
在 $n$ 维空间里,独立方向最多只有 $n$ 个。这是后面"秩 ≤ min(行数, 列数)"的来源。
4 秩:一组向量"真正有多少个独立方向"
秩(rank)的几个等价说法:
| 说法 | 含义 |
|---|---|
| 矩阵的秩 | 化成阶梯形后,非零行的个数 |
| 向量组的秩 | 这组向量中极大线性无关组的向量个数 |
| 本质 | 独立方向的个数 |
关键不等式(必记):
$$\text{rank}(A) \le \min(m, n)$$
一个 $m\times n$ 矩阵,秩最多是行数和列数里小的那个。
满秩:$\text{rank}(A) = \min(m,n)$(达到了上限)。
4.1 完整例子
求 $A = \begin{pmatrix} 1 & 2 & 3 \\ 2 & 4 & 6 \\ 1 & 1 & 1 \end{pmatrix}$ 的秩。
观察:第 2 行 = 第 1 行 × 2(冗余),所以秩最多 2。
用行变换化阶梯形:
$$A = \begin{pmatrix} 1 & 2 & 3 \\ 2 & 4 & 6 \\ 1 & 1 & 1 \end{pmatrix} \xrightarrow{R_2 - 2R_1} \begin{pmatrix} 1 & 2 & 3 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \end{pmatrix} \xrightarrow{R_3 - R_1} \begin{pmatrix} 1 & 2 & 3 \\ 0 & 0 & 0 \\ 0 & -1 & -2 \end{pmatrix}$$
交换第 2、3 行,得阶梯形:
$$\begin{pmatrix} 1 & 2 & 3 \\ 0 & -1 & -2 \\ 0 & 0 & 0 \end{pmatrix}$$
非零行有 2 行 → $\text{rank}(A) = 2$ ✓(sympy 实算确认)
5 高斯消元法与"行最简形"
高斯消元:反复用三种行变换把矩阵化简。
| 行变换 | 说明 | 对解的影响 |
|---|---|---|
| 交换两行 | $R_i \leftrightarrow R_j$ | 解不变 |
| 某行乘非零数 | $R_i \times k$ | 解不变 |
| 某行加另一行的 $k$ 倍 | $R_i + kR_j$ | 解不变 |
为什么这三种不改变解:它们对应"把方程换个顺序""方程两边同乘""两个方程相加",都是等价变形。
行最简形(rref):继续消元,让每个主元都是 1,且所在列其余元素都是 0。
$$A \xrightarrow{\text{行变换}} \begin{pmatrix} 1 & 0 & -1 \\ 0 & 1 & 2 \\ 0 & 0 & 0 \end{pmatrix}$$
这就是"解已经摆在眼前"的形式。
💡 rref 是 numpy/sympy 里算秩和求解的实际方法。
阶段 8 说的"计算机用高斯消元",具体就是它。
6 线性方程组解的结构 ⭐
6.1 齐次方程组 $AX = 0$
永远有零解 $X = 0$。问题是"有没有非零解"。
$$\boxed{\text{有非零解} \iff \text{rank}(A) < n \ (\text{未知数个数})}$$
直觉:$n$ 个未知数需要 $n$ 条独立的约束才能被完全钉死。
约束不够(秩 < $n$),就有自由度,解就不唯一。
6.2 完整例子(齐次)
解 $\begin{cases} x + y - z = 0 \\ 2x - y + z = 0 \\ 3x + 0y + 0z = 0 \end{cases}$
系数矩阵 $H = \begin{pmatrix} 1 & 1 & -1 \\ 2 & -1 & 1 \\ 3 & 0 & 0 \end{pmatrix}$,$\det(H) = 0$,$\text{rank}(H) = 2$。
秩 2 < 未知数 3 → 有非零解 ✓
化 rref:
$$H \to \begin{pmatrix} 1 & 0 & 0 \\ 0 & 1 & -1 \\ 0 & 0 & 0 \end{pmatrix}$$
读出来:$x = 0$,$y - z = 0$,即 $y = z$。
通解($z$ 是自由变量):
$$\begin{pmatrix} x \\ y \\ z \end{pmatrix} = t\begin{pmatrix} 0 \\ 1 \\ 1 \end{pmatrix}, \qquad t \in \mathbb{R}$$
$\begin{pmatrix}0\\1\\1\end{pmatrix}$ 叫基础解系,齐次方程组的解是它的所有倍数。(sympy 实算:通解 $\{(0, z, z)\}$ ✓)
💡 自由变量个数 = 未知数个数 − 秩 = $3 - 2 = 1$。
有几个自由变量,基础解系就有几个向量,这是最快的检查法。
6.3 非齐次方程组 $AX = b$
解的结构定理:
$$\text{通解} = \underbrace{\text{一个特解}}_{\eta^*} + \underbrace{\text{齐次通解}}_{k_1\xi_1 + \cdots}$$
直觉:非齐次方程组的解,是"一个具体的解"加上"所有让它等于 0 的扰动"。
几何图像:一条不过原点的直线(特解定位置)+ 它的方向(齐次解定方向)。
6.4 完整例子(非齐次,无穷多解)
解 $\begin{cases} x + 2y + 3z = 1 \\ 2x + 4y + 6z = 2 \\ x + y + z = 2 \end{cases}$
检查秩:$\text{rank}(A) = 2$,$\text{rank}([A \mid b]) = 2$,相等 → 有解。
未知数 3 > 秩 2 → 无穷多解。
通解(sympy 实算):
$$\begin{pmatrix} x \\ y \\ z \end{pmatrix} = \begin{pmatrix} 3 \\ -1 \\ 0 \end{pmatrix} + t\begin{pmatrix} 1 \\ -2 \\ 1 \end{pmatrix}$$
验算特解 $(3,-1,0)$:$3 + 2(-1) + 3(0) = 1$ ✓,$2(3)+4(-1)+6(0) = 2$ ✓,$3 - 1 + 0 = 2$ ✓
验算齐次部分 $(1,-2,1)$:$1 + 2(-2) + 3(1) = 0$ ✓,$1 - 2 + 1 = 0$ ✓
7 解的存在性判定(一张表解决所有情况)
记 $r_A = \text{rank}(A)$,$r_b = \text{rank}([A \mid b])$,$n$ = 未知数个数。
| 情况 | 判定 | 解 |
|---|---|---|
| 无解 | $r_A < r_b$ | 方程组矛盾 |
| 唯一解 | $r_A = r_b = n$ | 一组解 |
| 无穷多解 | $r_A = r_b < n$ | 有 $n - r_A$ 个自由变量 |
注意 $r_A \le r_b$ 恒成立,增广矩阵只多了"一列",秩不可能比原来小。
7.1 三个例子对照(都用 sympy 实算过)
例一(无解):$\begin{cases} x + 2y = 1 \\ 2x + 4y = 3 \end{cases}$
$r_A = 1$,$r_b = 2$,$r_A < r_b$ → 无解 ✓
几何:两条平行直线。
例二(无穷多解):$\begin{cases} x + 2y + 3z = 1 \\ 2x + 4y + 6z = 2 \end{cases}$
$r_A = r_b = 1 < 3$ → 无穷多解,自由变量 $3 - 1 = 2$ 个 ✓
几何:两个重合的平面。
例三(唯一解):$\begin{cases} 2x + y - z = 1 \\ x - y + z = 2 \\ 3x + 2y = 3 \end{cases}$
$\det = -6 \ne 0$,$r_A = r_b = 3 = n$ → 唯一解 $(1, 0, 1)$ ✓
验算:$2(1)+0-1 = 1$ ✓,$1-0+1 = 2$ ✓,$3+0+0 = 3$ ✓
8 常见的坑
| 坑 | 说明 |
|---|---|
| 把"有解"和"唯一解"混为一谈 | 有解包含唯一解和无穷多解,要分开判 |
| 忘记检查增广矩阵的秩 | 只算 $\text{rank}(A)$ 不够,必须算 $\text{rank}([A\mid b])$ |
| 以为秩可以大于 $\min(m,n)$ | 不可能。$2\times3$ 矩阵秩最多 2 |
| 以为"向量多就一定相关" | 要看维数:$n$ 维空间里超过 $n$ 个才必然相关 |
| 齐次方程组说"无解" | 齐次永远有零解。要么唯一(零解),要么无穷多 |
| 自由变量个数算错 | 是 $n - \text{rank}$,不是 $n - $ 方程个数 |
| 行变换时忘了"某行乘 $k$ 要 $k \ne 0$" | 乘 0 会把整行抹掉,解就变了 |
| 混淆"列满秩"和"行满秩" | 要看秩等于的是行数还是列数 |
9 🧠 这跟认知科学有什么关系
这一节的对应关系是我验证过的(用代码实算),但心理学结论请对照文献。
秩亏(rank deficiency)= 参数不可识别(non-identifiability),实验设计里最致命的问题。
9.1 关键前提
回归系数 $\hat\beta = (X^TX)^{-1}X^Ty$ 能算出来的前提,是设计矩阵 $X$ 满列秩。
秩亏 = 你设计的实验根本区分不出你想区分的参数。
9.2 实算给你看
我模拟了 50 个被试的数据,故意让自变量 $x_2$ 几乎等于 $x_1$:
| 量 | 实算结果 |
|---|---|
| 设计矩阵 $X$ 形状 | $50 \times 4$ |
| 4 个奇异值 | $9.71,\ 7.17,\ 6.07,\ \mathbf{3.91\times10^{-10}}$ |
| 条件数 | $\mathbf{2.48\times10^{10}}$ |
看最后那个奇异值:$3.91\times10^{-10}$,实际上是 0。
我把 $x_2$ 换成独立随机的变量再看:
| 量 | 实算结果 |
|---|---|
| 4 个奇异值 | $8.06,\ 6.98,\ 6.19,\ 5.85$ |
| 条件数 | $1.38$ |
对比:条件数从 $2.5\times10^{10}$ 掉到 $1.38$,差了十个数量级。
⚠️ 一个非常值得注意的细节:
np.linalg.matrix_rank(X)在上面那个共线例子里返回了 4(看似满秩)!
这是因为它用了默认容差判断。
教训:判断共线性不能只看matrix_rank,一定要看条件数。
matrix_rank说"满秩",条件数说"几乎奇异",后者才是你要信的。
9.3 秩亏在实验设计里长什么样
| 设计错误 | 数学后果 | 通俗说法 |
|---|---|---|
| 两个条件完全共线(如"条件 A"列 = "非条件 B"列) | 秩亏 | 两个参数在抢同一份数据 |
| 忘了去掉截距列,又加了全 1 的"组间"列 | 秩亏 | 同一信息说了两遍 |
| 被试所有试次都是同一个条件 | 该被试的对比估计不出 | 没有变化,就没有信息 |
| 自变量线性相关(接近但不完全) | 秩不满但数值上能算 | 能算,但结果不可信(阶段 8/9 讲过) |
秩 = 你的数据里"独立的约束"有多少条。
约束不够,参数就定不下来;约束重复,参数就打架。这是所有统计模型的地基。
9.4 这对你做研究意味着什么
| 现象 | 检查方法 |
|---|---|
| 模型报错 "singular matrix" / "rank deficient" | 算 $\text{rank}(X)$,看是否 < 列数 |
| 系数大得离谱、符号乱跳 | 算条件数,> 30 就警惕,> 1000 基本不可信 |
| 去掉一个自变量,其他系数剧烈变化 | 看自变量间的相关矩阵 |
💡 务实建议:分析数据前,先算一次设计矩阵的条件数。
这一步只要一行代码,却能提前发现最致命的错误,比你跑完模型再debug省事得多。⚠️ 诚实说明:
- 上面的奇异值、条件数都是我实算的(可复现),"满列秩 ⇒ 可唯一识别"
"秩亏 ⇒ 参数不可识别"是数学/统计事实。
- 但"该怎么处理共线""什么条件下可以接受"是方法学问题,
请以回归分析教材(如 Fox 的 Applied Regression Analysis)和方法论文献为准。
我保证数学,不保证研究规范。
10 数学作业 S10
- [ ] 1. 判断 $\alpha_1 = (1,1)$,$\alpha_2 = (1,-1)$ 是否线性无关(用定义)
- [ ] 2. 判断 $(1,0,0)$、$(0,1,0)$、$(1,1,0)$ 是否线性相关,并写出一个不全为零的关系式
- [ ] 3. 求 $\begin{pmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 3 & 6 & 3 \end{pmatrix}$ 的秩
- [ ] 4. 用行变换把 $\begin{pmatrix} 2 & 1 & -1 \\ 1 & -1 & 1 \\ 3 & 2 & 0 \end{pmatrix}$ 化成阶梯形,求秩
- [ ] 5. 判断 $\begin{cases} x+y-z=0 \\ 2x-y+z=0 \\ x+2y-2z=0 \end{cases}$ 是否有非零解,若有,求基础解系
- [ ] 6. 判断下列方程组解的情况(不解,只看秩):
(a) $\begin{cases} x+y=1 \\ 2x+2y=3 \end{cases}$ (b) $\begin{cases} x+y=1 \\ x-y=3 \end{cases}$ (c) $\begin{cases} x+y+z=1 \\ 2x+2y+2z=2 \end{cases}$
- [ ] 7. 求 $\begin{cases} x+2y+3z=1 \\ 2x+4y+6z=2 \\ x+y+z=2 \end{cases}$ 的通解(特解 + 齐次通解)
- [ ] 8.(重点)用三种方法判断 $\begin{cases} x+2y=1 \\ 3x+6y=5 \end{cases}$ 无解:行变换、秩比较、几何(两条直线)
- [ ] 9.(Python)用 numpy 求作业 4 的秩、作业 7 的通解,并计算条件数判断方程组是否病态
第 9 题参考代码:
import numpy as np
# --- 作业 4:秩 ---
A = np.array([[2., 1., -1.],
[1., -1., 1.],
[3., 2., 0.]])
print("A 的秩 =", np.linalg.matrix_rank(A))
print("det =", np.linalg.det(A))
# 秩 = 3 = 列数 -> 满秩
# --- 作业 7:通解 ---
M = np.array([[1., 2., 3.],
[2., 4., 6.],
[1., 1., 1.]])
b = np.array([1., 2., 2.])
print("rank(A) =", np.linalg.matrix_rank(M))
print("rank([A|b]) =", np.linalg.matrix_rank(np.column_stack([M, b])))
# 用最小二乘解出一个特解(无穷多解时它给的是"最小范数解")
sol, residuals, rank, sv = np.linalg.lstsq(M, b, rcond=None)
print("最小范数解 =", sol)
# 手算验证通解 (3,-1,0) + t*(1,-2,1)
t = np.linspace(-1, 1, 3)
for ti in t:
x = np.array([3., -1., 0.]) + ti * np.array([1., -2., 1.])
assert np.allclose(M @ x, b), f"t={ti} 不满足方程"
print("通解族全部满足方程 ✓")
# --- 条件数:判断是否病态 ---
print("A 的条件数 =", np.linalg.cond(A))
# 对照:接近奇异的矩阵
S = np.array([[1., 2., 3.],
[2., 4., 6.0000001],
[1., 1., 1.]])
print("S 的条件数 =", np.linalg.cond(S), " <- 大得离谱,不可信")
💡
np.linalg.lstsq是个"兜底"工具:方程组无解时它给最小二乘解,
无穷多解时它给范数最小的那个解。做回归时它就是主力。⚠️ 但注意:
lstsq会给"某个"解,不会告诉你解不唯一。
必须自己先检查秩,这是本阶段最重要的一课。
第二部分 · 编程
11 为什么要读写文件
到目前为止,你的数据都是直接写死在代码里的:
data = [1, 2, 3] # 关了程序就没了
真实工作里,数据在文件里:实验软件导出的 .csv、仪器记录的 .txt、网页下载的 .json。
读文件 = 把数据从磁盘搬进内存;写文件 = 把结果存下来。
⚠️ 程序的内存是"易失"的,程序一关,内存里的东西全消失。
想让结果留到明天,就必须写进文件。这是你从"练习"进入"做研究"的分界线。
12 读文件:with open(...)
标准写法(一定要用 with):
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read() # 一次读全部,得到一个字符串
print(content)
逐个部分拆解:
| 部分 | 作用 |
|---|---|
open(路径, 模式, encoding=...) |
打开文件,返回一个文件对象 |
"r" |
模式:r 读、w 写(覆盖)、a 追加、b 二进制 |
encoding="utf-8" |
编码。处理中文必须写,否则可能乱码或报错 |
as f |
给文件对象起名 f |
with ...: |
离开缩进块时自动关闭文件,哪怕中途报错 |
12.1 三种读法
with open("data.txt", "r", encoding="utf-8") as f:
whole = f.read() # ① 整个文件 → 一个字符串
with open("data.txt", "r", encoding="utf-8") as f:
lines = f.readlines() # ② 所有行 → 列表(每行末尾带 \n)
with open("data.txt", "r", encoding="utf-8") as f:
for line in f: # ③ 逐行处理(大文件必须用这个)
print(line.strip())
⚠️
readlines()会把整个文件读进内存。文件几百 MB 时会卡死甚至崩掉。
大文件一定用 ③ 逐行读取,这是"数据科学"里第一个真正会踩的性能坑。
12.2 为什么一定要 with
不用 with 的写法也有:
f = open("data.txt", "r", encoding="utf-8")
content = f.read()
f.close() # 必须手动关!
问题:如果 f.read() 那行报错了,f.close() 就不会执行,
文件句柄一直占着,久了会耗尽系统资源。
with 保证无论如何都会关闭,所以永远用 with。
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# 出了这个缩进块,f 自动关闭
⭐ 这就是"上下文管理器":进入时准备好资源,退出时自动清理。
阶段 12 讲面向对象时你会看到它的实现原理。
13 写文件
with open("output.txt", "w", encoding="utf-8") as f:
f.write("第一行\n") # ⚠️ write 不会自动换行!
f.write("第二行\n")
f.writelines(["a\n", "b\n"]) # 写一个列表,同样要自己加 \n
⚠️ 两个新手必踩的坑:
①write()不会自动加换行,必须自己写"\n"。这和print()不一样!
②"w"模式会清空原文件,如果文件已存在,内容立刻全没了。
想追加内容用"a"模式。
with open("log.txt", "a", encoding="utf-8") as f:
f.write("又追加了一行\n") # 追加,不覆盖原有内容
⚠️
"w"清空是不可撤销的!打开文件前先想清楚你要的是"w"还是"a"。
14 路径:文件到底在哪
这是新手最容易懵的地方,代码明明没错,但报 FileNotFoundError。
两种路径:
| 类型 | 例子 | 特点 |
|---|---|---|
| 绝对路径 | /Users/mamingyu/Desktop/data.txt |
从根目录开始,换电脑就失效 |
| 相对路径 | data.txt 或 ../data.txt |
相对于当前工作目录 |
⚠️ "相对路径相对于谁"是所有人第一次都会搞错的点:
相对路径的基准是"运行时你所在的工作目录",不是脚本文件所在的目录。
同一个脚本,从不同目录运行,相对路径的结果完全不同。
推荐写法:用 pathlib 基于脚本自身位置构造路径
from pathlib import Path
# __file__ 是"当前脚本的路径",.resolve() 拿到绝对路径
这里 = Path(__file__).resolve().parent # 脚本所在目录
数据文件 = 这里 / "data.txt" # / 运算符用来拼路径
print(数据文件)
with open(数据文件, "r", encoding="utf-8") as f:
print(f.read())
这样写,无论从哪个目录运行脚本、无论把文件夹搬到哪,都能找到文件。
⭐
pathlib是现代 Python 的推荐做法,比老的os.path.join更好读。
Path("a") / "b"就是a/b,跨平台自动选对分隔符(Windows 用\)。
常用操作:
from pathlib import Path
p = Path("data") / "raw.txt"
print(p.exists()) # 文件/目录存在吗
print(p.is_file()) # 是文件吗
print(p.suffix) # '.txt' 扩展名
print(p.stem) # 'raw' 不含扩展名的名字
print(p.parent) # Path('data') 所在目录
p.parent.mkdir(parents=True, exist_ok=True) # 建目录(已存在也不报错)
💡
parents=True, exist_ok=True这两个参数几乎总要带上:
parents=True表示"父目录不存在就一起建",exist_ok=True表示"已存在别报错"。
少了它们,重跑脚本就会因为"目录已存在"而崩。
15 异常处理:try / except
异常(exception) = 程序运行时出的错。程序会当场停下,除非你处理它。
x = int("abc") # ValueError: invalid literal for int() with base 10: 'abc'
处理它:
try:
x = int("abc")
except ValueError:
print("转换失败,用了默认值")
x = 0
print(x) # 0 ← 程序没崩,继续跑
结构:
try:
... # 可能出错的代码
except 某种错误 as e:
... # 出错时做什么
else:
... # 没出错时才做(可选)
finally:
... # 无论如何都做(可选,常用于清理)
15.1 常见异常类型(必须认识)
| 异常 | 什么时候出现 | 例子 |
|---|---|---|
FileNotFoundError |
文件不存在 | open("没有的文件.txt") |
ValueError |
类型对但值不对 | int("abc") |
TypeError |
类型不对 | 1 + "a" |
KeyError |
字典键不存在 | d["没有的键"] |
IndexError |
索引越界 | [1,2][10] |
ZeroDivisionError |
除以零 | 1 / 0 |
PermissionError |
没权限 | 写系统目录 |
💡 技能卡 1(读懂报错)的报错,最后一行就是异常类型和原因。
现在你能主动抓到它了,这是从"被动挨打"到"主动防守"的转折。
15.2 什么时候该用 try/except
该用:
# 读一个可能不存在的文件
try:
with open(path, encoding="utf-8") as f:
data = f.read()
except FileNotFoundError:
print(f"文件不存在:{path},跳过")
data = ""
不该用:
# ❌ 把大段代码包起来,"什么都抓"
try:
result = 复杂计算(a, b, c)
save(result)
send_email(result)
except Exception: # 抓了所有异常
pass # 什么也不做
⚠️ 两种滥用必须避免:
①except Exception: pass,把所有错误静默吞掉。
程序不崩了,但结果是错的你也不知道。这是最有害的写法。
② 用 try/except 掩盖你该修的逻辑错误,异常处理是处理"意外",不是处理"我写错了"。⭐ 原则:尽量抓具体的异常类型(
ValueError而不是Exception),
并且在 except 里做点实事(记日志、给默认值、明确地跳过)。
16 🔧 技能卡 10:文件读写与路径
技能卡 9 教你处理字符串;这张卡教你"数据从哪来、到哪去",这是科研代码里最容易出错的一环。
16.1 决策一:我要读还是写?
| 需求 | 模式 | 注意 |
|---|---|---|
| 读 | "r" |
文件必须存在,否则 FileNotFoundError |
| 覆盖写 | "w" |
⚠️ 立刻清空原内容 |
| 追加 | "a" |
在末尾加 |
| 读二进制(图片/模型) | "rb" |
不做编码转换 |
读文件一律用 with,没有例外。
16.2 决策二:路径怎么写才不会崩?
| 写法 | 什么时候用 |
|---|---|
Path(__file__).parent / "data.txt" |
推荐。脚本要给别人用、要换电脑 |
"data.txt" |
只在"确定工作目录"的临时脚本里 |
/Users/mamingyu/... |
⚠️ 永远不要写死,换电脑就废 |
16.3 决策三:要不要 try/except?
| 情况 | 处理 |
|---|---|
| 文件可能不存在,且你能给合理默认 | 用 try/except FileNotFoundError |
| 文件必须存在,没有它程序无意义 | 不要抓,让它崩,你才知道 |
| 网络请求 / 用户输入 | 一定要抓 |
16.4 三条新手铁律
① 编码永远写 encoding="utf-8"
不写的话,Python 用系统默认编码。处理中文时非常容易出问题,而且
在你的电脑上能跑、在别人电脑上崩,这种 bug 最难查。
② 打开文件前先 print 一下路径
from pathlib import Path
p = Path(__file__).parent / "data.txt"
print("准备读取:", p)
print("文件存在吗?", p.exists()) # ← 这一行能省你半小时
③ 写文件先确认是 "w" 还是 "a"
"w" 会清空原来的内容。处理原始数据时永远不要用 "w",
原始数据只读不写,这是数据安全的第一原则。
⚠️ 一个真实的灾难场景:你想把处理结果存进
data.txt,
写成了open("data.txt", "w")而不是open("clean_data.txt", "w"),
原始数据当场被清空,无法恢复。防呆做法:原始数据文件名里带
raw,处理结果带clean,且从不往 raw 文件里写。
17 🧠 这跟认知科学有什么关系(编程侧)
概念性内容,无法用代码验证,请对照文献。
文件读写 ↔ "可复现性"(reproducibility),近年认知科学最受关注的方法学危机。
17.1 为什么"读写文件"这么基础的事,值得单独讲
因为它决定你的研究能不能被别人复现。
| 做法 | 后果 |
|---|---|
| 数据在代码里写死 | 换一批数据要改代码 → 别人无法复现 |
| 路径写死绝对路径 | 别人电脑上跑不了 → 无法复现 |
| 手工在 Excel 里改数据 | 中间步骤无人知道 → 无法复现 |
| 分析脚本化 + 数据文件只读 | ✓ 一条命令重跑全部结果 |
17.2 认知科学的"可复现危机"(replication crisis)
2015 年,Open Science Collaboration 做了一个大规模重复研究:
100 项已发表的心理学研究,只有约 36% 得到了显著重复结果。
原因很复杂,但其中一个技术因素是:很多研究没有共享数据和分析脚本,
别人无法知道"到底做了什么处理"。
解决办法在工程上很朴素:
| 做法 | 对应本阶段学的什么 |
|---|---|
| 数据用文件存,不写死在代码里 | open() 读文件 |
| 路径用相对路径/脚本相对路径 | pathlib + __file__ |
| 处理步骤全部写成脚本 | 而不是手工操作 |
| 原始数据只读,结果另存 | "r" 读、"w" 写到新文件 |
"可复现"在技术上的最低要求,就是"把数据处理写成能一条命令跑完的脚本"。
这正是你从这一阶段开始要做的事。
17.3 异常处理在研究代码里的角色
| 场景 | 该怎么做 |
|---|---|
| 一个被试的数据文件缺失 | try/except FileNotFoundError,记录下来,继续处理其他人 |
| 某行数据格式不对 | try/except ValueError,输出是哪一行,不要静默跳过 |
| 批量处理 200 个文件 | 必须能报告"哪几个失败了",而不是"跑了但少了一半" |
⚠️ 最危险的做法是
except: pass,
你的数据里少了一批被试,但程序"成功"跑完了,你还以为一切正常。
科研代码里,"静默失败"比"崩溃"危险得多。⚠️ 诚实说明:
- 上面提到的重复率约 36% 来自 Open Science Collaboration (2015, Science) 的
大规模重复研究,具体数字请核对原文;
- "数据共享不足是可复现危机的原因之一"是学界的普遍共识,但各领域严重程度不同;
- 这一节是让你知道"为什么工程习惯在研究里重要",不是让你把它当结论。
请以方法论文献为准。
18 编程作业 S10
- [ ] 1. 创建
hello.txt,写入三行中文(注意编码),再用with读回来打印 - [ ] 2. 用
"a"模式往hello.txt追加一行,再读回来确认是 4 行 - [ ] 3. 写
count_lines(path),返回文件的行数(逐行读,不用readlines) - [ ] 4. 用
pathlib构造"脚本同目录下的 data.txt"路径,打印它、判断是否存在 - [ ] 5. 写
safe_int(s, default=0),转换失败时返回默认值(用 try/except) - [ ] 6.(重点)写出
"w"和"a"的区别,并用一个实验证明(先写内容,再用两种模式打开) - [ ] 7. 下面代码有两个 bug,找出来并说明后果:
with open("data.txt", "r") as f: for line in f: print(line)
提示:一个和编码有关,一个和输出格式有关
- [ ] 8. 写read_records(path),读一个 CSV 文件,返回元组列表
要求:跳过空行、去掉空格、转换类型、报错时说明是第几行
- [ ] 9.(技能)写describe(path),打印"文件存在吗/多大/多少行/前 3 行",用于调试路径问题
- [ ] 10.(Python)写clean_folder(raw_path, out_path),读原始数据、清洗后写到新文件(原始文件不动)
第 3 题参考写法:
from pathlib import Path
def count_lines(path):
"""数文件有多少行。用逐行读取,大文件也不会吃爆内存。"""
count = 0
with open(path, "r", encoding="utf-8") as f:
for line in f: # 逐行读,一次只有一行的内存开销
count += 1
return count
# 先造一个测试文件
p = Path("t3.txt")
p.write_text("第一行\n第二行\n第三行\n", encoding="utf-8")
print(count_lines(p)) # 3
第 5 题参考写法:
def safe_int(s, default=0):
"""把字符串转成整数。转不了就返回默认值,不抛异常。"""
try:
return int(s.strip()) # strip 去掉前后的空格
except (ValueError, AttributeError):
return default
print(safe_int("42")) # 42
print(safe_int("abc")) # 0
print(safe_int("", -1)) # -1
print(safe_int(None, -1)) # -1 None 没有 strip -> AttributeError,也被抓了
💡 为什么抓两个类型:
int("abc")抛ValueError,
但None.strip()抛AttributeError。抓多个类型要用元组:
except (A, B):。这是新手常漏的地方。
第 8 题参考写法:
from pathlib import Path
def read_records(path):
"""读 CSV,返回 [(被试, 反应时, 正确), ...],并报告出错的行号。"""
records = []
with open(path, "r", encoding="utf-8") as f:
for lineno, line in enumerate(f, start=1): # 从 1 开始数行号
line = line.strip()
if not line: # 空行跳过
continue
parts = line.split(",")
try:
subject = parts[0].strip()
rt = float(parts[1].strip())
correct = bool(int(parts[2].strip()))
except (IndexError, ValueError) as e:
# 关键:报出是第几行,便于排查,而不是静默跳过
print(f"⚠️ 第 {lineno} 行格式错误,已跳过:{line!r} ({e})")
continue
records.append((subject, rt, correct))
return records
# 造测试数据
p = Path("t8.csv")
p.write_text(
"A01, 0.523, 1\n"
"\n"
"A02, 1.234, 0\n"
"坏行, 没有逗号\n"
"A03, 0.789, 1\n",
encoding="utf-8",
)
data = read_records(p)
print(data)
# ⚠️ 第 4 行格式错误,已跳过:'坏行, 没有逗号' (could not convert string to float: '没有逗号')
# [('A01', 0.523, True), ('A02', 1.234, False), ('A03', 0.789, True)]
assert len(data) == 3 # 坏行被跳过,空行也被跳过
assert data[0] == ("A01", 0.523, True)
print("检查用例通过")
第 9 题参考写法:
from pathlib import Path
def describe(path):
"""调试路径问题的万能小工具:文件到底在不在、长什么样。"""
p = Path(path)
print(f"路径 : {p}")
print(f"绝对路径 : {p.resolve()}")
print(f"存在吗 : {p.exists()}")
if not p.exists():
print(" -> 文件不存在,检查路径或先创建它")
return
print(f"是文件吗 : {p.is_file()}")
print(f"大小 : {p.stat().st_size} 字节")
print(f"行数 : {len(p.read_text(encoding='utf-8').splitlines())}")
print("前 3 行 :")
with open(p, "r", encoding="utf-8") as f:
for i, line in enumerate(f):
if i >= 3:
break
print(f" {i + 1}: {line.rstrip()}")
describe("t8.csv")
💡 把这个
describe()存进你的工具箱。以后遇到FileNotFoundError,
第一件事就是把它指向那个路径,比瞎猜快得多。
第 7 题答案:
# Bug 1:没写 encoding="utf-8"
# 中文系统上可能乱码,换到别的电脑上可能直接 UnicodeDecodeError
# Bug 2:print(line) 会多打一个空行
# 因为文件每行末尾本来就有 \n,print 又加一个 \n -> 两个换行
# 修正版
with open("data.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.rstrip()) # 去掉行尾换行,避免多空行
# 或者用 print(line, end="") 让 print 不加换行
本阶段自评
- 数学:线性相关和线性无关的定义是什么?"相关"的直觉是什么?
- 数学:为什么"$n$ 维空间里超过 $n$ 个向量一定相关"?
- 数学:秩的三种说法是什么?秩的上限是多少?
- 数学:齐次方程组什么时候有非零解?判定条件是什么?
- 数学:非齐次方程组的解由哪两部分组成?几何上怎么理解?
- 数学:三种解的情况(无解/唯一/无穷多)分别对应什么秩的关系?
- 数学:为什么只算 $\text{rank}(A)$ 不够,还要算 $\text{rank}([A\mid b])$?
- 编程:为什么读文件一定要用
with?不用会有什么风险? - 编程:
"w"和"a"有什么区别?处理原始数据该用哪个? - 编程:为什么路径不能写死成
/Users/mamingyu/...?推荐怎么写? - 编程:
except Exception: pass为什么危险? - 技能:遇到
FileNotFoundError,你的第一步该做什么? - 连接:为什么"秩亏"在实验设计里是致命的?怎么提前发现?
全部答得上 → 进阶段 11。
有任何一个答不上 → 回去补对应小节,别跳。