简介《程序员数学》配套 Python 源码与练习包面向希望用编程学透线性代数、微积分的学习者与开发者对应书中向量矩阵、行列式、特征值、线性变换、极限导数与积分等章节并对原书示例中的错误和 bug 作了修正便于边读边跑、对照验证。压缩包共 104 个文件约 13.55MB以 74 个 py 脚本和 17 个 ipynb 笔记为主辅以少量 jpg 图示、off 文件及 md 说明覆盖逐章演练与可视化实验。已有 1653 人学习下载。读者可直接获得可运行的章节代码、交互式笔记本和修正后的排错参考在 numpy、scipy、sympy、matplotlib 环境中复现矩阵运算、符号求导与函数绘图减少踩坑并加深对抽象数学概念的理解。1. 从一行 AttributeError 说起数学书的配套代码为什么一跑就崩跑过程序员数学配套源码的人基本都遇到过这种场面照着书上第几页找到对应文件双击运行屏幕上甩出一行红字AttributeError: module numpy has no attribute float。线性代数的推导明明看懂了微积分公式也抄对了代码却连一个矩阵都构造不出来。这类故障几乎全部集中在三块NumPy 接口随版本变动、矩阵形状和书上推导对不上、绘图与符号计算依赖本地环境。这份按章节与书本一一对应、并把原示例里的 bug 逐条修过的 Python 源码价值就在这里——它把「书上是正确的、代码是过时的」这种最磨人的情况提前处理掉了改动行还留了注释方便你对照原书公式理解为什么这么改。下面按报错类型逐个拆。2. NumPy 弃用别名引发的连环报错np.float、np.int 改成什么才不改变数值2.1 报错现场AttributeError: module numpy has no attribute float典型现场是这样的跑到讲向量与矩阵运算的那一章构造标量或者给数组指定类型时直接挂掉Traceback (most recent call last): File ch02/vector_basic.py, line 14, in module scale np.float(3.14) AttributeError: module numpy has no attribute float同一类错误会以四种面貌出现只是后半截名字不同np.int、np.bool、np.object、np.str。还有一种更隐蔽的变体报错发生在dtype参数上比如np.zeros(3, dtypenp.int)看起来只是建了个零向量实际上在解释器解析这一行时就已经炸了你甚至走不到打印那一步。新手最容易误判的地方是错误信息说module numpy has no attribute float于是在文件头加一行import numpy as np就以为能解决或者怀疑自己装了个假 NumPy。其实 NumPy 装得好好的只是这些名字在新版本里被官方拿掉了。判断依据很简单先确认环境# 同时打印解释器路径和 NumPy 版本避免改了半天发现改的是另一个 Python python -c import sys, numpy; print(sys.executable); print(numpy.__version__)如果你看到版本在 1.24 及以上而源码里还在写np.float那基本就是这个坑不需要继续往下猜。2.2 根因别名被下线了而且它和内置类型不是一回事np.float、np.int、np.bool这几个名字本质上是 Python 内置float、int、bool的别名。当初保留它们只是为了写法短一点但带来一个严重问题宽度不明确。np.int到底是 32 位还是 64 位取决于平台和 C 编译器的long同一份代码在两种机器上算出来的 dtype 可能不同。NumPy 从 1.20 开始把这些别名标记为弃用跑起来会给你一条DeprecationWarning1.24 之后直接移除于是老代码全线报错。这里有个高频误解必须点掉很多人以为「np.float就是 Python 的float那我直接换成内置float总没错」。换成内置float在多数场景确实能用因为 NumPy 会把dtypefloat解释成float64但换成内置int就有风险了dtypeint在 64 位平台上映射到int64在一些老环境里却可能落到int32而数组元素个数一多、索引一超范围结果就悄悄变了。数学书里大量示例依赖固定的浮点精度和整数宽度这种「能跑但数字对不上」的坑比直接报错难查十倍。我一般按语义对照着改而不是一律替换旧写法推荐新写法说明np.floatnp.float64老别名实际就是双精度显式写出来最稳np.intnp.int64原宽度依赖平台统一到 64 位避免跨机器不一致np.boolnp.bool_注意尾部下划线np.bool已不存在np.objectnp.object_或直接object对象数组场景多用于存放不规整数据np.float_np.float64这个带下划线的变体在更新的版本里也下线了提示dtypefloat、dtypeint这种直接写内置类型的写法在新版本仍然可用报错的只有np.float这类带np.前缀的别名别把两者混为一谈也别顺手把前者一起批量改掉。2.3 改法先定位、再按语义替换、最后跑断言我不建议上来就全局替换因为注释和字符串里的np.float会被一起改掉更麻烦的是np.float128、np.int0这类长名字可能被截断。正确顺序是先扫一遍看清楚有多少处、分别是什么语境# 在当前源码目录下扫描所有旧别名用法只列行号和内容不动文件 # -r 递归子目录-n 带行号--include 限定只扫 .py-E 启用扩展正则\b 保证不误伤 np.float64 grep -rn --include*.py -E np\.(float|int|bool|object|str)\b .扫出来的结果里如果某一章的文件几乎全是旧别名说明那部分代码的写作年代较早需要整章核对如果只有零星几处逐个改更快。改完之后用一段小脚本验证 dtype 语义没有跑偏import numpy as np # 反例下面这行在当前 NumPy 上会抛 AttributeError保留在注释里作为对照 # x np.float(3.14) # 正例一需要固定双精度时显式指定数值行为与老代码保持一致 x np.float64(3.14) # 正例二构造数组时用 dtype 参数比在元素上做类型转换更清晰 a np.zeros(3, dtypeint64) # 正例三布尔数组的标量类型是 np.bool_判断 dtype 时不要写成 np.bool mask np.array([True, False], dtypenp.bool_) # 断言的作用改完之后立刻验证避免只改了语法、语义没对上 assert x.dtype np.float64 assert a.dtype np.int64 assert mask.dtype np.bool_ print(x, a.dtype, mask.dtype)这段代码里np.float64(3.14)得到的是一个 NumPy 标量可以正常参与后续矩阵运算dtype属性是实打实的float64dtypeint64用字符串更直观也方便批量搜索三条assert是关键很多人改完直接看程序不报错就往下走结果在梯度下降那一章拿到一堆精度异常的结果回头再找就来不及了。还有一个常见分支有人图省事把 NumPy 降级到老版本想着「反正书上的代码能跑」。短期可行但绘图层、符号计算库通常会跟着要求新版本 NumPy降级之后另一个模块又开始报ImportError来回折腾的时间远超改这几行代码。我更倾向改代码而不是改环境环境是共享的代码是自己的。3. 矩阵形状对不上ValueError: shapes not aligned 的三步定位顺序3.1 先分清两类形状报错它们根因完全不同同样叫「形状问题」报错文本却指向两种完全不同的机制先分桶再动手能省一半时间。报错文本触发算子含义第一反应shapes (3,2) and (3,2) not aligned、np.dot、np.matmul矩阵乘法要求左矩阵列数等于右矩阵行数这里 2 不等于 3检查是不是有一个矩阵该转置operands could not be broadcast together with shapes (3,1) (3,)、*、-等逐元素运算广播规则要求两个数组从末维对齐(3,1)和(3,)对不上检查是不是把行向量和列向量搞混了第一种是线代本身的问题书上写的是 $A_{m\times n}B_{n\times p}$代码里却把 $B$ 也建成了 $m\times n$。第二种是 NumPy 广播的问题数学上「向量乘标量」没问题但两个形状不同的数组做逐元素运算时NumPy 需要从后往前逐维匹配任何一维既不相等也不为 1就报广播失败。这两类错误在第 3、4 章的示例里出现频率最高因为它们正好处在「手推公式」到「代码实现」的转换点上。书上画的是竖排的列向量敲代码时np.array([1,2,3])得到的却是一维的(3,)形状概念在纸面上根本不存在。3.2 shape 断点法在乘法前打印而不是反复读 tracebackValueError的 traceback 只给你行号不给中间变量。而数学代码往往一行里套三四个运算比如w X.T y b报错说形状不对你根本不知道是X建错了还是y拿错了。我的做法是在可疑行之前插一个调试函数把参与运算的每个数组形状打出来import numpy as np A np.array([[1., 2.], [3., 4.], [5., 6.]]) # 3x2 B np.array([[1., 2.], [3., 4.], [5., 6.]]) # 书上要求 2x3这里建错了 def matmul_debug(x, y): # 打印形状和关键维度比冻在 traceback 上猜要快很多 print(fx.shape{x.shape}, y.shape{y.shape}) print(f内维校验: x.shape[-1]{x.shape[-1]} vs y.shape[0]{y.shape[0]}) # 断言把错误提前到这一行堆栈信息直接指向真实位置 assert x.shape[-1] y.shape[0], 内维不一致左矩阵列数必须等于右矩阵行数 return x y print(matmul_debug(A, B))这里x.shape[-1]取的是左矩阵的最后一维也就是列数y.shape[0]是右矩阵的第一维也就是行数。这两个数相等才允许相乘写出来一目了然。assert的价值在于把错误拦在真正出问题的那一行而不是让它流到后续十几行之外才炸。打印出来的(3, 2)和(3, 2)一眼就能看出右边那个应该是(2, 3)这时候回到构造B的地方核对数据来源通常能发现是把转置矩阵和原矩阵抄反了。参数上还有个小细节多维数组时用x.ndim一起打出来更保险因为(3,)和(3,1)的shape明显不同但ndim分别为 1 和 2能立刻判断出是维度缺失还是维度不匹配。3.3 转置、reshape、补维三种修法各管一段别混用定位到问题之后修法有三种选错会造成「不报错但结果错」这种更麻烦的局面。修法写法适用场景风险转置B.T书里公式没错只是数据按行列颠倒存了转置是视图改它会改到原数组重排B.reshape(2, 3)数据本身是一维序列只是按顺序切成矩阵元素顺序错了不会报错结果静默变化补维v[:, np.newaxis]一维向量需要显式变成列向量参与广播补错方向会得到外积而不是内积对应的代码长这样import numpy as np v np.array([1., 2., 3.]) # 同样是三个元素的向量行向量和列向量在广播里的表现完全不同 row v[np.newaxis, :] # 形状 (1, 3) col v[:, np.newaxis] # 形状 (3, 1) print(row.shape, col.shape) print((row * col).shape) # (3, 3)逐元素相乘被广播成外积矩阵 print(np.dot(row, col).shape) # (1, 1)这才是内积结果是标量包裹在矩阵里v[np.newaxis, :]相当于在第一个维度前插一个长度为 1 的轴v[:, np.newaxis]则是在第二个维度前插。这两个写法线代教材里几乎不会讲但书本示例一旦涉及二次型、协方差矩阵、梯度向量的转置就离不开它们。注意形状对不上会报错形状对上了但语义错却不会报错。把row * col当成内积写进损失函数程序一路跑到底最后画出条水平线这种翻车我见过不止一次。所以在数学代码里我宁可多写一行print确认形状也不靠肉眼扫一遍就往下走。顺带说一句*和的区别这是线代代码里最常见的静默错误来源A * B是逐元素相乘要求两个矩阵形状相同或可广播A B才是矩阵乘法。书本上的矩阵乘法公式对应的是很多示例为了兼容老版本会写np.dot二维情况下两者等价一旦升到三维以上np.dot和的批量语义就不同了。修函数里如果混用了两种写法最好统一成读起来也更贴近数学符号。4. 绘图与可视化翻车后端、中文字体和「图不出来」的三类现场4.1 no display name没有图形界面时 plt.show() 必然失败先看报错原文这是服务器和容器里跑书上示例时最典型的一条_tkinter.TclError: no display name and no $DISPLAY environment variable触发条件很明确代码走到plt.show()matplotlib 想调起图形窗口但当前环境没有 X 显示、没有桌面会话于是抛出异常。注意这个报错有时不是一上来就出现而是在你跑完前面所有计算、眼巴巴等着看图的时候才崩前面几十秒的计算全白费。根因在 matplotlib 的后端机制。后端分两类交互式的TkAgg、QtAgg 这类能弹窗和非交互式的Agg只负责渲染成像素不负责显示。默认后端是根据环境探测出来的探测到有显示就选交互式探测不到就报错。所以解决办法不是给系统装桌面而是显式指定非交互后端并且把结果写进文件import matplotlib # 必须在 import pyplot 之前设置后端顺序反了不生效 matplotlib.use(Agg) import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(4, 3)) ax.plot([0, 1, 2, 3], [0, 1, 4, 9]) ax.set_title(square) fig.savefig(square.png, dpi120, bbox_inchestight) # 无显示环境下直接落盘 print(saved)matplotlib.use(Agg)的作用是告诉渲染层只走内存绘制不碰任何窗口系统dpi120决定输出清晰度教学图 120 已经够用想嵌进讲义可以提到 200bbox_inchestight会裁掉图周围多余的空白边距避免坐标标签被切掉。这段配置我一般放在脚本最顶上比在每张图前面判断环境省事。4.2 中文变方块、负号消失字体与 rcParams 的两行配置图形能出来了第二个经典问题随之而来标题里的中文全变成方块坐标轴负号变成一个小方框。这不报错但图上信息全废。原因是 matplotlib 默认字体是英文字体族找不到中文字形就用占位方块替代负号的问题则是默认用 Unicode 减号同样依赖字体支持。修法是显式指定系统里真实存在的中文字体并关掉 Unicode 负号import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt # 按优先级列出候选字体系统里装了哪个用哪个全部缺失时仍会退化成方块 plt.rcParams[font.sans-serif] [Noto Sans CJK SC, WenQuanYi Zen Hei, SimHei] plt.rcParams[axes.unicode_minus] False # 用 ASCII 减号避开负号变方块的经典开关 fig, ax plt.subplots(figsize(5, 3)) ax.plot([-2, -1, 0, 1, 2], [4, 1, 0, 1, 4]) ax.set_title(函数图像) ax.set_xlabel(x) fig.savefig(curve.png, dpi120, bbox_inchestight)font.sans-serif接收的是一个列表matplotlib 从左往右找找到第一个可用的就用。这里一定不要硬编码一个你没装过的字体名否则它不报错只是默默退回方块你还以为配置生效了。验证方法很直接把系统里实际的字体名列出来# 列出 matplotlib 能识别的字体名取前 20 个看有没有中文字体 python -c from matplotlib import font_manager as fm; print([f.name for f in fm.fontManager.ttflist][:20])axes.unicode_minus False这一行单独说明它不是把负号变成别的符号而是让 matplotlib 改用 ASCII 的-来渲染负号牺牲一点排版美观换取在所有字体下都能正常显示。凡是数学图里带负值坐标的这行几乎必加。4.3 图对了但线多了全局状态污染与采样点数第三类现场最容易被忽略图能出来中文也正常但曲线上多了几条你没画的线或者函数图像跟书上明显不是一个形状。前者是状态污染。plt.plot()这种全局接口把所有东西画进当前 figure 的当前 axes如果你的脚本里连续调用多次、中间又没有plt.close()第二次运行时上一次的线还留在画布上。修复方式是把绘图改成面向对象写法每次新建画布import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt import numpy as np # 面向对象写法画布和坐标轴都是显式对象不会互相污染 fig, ax plt.subplots(figsize(5, 3)) x np.linspace(-np.pi, np.pi, 400) # 采样点数决定曲线光滑度 ax.plot(x, np.sin(x), labelsin) ax.plot(x, np.cos(x), labelcos) ax.legend() ax.grid(True) fig.savefig(trig.png, dpi120, bbox_inchestight) plt.close(fig) # 显式关闭批量跑多张图时防止内存堆积 print(x.shape, x[:3])np.linspace(-np.pi, np.pi, 400)里的 400 是采样点数微积分章节里画切线、画黎曼和时会大量用到。采样点太少曲线看着像折线采样点太多批量出图时又慢。教学场景 200 到 400 之间比较合适。plt.close(fig)在批量生成几十张图的脚本里很重要不关的话内存会一直涨跑到后面几张时明显变慢。曲线形状和书上对不上还有一种可能书上用的是解析式直接采样代码里用了数值近似两者在端点附近的差异肉眼可见。这种情况不是 bug但要能分辨出来——把采样点从 100 提到 400如果差异明显收敛那就是采样密度问题如果形状依然对不上再回头查公式实现。5. 避坑源码跑不通时最容易踩的 5 个坑5.1 改了却没生效字节码缓存和解释器错位现象明明已经把np.float改成np.float64保存后重跑报错行号还是原来那一行甚至报错内容一字不差。改到第三遍开始怀疑人生。原因通常有两个。一是__pycache__里的.pyc字节码和源文件时间戳对不上模块被直接复用了旧缓存二是 IDE 的运行配置指向另一个解释器或者指向了另一份拷贝的源码目录你改的是 A 目录跑的是 B 目录。第二种在「书配套源码解压了两份」的情况下特别常见。解决分两步先清缓存再确认解释器# 递归清掉当前目录下所有字节码缓存目录 find . -name __pycache__ -type d -prune -exec rm -rf {} # 确认当前 python 指向哪个可执行文件和你 IDE 里配的是不是同一个 python -c import sys; print(sys.executable)-type d限定只找目录-prune阻止 find 继续往缓存目录里钻-exec rm -rf {} 批量删除。清完缓存后如果报错内容变了说明确实是缓存问题如果一模一样重点查解释器路径和文件路径用python -c import 模块名; print(模块名.__file__)直接把实际加载的文件路径打出来一比就知道跑的是哪份。5.2 FileNotFoundError相对路径按工作目录解析不是按脚本位置现象从命令行在项目根目录跑脚本一切正常换到 IDE 里点运行立刻FileNotFoundError: [Errno 2] No such file or directory: data/ch02/points.csv。原因是相对路径的基准是进程的当前工作目录而不是脚本文件所在目录。命令行里你cd到了项目根所以data/ch02/...能找到IDE 默认的工作目录往往是脚本所在的那一层路径就短了一截。解决方式是以脚本自身位置为锚点拼路径from pathlib import Path # 以脚本所在目录为基准无论从哪个工作目录启动都能定位到数据文件 BASE Path(__file__).resolve().parent csv_path BASE / data / ch02 / points.csv print(csv_path) assert csv_path.exists(), f数据文件不存在检查目录结构: {csv_path}Path(__file__).resolve()会把符号链接和..都解开得到绝对路径.parent取所在目录/运算符在 pathlib 里就是拼接路径比手写字符串加斜杠更不容易出错。assert那行是有意加的把「文件不存在」这个错误提前到脚本入口处而不是等到读了半本书的数据之后才崩。注意Jupyter Notebook 里没有__file__这个变量这段写法只适用于.py脚本。在 Notebook 里跑书上示例时改用Path.cwd()并显式确认当前目录或者干脆用绝对路径。5.3 结果变 nan 或 inf溢出被静默吞掉警告不是噪音现象梯度下降那一章的示例跑完损失值打印出来是nan或者参数一路涨到inf控制台只飘过一行RuntimeWarning: overflow encountered in exp。原因是数值计算里的溢出和无效运算默认只发警告不中断程序继续往下跑错的值一路传染。常见触发点是学习率设得过大、迭代步数过多、exp的指数项太大、阶乘或幂次直接炸掉浮点范围。处理方式分两层。先让警告变成异常把现场冻住再去调参数import numpy as np # 把溢出和无效运算升级为异常出错时直接停在真实位置而不是最后打印出 nan np.seterr(overraise, invalidraise) def gradient_step(w, grad, lr1e-3): # 学习率是首要怀疑对象先降一个数量级再观察 return w - lr * grad w np.array([1.0, 2.0, 3.0]) grad np.array([10.0, -4.0, 0.5]) print(gradient_step(w, grad))np.seterr的over管上溢、invalid管0/0、inf - inf这类无效运算。设成raise之后程序会在第一次出问题的那一行抛FloatingPointError堆栈直指源头比在几百行之后看到nan强太多。注意这行配置是全局的调试完记得改回warn否则正常的边界情况也会被打断。学习率这一项我一般的顺序是先缩到原来的十分之一如果还炸再检查输入数据有没有未归一化。5.4 SympifyError符号计算和浮点数混着用现象微积分章节里用符号库求极限或导数报SympifyError: Sympify of expression could not parse ... failed或者干脆抛一个类型错误。原因是把 NumPy 的数组、np.float64标量直接塞进了符号表达式。符号计算库要的是它自己的符号对象和精确常量NumPy 的浮点标量对它来说是个不认识的对象转换失败就报这个错。解决原则是符号阶段全程用精确值最后一步再转浮点。import sympy as sp x sp.symbols(x) # 全程用符号常量和对分数避免中途引入浮点误差 expr sp.sin(x) / x limit_val sp.limit(expr, x, 0) deriv sp.diff(sp.exp(-x) * sp.cos(x), x) print(limit_val, float(limit_val)) # 只有最后展示时才转成浮点数 print(deriv)sp.symbols声明符号变量sp.limit和sp.diff都返回符号表达式而不是数值所以能保留精确形式。float(limit_val)放在打印语句里是为了让你看到 1.0 这样的直观结果但计算链条里不该出现它。同理需要写 $\pi$ 的时候用sp.pi而不是np.pi需要写二分之一的时候用sp.Rational(1, 2)而不是0.5否则一开始就丢了精度后面求出来的极限可能带着微小残差。5.5 输出和书上对不上随机性、精度和已修过的 bug 三件事现象代码跑通了图也出来了可数值和书上的印刷结果差一点点或者散点分布明显不一样。原因通常有三种。第一是随机性书上示例用了随机初始化但没有强调种子你每次跑结果都不同第二是精度差异书里可能只印了三位小数你的结果是0.3333333333333333第三也是最容易被忽略的一种——源码里已经修过原示例的 bug修完之后结果本来就该和书上不同。第一种用显式随机数生成器控制第二种对齐打印格式即可第三种要去读源码里的改动注释import numpy as np # 显式种子保证可复现default_rng 比老的 np.random.seed 更隔离不会污染全局状态 rng np.random.default_rng(42) sample rng.normal(size5) # 和书上对比时统一保留位数避免把精度差异误判成逻辑错误 print(np.round(sample, 3)) print(f{1/3:.3f})default_rng(42)创建的是一个独立的生成器对象好处是不受其他地方调用全局随机函数的影响老写法np.random.seed(42)改的是全局状态只要中间插了任何一次随机调用结果就全变。如果发现改了种子还是对不上就把源码里那一行改动前后的表达式都手算一遍——很多情况下书上印的是错误版本的结果源码修好了数字当然不一样这时候以源码为准。6. 快速定位这类数学代码故障的顺序故障排查最耗时间的不是修而是判断该往哪个方向查。我的习惯是三步走尽量在三分钟内把范围缩到一类。第一步只看 traceback 的最后一行异常类型把它丢进三个桶里。AttributeError、ImportError、ModuleNotFoundError归到环境和接口桶优先查版本和依赖ValueError、IndexError、KeyError归到数据和形状桶优先查数组维度、索引边界、文件路径FloatingPointError、RuntimeWarning、结果为nan归到数值桶优先查步长、精度和溢出。这个分桶动作看起来粗糙但能立刻排掉三分之二的错误方向避免在版本问题上找形状错误。第二步打一份环境快照存下来。同类问题第二次出现时直接比对快照能立刻看出是不是换了机器或换了环境# 一次性记录关键库版本粘到排查笔记里比事后回忆靠谱 python - PY import sys import numpy import matplotlib print(python, sys.version.split()[0]) print(numpy, numpy.__version__) print(matplotlib, matplotlib.__version__) print(executable, sys.executable) PY这段脚本用 heredoc 方式直接执行不落地文件。重点看两处Python 大版本决定语法特性是否可用NumPy 版本决定用哪套接口这两个对上了再去看具体代码。第三步做最小复现。把出问题的那一段从书本示例里摘出来删掉所有绘图、打印、文件读写只留下构造数据和调用出错函数的那几行。绝大多数「跑不通」在这一步就暴露出真实原因——要么是某个数组形状在删减过程中露了出来要么是删到最后发现根本不是这段代码的问题而是上游传进来的参数类型不对。最小复现还有个额外好处它是可以直接贴给别人看的沟通成本比描述现场低得多。最后给一张对照表把前面几章的信号和查法压缩成速查报错信号最可能的根因先查什么has no attribute floatNumPy 别名下线改成np.float64等显式类型shapes not aligned矩阵内维不匹配乘法前打印两个 shapecould not be broadcast行向量列向量混淆检查(3,)与(3,1)no display name无图形界面环境提前matplotlib.use(Agg)中文方块、负号丢失字体与 Unicode 负号设font.sans-serif与unicode_minus结果nan/inf步长过大或精度溢出np.seterr升级为异常输出与书上不一致随机种子、精度、已修 bug固定种子并对照改动注释血泪经验是这类数学代码的故障九成不在数学本身而在版本、形状、环境这三层。数学推导写错反而最容易发现因为它通常会在第一组数据上就给出明显离谱的结果真正难查的是那些能跑完、图也画出来、唯独某个中间量差了 0.001 的问题。从那以后我每次拿到一份配套源码都强制走一遍「环境快照 → 扫旧接口 → 跑最小示例 → 对一遍输出」这四步再开始看正文。看起来多花了十分钟实际上省掉的是后面几个小时的回溯。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?