简介:围绕《程序员数学》整理的配套源码与练习包,面向希望用Python打通线性代数与微积分的IT从业者、初学者及有一定编程基础的开发者。内容与书本章节对应,涵盖向量与矩阵运算、行列式与特征值、线性变换、极限与导数、积分及微分方程等知识点,并针对原书示例中的错误与bug做了修正,便于读者直接运行代码验证结论、对照图表理解抽象概念。压缩包共104个文件,以74个Python脚本和17个Jupyter Notebook为主,另有7张jpg图示、3个off模型文件及说明文档,整体约13.55MB,按章节组织便于检索复现。已有1653人学习下载。章节walkthrough笔记本可循序渐进演练计算过程,配图辅助观察二维三维空间中的向量与函数变化,修正后的脚本减少排错干扰,适合边读书边动手实践、巩固数学直觉与编程实现能力。
1. 从一张空白图说起:数学书的配套代码为什么总要先修一遍
最反直觉的一次翻车,是同一份 Python 源码一个字没改,昨天还能画出的函数曲线,今天打开只剩一张白板。这套配合某本数学教材的代码,按章节把线性代数和微积分的例子逐个实现,书里印错的地方也做过勘误和 bug 修正,可它照样会因为你机器上的一个后端、一个版本号、一次路径写歪而全线报错。带着报错信息来找答案的人,卡住的地方高度集中:import 就崩、矩阵一乘就shapes not aligned、图出不来、算到最后是nan。这些不是数学没学懂,是环境和数值细节在捣鬼。下面按「现象—根因—改法」一条条拆,命令都能直接抄。
2. 导入就崩:ModuleNotFoundError 与 numpy 属性错误的定位顺序
配套源码第一个要过的坎,往往不是公式,而是import。线性代数和微积分的例子绕不开numpy、scipy、matplotlib、sympy这几个库,而它们恰好是版本变动最频繁的一批。同一行import numpy as np,在不同机器上会演出三种完全不同的结局:包根本没装、包装了但装到了别的解释器里、包装了但书里的旧写法在新版本上被删了。这三种结局的修法完全不同,混在一起试只会越试越乱,所以顺序必须是「先确认解释器,再确认版本,最后才改代码」。
2.1 先确认报错来自哪个解释器
最常见的迷惑场景是这样的:终端里pip install numpy打印Successfully installed numpy,切回编辑器一运行,仍然ModuleNotFoundError: No module named 'numpy'。不用怀疑人生,八成是pip和python根本不是同一个东西。系统自带的 Python、包管理器装的 Python、某个工具链自带的 Python,在同一台机器上并存是常态,pip装进 A,python跑的是 B。
定位只需要两行命令,看输出路径是不是同一个前缀:
python -c "import sys; print(sys.executable)" python -m pip -V第一行打印当前python的真实路径,第二行打印pip绑定的解释器和它自己的位置。两个路径如果不是同一个环境(比如一个是/usr/bin/python3,另一个指向某个虚拟环境或用户目录下的 site-packages),问题就确认了。这里的关键习惯是:永远用python -m pip而不是裸pip,-m的含义是「用当前这个解释器去执行它自己的 pip 模块」,从机制上排除了装错地方的可能。
还有一个更隐蔽的导入失败,报错长这样:
ImportError: cannot import name 'xxx' from partially initialized module 'numpy'partially initialized(部分初始化)这个词是线索,它通常不是库坏了,而是你自己的目录里有个文件叫numpy.py、random.py、math.py之类,Python 优先从当前目录导入,把你的文件当成了标准库,然后那个文件里又反过来import numpy,形成循环。改法很简单:给文件改名,或者把示例脚本放进子目录再运行,别在库名同名的前提下执行。我一般会在跑任何示例前先执行一句python -c "import numpy; print(numpy.__file__)",确认它指向 site-packages 而不是当前工作目录。
2.2 用虚拟环境锁住版本,而不是反复全局安装
确认解释器之后,第二个动作是隔离。全局环境里装的库是给所有项目共用的,你为了跑通这份源码把numpy降级,另一个项目可能立刻挂掉;反过来,你为了别的项目升级,这份源码又会崩。这不是玄学,是依赖冲突的必然结果。
# 在源码根目录创建独立环境,目录名 .venv 是社区惯例 python -m venv .venv # Linux / macOS 激活 source .venv/bin/activate # Windows PowerShell 激活 # .venv\Scripts\Activate.ps1 # 升级 pip 自身,旧版 pip 解析依赖时容易选到过老的轮子 python -m pip install --upgrade pip # 按锁文件安装,而不是一个个手动 pip install python -m pip install -r requirements.txtrequirements.txt里的版本不该随手写latest,而是写你验证通过的那一组。下面这种带上下界的写法比较稳,既允许补丁更新,又挡住会破坏兼容的大版本:
numpy>=1.26,<2.0 scipy>=1.11,<2.0 matplotlib>=3.8,<4.0 sympy>=1.12,<2.0>=后面的数字是下限,<后面的是上限。之所以给numpy卡<2.0,是因为 2.x 在类型提升规则、部分函数行为上做了调整,书里那些依赖隐式类型转换的示例可能算出不一样的结果;等你确认整本书的代码都跑通了,再考虑放宽上限。装完之后立刻固化一次现场:
python -m pip freeze > env.lock.txtfreeze会把当前环境所有包的精确版本导出,env.lock.txt就是你的后悔药。以后换机器、重装系统、或者某天突然跑不通了,拿它对照一下就知道是哪个包悄悄变了。
2.3 别名被移除:AttributeError 的批量修法
环境和版本对齐之后,还会撞上第三类问题:包能导入,但代码里某个属性没了。
AttributeError: module 'numpy' has no attribute 'float'这个报错的原因很明确:np.float、np.int、np.bool、np.object、np.str这些是早期为了兼容 Python 2 留的内置类型别名,后来被标记为弃用,再后来直接从库里删掉了。数学书出版的周期长,配套代码里出现np.float(3.14)这种写法一点都不奇怪。
逐行改是下策,几百个文件改不完。正确做法是先全局搜出所有命中点,再决定批量替换:
# -E 开启扩展正则,[^_0-9a-zA-Z] 保证不会误伤 np.float64 / np.int32 grep -rnE "np\.(float|int|bool|object|str)[^_0-9a-zA-Z]" --include="*.py" .这个正则里[^_0-9a-zA-Z]是关键:如果只搜np.float,会把np.float64一起命中,改完整份代码就废了。搜出来之后按这张表替换:
| 旧写法 | 新写法 | 说明 |
|---|---|---|
np.float(x) | float(x)或np.float64(x) | 需要标量用前者,需要定宽类型用后者 |
np.int(x) | int(x)或np.int64(x) | 同上,索引场景优先int |
np.bool(x) | bool(x)或np.bool_(x) | 数组布尔类型用np.bool_ |
np.object | object | 对象数组的 dtype 直接写object |
np.str | str | 字符串同理 |
同类问题还会出现在scipy上:一些早期放在scipy.misc下的便捷函数(比如数值求导)在新版本里被移走了,from scipy.misc import derivative直接ImportError。这类函数不难自己补:一阶导用中心差分(f(x+h) - f(x-h)) / (2*h)就够了,或者用numpy.gradient对采样点整体求差分。补的时候注意h别取太小,1e-15这种量级会被浮点舍入误差吃掉,1e-6到1e-5是比较稳的区间。
3. 图出不来也看不清:空白窗口、中文方块与 LaTeX 渲染失败的修法
数学示例离了图基本没法看,函数长什么样、切线贴不贴合、积分面积对不对,全靠画出来。而画图恰恰是这份源码里报错最花哨的部分:有的报错是异常,有的干脆不报错——程序正常退出,你去文件夹里找图,发现是张全白的。区分「抛异常的失败」和「静默的失败」,是这一章的核心。
3.1 plt.show() 没反应与空白图
先看一个不报错但什么都没发生的场景:脚本跑完,终端干干净净,窗口没弹出来,图片文件也不存在。第一件要确认的事是当前用的哪个后端:
import matplotlib print(matplotlib.get_backend())如果输出是Agg、pdf、svg这类名字,说明当前是非交互后端,它只负责把图渲染成文件,压根没有弹窗能力。这在服务器、容器、CI 环境里是默认行为,不是配置错了。解决办法分两种,取决于你要什么:
# 方案一:明确用无界面后端,把图存成文件 import matplotlib matplotlib.use("Agg") # 必须写在 import pyplot 之前 import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(6, 4)) ax.plot([0, 1, 2, 3], [0, 1, 4, 9]) ax.set_title("quadratic") fig.savefig("quadratic.png", dpi=150, bbox_inches="tight") # 先保存 plt.close(fig) # 再释放这里有两个必须记住的顺序问题。第一,matplotlib.use("Agg")一定要放在import matplotlib.pyplot之前,因为后端是在pyplot导入时初始化的,之后再use往往无效,甚至抛ImportError。第二,savefig一定要放在show之前、close之前。很多示例图是空白,就是因为在交互后端下调了plt.show(),窗口关掉时 figure 已经被销毁,后面那句savefig保存的是一张空画布。
如果你在 Jupyter 里跑,逻辑又不一样:notebook 需要内联渲染,靠的是魔法命令而不是show。
# Jupyter 单元格里第一行写上,保证图直接嵌在输出区 %matplotlib inlinedpi=150控制输出分辨率,屏幕预览 100 够用,要打印或放大看细节就提到 200 到 300;bbox_inches="tight"会裁掉图四周多余的留白,避免标题或轴标签被切掉。这两个参数不影响计算,只影响观感,但被切掉的标签经常让人误以为图画错了。
3.2 中文方块与负号显示异常
第二类静默失败是字变方块。程序不报错,图也正常生成,只是标题里的中文全成了「口口口」,负号位置也变成一个方框。控制台里通常还飘着一条警告:
UserWarning: Glyph 8722 missing from current font.根因是 matplotlib 默认字体族里没有中文字形,缺哪个字就画哪个方块。8722就是数学减号−的码位,它和 ASCII 的连字符-不是同一个字符,很多中文字体里没有这个字形。两行配置能同时解决:
import matplotlib matplotlib.rcParams["font.family"] = "sans-serif" # 按优先级从左到右找,装哪个用哪个;Windows 常见雅黑,Linux 常见思源或 Noto matplotlib.rcParams["font.sans-serif"] = [ "Noto Sans CJK SC", "Source Han Sans SC", "Microsoft YaHei", "SimHei" ] # 关掉 Unicode 负号,改用 ASCII 连字符,避开缺字形 matplotlib.rcParams["axes.unicode_minus"] = Falsefont.sans-serif给的是一个候选列表,matplotlib 会按顺序找第一个存在的字体,所以多写几个没有坏处,换机器不用重改代码。如果你不确定机器上到底有什么中文字体,别猜,直接列出来:
from matplotlib import font_manager names = {f.name for f in font_manager.fontManager.ttflist} print([n for n in sorted(names) if "CJK" in n or "Hei" in n or "Song" in n])ttflist是 matplotlib 启动时扫描到的字体清单,输出里出现什么名字,上面那个列表里就填什么,一个字符都不能差。这里踩过的坑是:有人写"SimHei "(末尾多个空格)或者写成文件名"simhei.ttf",配置看着对,实际完全不生效,而且不报错,只是继续画方块。
3.3 LaTeX 公式与保存图片的坑
数学书的图经常带公式标注,比如在图上标 $f'(x)$、$\int_0^1 f(x)dx$。默认的mathtext引擎能渲染基础的数学符号,不需要装任何东西;但有些示例为了排版好看,会开启完整 LaTeX 支持:
# 这一行是很多环境下的报错源头 # matplotlib.rcParams["text.usetex"] = True一旦开了usetex,matplotlib 会去调用外部的 LaTeX 程序,没装或者不在 PATH 里就会抛:
RuntimeError: Failed to process string with tex because latex could not be found修法有取舍:要么把text.usetex设回False,接受mathtext的排版效果(对学习线性代数和微积分来说完全够用);要么把完整的 LaTeX 发行版装上并确认latex --version能跑。我一般选前者,因为示例里的公式无非上下标、希腊字母、积分号,mathtext全都支持,没必要为排版引入一整套依赖。
另一个高频静默问题是图片越存越多、内存越跑越高,控制台给个警告:
RuntimeWarning: More than 20 figures have been opened.这不是错误,是提醒你 figure 没有释放。在循环里逐帧画图的示例(比如画参数曲线族)特别容易踩。养成习惯:每张图画完plt.close(fig),用fig, ax = plt.subplots()这种面向对象的写法而不是全局的plt.plot,释放关系会清楚很多。用plt.close("all")可以一次性清空,但别写在savefig前面。
4. 数字算不对:shapes not aligned、奇异矩阵与 nan 的三条排查线
环境通了、图画出来了,剩下的问题才真正考验基本功。这一章的三个报错指向三类完全不同的根因:维度不匹配是形状理解错,奇异矩阵是问题本身无解,nan是数值在中间过程炸了。它们的共同点是:报错信息给出的位置往往不是真正出错的位置。
4.1 shapes not aligned:先打 shape 再谈公式
ValueError: shapes (3,2) and (3,2) not aligned: 2 (dim 1) != 3 (dim 0)这条报错的信息量其实很足,只是很多人不看后半句。它的意思是:左边矩阵第 1 维长度 2,右边矩阵第 0 维长度 3,矩阵乘法要求A.shape[1] == B.shape[0],不满足就报错。真正难的不是看懂这句话,而是搞清「我明明想的是内积,为什么形状会变成这样」。
九成的根源在一维数组的方向问题上。在 numpy 里,np.array([1, 2, 3])的形状是(3,),既不是行向量也不是列向量,它没有第二个维度。参与矩阵乘法时,numpy 会按位置把它当成「长度 3 的向量」,行为和你脑子里想的3×1列向量并不总是一致。
import numpy as np A = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) # 形状 (2, 3) v = np.array([1.0, 2.0, 3.0]) # 形状 (3,),注意不是 (3, 1) print(A.shape, v.shape) # (2, 3) (3,) y = A @ v # (2,3) @ (3,) -> (2,),等价于 A 与 v 做内积 print(y, y.shape) col = v.reshape(-1, 1) # 显式转成 (3, 1) 列向量 row = v.reshape(1, -1) # 显式转成 (1, 3) 行向量 print((row @ col).item()) # 1x3 @ 3x1 -> 标量,和上面结果一致reshape(-1, 1)里的-1是「这一维你自己算」的意思,元素总数不变、另一维定为 1,于是(3,)变成(3,1)。这是把「向量」变成「矩阵」最省事的写法,也是排查维度问题时的第一步动作。
再往下有一个更隐蔽的陷阱:本该做内积的地方,因为两边都是列向量,被 numpy 广播成了矩阵。
a = np.array([1.0, 2.0, 3.0]) b = np.array([4.0, 5.0, 6.0]) print(a @ b) # 32.0,一维数组之间的 @ 是内积 print(a.reshape(-1, 1) * b.reshape(1, -1)) # (3,1) * (1,3) 广播成 3x3 外积如果你本来想要的是一个数,结果拿到一个 3×3 的矩阵,问题就在这:*在 ndarray 上是逐元素乘法,配合广播规则会长出新的维度,而@才是矩阵乘法。诊断这类问题的方法只有一个,就是在每个容易出错的运算前后打一次形状:
def matmul_checked(A, B): # 在真正相乘之前把维度关系校验掉,报错位置就是出错位置 assert A.ndim == 2 and B.ndim == 2, f"需要二维: {A.shape}, {B.shape}" assert A.shape[1] == B.shape[0], f"维度不匹配: {A.shape} x {B.shape}" return A @ B这段包装的价值在于:assert报错时给出的是两个参与运算的真实形状,而不是 numpy 在内部某个位置抛出的、让人对不上号的提示。等你确认整条链路都对了,可以把断言留着,它几乎不影响性能。
还有一类容易忽略的:np.matrix和np.ndarray混用。np.matrix的*是矩阵乘法,np.ndarray的*是逐元素乘法。书里如果有用np.matrix的旧示例,而你在别处用了 ndarray,同一个*写出来结果能差出十万八千里。手机制很简单:统一用ndarray加@,把np.matrix的示例改写掉。
4.2 Singular matrix:inv 之前先看条件数
numpy.linalg.LinAlgError: Singular matrix「奇异矩阵」听着抽象,翻译过来就是:行列式为 0,方程组的解不唯一或者根本不存在,所以没法求逆。数学书上这种题是用来分析解的结构,但代码里出现这个报错,往往意味着你的输入有问题,或者你的解法选错了。
先确认它是不是真的奇异。不要只看det是不是「等于 0」,浮点运算下det可能有1e-17这样的小量,看着不等于 0,实际已经病态得没法用。这时候看条件数更靠谱:
import numpy as np A = np.array([[1.0, 2.0], [2.0, 4.0]]) # 第二行是第一行的 2 倍,线性相关 print(np.linalg.det(A)) # 约等于 0 print(np.linalg.cond(A)) # 极大(1e16 量级),说明病态 b = np.array([3.0, 6.0]) # np.linalg.solve(A, b) # 这里就会抛 LinAlgError: Singular matrix # 退化情形改用最小二乘,给出最小范数解而不是崩掉 x, residuals, rank, sv = np.linalg.lstsq(A, b, rcond=None) print(x, rank) # rank 会告诉你实际有效秩lstsq的四个返回值分别是解、残差平方和、矩阵的有效秩、奇异值。rank是最有用的一项:它小于变量个数,就说明约束不够,解不唯一。rcond=None表示用机器精度自动决定截断阈值,比手填一个固定小数更稳。用inv(A) @ b求线性方程组在任何情况下都不是好选择:它多做一次求逆,数值上更不稳定,速度也更慢。能solve就solve,退化就lstsq。
如果是符号推导的示例,同样的病也会以另一种形式出现:分母出现0,sympy给出zoo(复数无穷)或者直接抛除零。这时候要回到公式本身,检查是不是某个参数取到了使矩阵降秩的值,而不是去改代码。
4.3 nan 与浮点误差:让错误在源头炸掉
最难受的一类失败是程序不报错,跑完给你一个nan。nan的特点是传染:一旦中间某个值变成nan,后面所有涉及它的加减乘除全是nan,等你在线性回归的损失值上看到它,原始出错点可能已经过去几十行。
import numpy as np # 让除零和无效运算立刻抛异常,而不是悄悄产生 nan/inf np.seterr(divide="raise", invalid="raise", over="warn")divide="raise"管的是1/0这类除零,invalid="raise"管的是0/0、inf - inf这类无效运算,over="warn"管的是溢出。设成raise之后,第一次出现非法值的位置会直接抛FloatingPointError,堆栈就停在那一行,这就是你要找的地方。常见源头无非三个:对0取对数、对负数开平方、梯度更新步长太大导致指数级放大。
浮点误差则是另一种性质的坑,它不产生nan,产生的是「明明该相等却不相等」:
print(0.1 + 0.2 == 0.3) # False print(np.isclose(0.1 + 0.2, 0.3)) # True,默认相对容差 1e-5 左右 # 验证矩阵求逆的正确性,用 allclose 而不是 == A = np.array([[4.0, 1.0], [1.0, 3.0]]) I = A @ np.linalg.inv(A) print(np.allclose(I, np.eye(2), atol=1e-8)) # Trueatol是绝对容差,rtol是相对容差,判断「接近 0 的数」时用atol,判断「量级很大的数」时用rtol。写测试、验证数值结果一律用这两个,别用==。
微积分的示例还会撞上积分不收敛。scipy.integrate.quad遇到端点奇点时不报错,只在返回值里给一个很大的误差估计,同时抛IntegrationWarning:
from scipy.integrate import quad import numpy as np f = lambda x: 1.0 / np.sqrt(x) # 在 x=0 处发散 val, err = quad(f, 0.0, 1.0) print(val, err) # 值勉强接近 2,但 err 明显偏大 # 做法一:告诉它奇点在哪,让它加密采样 val2, err2 = quad(f, 0.0, 1.0, points=[0.0], limit=200) # 做法二(更推荐):换元消掉奇点,x = t^2, dx = 2t dt,被积函数变成常数 2 val3, err3 = quad(lambda t: 2.0, 0.0, 1.0) print(val3, err3) # 2.0,误差极小points参数用来提示不连续点或奇点的位置,limit是子区间数量的上限。但换元才是根治:把奇点从被积函数里消掉,精度和速度都会好一个量级。梯度下降的示例同理,学习率取大了会直接发散到inf,进而变nan:
def gd(lr, steps=50): x = 1.0 for _ in range(steps): x = x - lr * 2 * x # f(x) = x^2,导数 2x return x print(gd(0.1)) # 收敛到接近 0 print(gd(0.6)) # 来回震荡但振幅衰减 print(gd(1.1)) # 发散,很快变成 inf / nan判断标准是更新式x <- x - lr * 2x的系数|1 - 2*lr|是否小于 1。lr=0.6时系数是 0.2,收敛;lr=1.1时系数是 1.2,发散。把这个系数算一遍,比反复调参快得多。
5. 避坑清单:五个反复翻车的现场与对应改法
这一章的五条,是我在实际复现过程中反复撞上的。它们有个共同特征:报错信息和真实原因隔了一层,照着字面意思改往往改不对。每条按「现象—原因—解决」写,遇到相似的直接对号入座。
5.1 终端能跑、Jupyter 报 ModuleNotFoundError
现象:在项目目录下python demo.py一切正常,同样的代码粘进 notebook 就ModuleNotFoundError: No module named 'numpy'。
原因:notebook 的 kernel 和终端里的python是两个独立进程,用的解释器不一定相同。Jupyter 的 kernel 是在启动时注册的,即使你后来激活了虚拟环境,已经开着的 notebook 依然连着老的 kernel。
解决:先看当前 kernel 用的是哪个解释器,在单元格里执行下面这段,输出路径和你终端里sys.executable的结果对比:
import sys print(sys.executable) print(sys.path[:3])路径不一致就在虚拟环境里注册一个新 kernel:python -m ipykernel install --user --name math-demo --display-name "Python (math-demo)",然后在菜单里切换到Python (math-demo)。切换完记得重启 kernel,否则环境变量还是旧的。
5.2 savefig 存出来是一片空白
现象:图上明明有曲线,savefig生成的 PNG 打开是纯白,尺寸正常。
原因:plt.show()写在savefig前面。交互式后端下show会进入事件循环,窗口关闭后 figure 被销毁,后续savefig保存的是空画布。另一种情况是多张图共用了全局的 current figure,savefig存到了别的图上。
解决:固定顺序为「画图 →savefig→close」,并且全程用面向对象写法,把 figure 对象显式传下去,避免依赖全局状态:
fig, ax = plt.subplots() ax.plot(xs, ys) fig.savefig("curve.png", dpi=200, bbox_inches="tight") plt.close(fig)5.3 抄书里的 np.float 报 AttributeError
现象:AttributeError: module 'numpy' has no attribute 'float',或者int、bool、object同款报错。
原因:这些是早期兼容 Python 2 的类型别名,新版 numpy 已经移除。书印得早,配套代码沿用旧写法。
解决:按 2.3 节的替换表批量改,替换前先用grep -rnE "np\.(float|int|bool|object|str)[^_0-9a-zA-Z]" --include="*.py" .把命中点全找出来,改完再跑一遍确认零命中。别用编辑器全局替换np.float字符串,那会把np.float64一起毁掉。
5.4 中文路径读文件报 FileNotFoundError
现象:FileNotFoundError: [Errno 2] No such file or directory: 'data\\样本.csv',文件确实存在。
原因:两个叠加问题。Windows 下反斜杠是转义符,\样这种组合在普通字符串里会被当成转义序列处理;同时不同工具对非 ASCII 路径的编码处理不一致,open时没指定编码还可能抛出UnicodeDecodeError。
解决:路径一律用pathlib拼接,读取时显式指定编码:
from pathlib import Path data_path = Path(__file__).parent / "data" / "样本.csv" with data_path.open("r", encoding="utf-8-sig") as f: # 带 BOM 的表格用 utf-8-sig rows = f.read().splitlines()Path(__file__).parent表示「当前脚本所在目录」,这样脚本在任何工作目录下运行都能找到相对路径,比os.getcwd()可靠。
5.5 循环画图越来越慢、内存持续上涨
现象:批量画几十张图,跑到第十几张开始明显变慢,最后内存吃满被系统杀掉,控制台只有一条RuntimeWarning: More than 20 figures have been opened。
原因:每次plt.subplots()或plt.figure()都创建了新 figure,而 matplotlib 默认会保留对这些 figure 的引用。不close就一直在内存里堆着。
解决:循环体内画完立刻关闭,优先用上下文管理或显式close:
for i, (xs, ys) in enumerate(series): fig, ax = plt.subplots(figsize=(5, 3)) ax.plot(xs, ys) fig.savefig(f"fig_{i:02d}.png", dpi=120) plt.close(fig) # 关键:释放当前 figure批量任务收尾再补一句plt.close("all")兜底。如果只是算数据不画图,干脆在脚本开头用matplotlib.use("Agg"),避免加载任何 GUI 组件。
6. 快速定位:把「跑不通」压缩成三步的排查习惯
排错慢,往往不是不会改,而是没有固定的下手顺序,每次都在随机试。我后来固定成三步,基本能把大部分问题在十分钟内收敛到具体位置。
6.1 三步定位法:固化环境、最小复现、二分注释
第一步,固化环境。在报错的第一时间,别急着改代码,先把现场存下来:
python -c "import sys, numpy, scipy, matplotlib; print(sys.version); print(numpy.__version__, scipy.__version__, matplotlib.__version__)" python -m pip freeze > env.before.txt这两行给出解释器版本、关键库版本和完整依赖快照。很多「昨天还能跑今天不行」的问题,对比一下env.before.txt和当前freeze的结果就水落石出了。
第二步,最小复现。把报错代码剥到只剩必要的几行,删掉所有绘图、打印、文件读写。如果剥完不报错了,说明问题出在被删掉的部分;如果还报错,你手上就有了一个几十行的可复现样本,试错成本极低。剥的过程中不要重写逻辑,只做删除。
第三步,二分注释。面对一个几百行的章节脚本,把中间一段注释掉再看是否报错,报错就说明问题在前半段,不报错说明在后半段,反复对半砍,五六次就能定位到具体行。这个方法笨,但在依赖链条长、报错位置偏移的情况下最有效。
6.2 报错到首查位置的一张对照表
把常见报错和第一检查点对应起来,能省掉大量瞎试的时间:
| 报错关键字 | 首查位置 | 一行验证 |
|---|---|---|
ModuleNotFoundError | 解释器是否一致 | python -c "import sys; print(sys.executable)" |
partially initialized module | 当前目录是否有同名文件 | ls *.py看有没有numpy.py之类 |
AttributeError: no attribute 'float' | 是否用了已移除的别名 | `grep -rnE "np.(float |
shapes not aligned | 参与运算的两个 shape | 在乘法前print(A.shape, B.shape) |
Singular matrix | 行列式与条件数 | print(np.linalg.det(A), np.linalg.cond(A)) |
结果为nan | 第一个出现 nan 的位置 | np.seterr(divide="raise", invalid="raise") |
Glyph missing from current font | 字体列表与实际可用字体 | 打印font_manager.fontManager.ttflist里的名字 |
| 图片空白 | savefig与show的先后 | 把savefig提到show之前 |
| 图越画越慢 | figure 是否释放 | 循环里有没有plt.close(fig) |
用法是先看报错最后一行(异常类型和消息),在表里找关键字,直接跳到对应验证命令。表里没有的,退回 6.1 的三步法。
有一个很实用但少有人用的开关:python -X dev会把运行时的资源警告、编码警告全部打出来,python -W error则把所有警告升级成异常。后者特别适合在确认「代码逻辑对但结果不对」的阶段用,因为 numpy 的RuntimeWarning: invalid value encountered、divide by zero平时只是打印一行就过去了,升级成异常之后,堆栈会直接指到产生非法值的那一行。我第一次用它就发现了一个藏在三层函数调用底下的log(0),之前靠肉眼找了两个小时。
从那以后我每次拿到一份新的示例源码,都强制走一遍固定流程:先建虚拟环境固化成env.lock.txt,再用python -X dev跑一遍完整示例,最后把np.seterr设成 raise 跑数值部分。这三步加起来不到五分钟,但能把后面几小时的困惑挡在门外。希望帮到你。
本文还有配套的精品资源,点击获取