它犯的错可以归成三类:事实错误(记错了)、范围错误(改多了)、一致性错误(断链了)。 三类的止损办法完全不同,认错了类别就会用错药。 事实错误靠查文档,范围错误靠写死边界,一致性错误靠跑全量测试。
上一篇讲「信息给不全」。这一篇讲「给全了它也会错」。 这两个边界合起来,就是这一章的标题:知道边界在哪里。 知道边界不是「别用它」,是「知道在哪儿加人」。 三类错误的分法在教材 2.4 里就有,但教材没给「怎么认」。 这一篇补上辨认办法:看错的样子。 事实错误是「跑起来结果怪」,范围错误是「diff 太大」,一致性错误是「单测过了但集成挂了」。
一、事实错误:库和 API 记错了
这一类最好认:代码语法对、函数名也是真的,但行为不是你以为的那样。
它不是在瞎编,它是把「见过的类似写法」往上凑。
Demo 里列了八条,六条跟直觉不一样。
最典型的一条:round(2.5) 得到 2,不是 3。
因为 Python 用的是「银行家舍入」——逢五取偶。
这条不是 bug,是设计;但它跟学校里教的「四舍五入」不一样。
还有一条更隐蔽的:re.match('a', 'ab') 返回 True。
很多人以为 match 是全匹配,其实它只看开头——全匹配要用 fullmatch。
这一条错在「名字的字面意思」上——
它叫 match,你自然会以为是「匹配」,而它实际是「从开头匹配」。
这类错误为什么难查?因为它不报错。
round(2.5) 返回 2,程序照跑,结果差一点,可能三个月后才发现账对不上。
所以这一类只能靠「跑一遍看结果」或者「查文档」。
二、范围错误:顺手改了不该改的
这一类最难防,因为它「改对了」,只是「改多了」。 你要它改一行判断,它顺手把引号统一了、把空行删了、把一个没用的 import 加了。 Demo 里量过:需求只有一处改动,diff 本该是「加 1 删 1」,实际变成「加 4 删 6」,合计 10 行。 多出来的 8 行全是「顺手」。 为什么它会顺手? 因为训练数据里「提交前顺手整理一下」是常见做法——很多教程甚至鼓励这么做。 但它不知道:在一个有审查流程的团队里,每一行 diff 都要人看一眼。 多 8 行,就是多看 8 行。 更麻烦的是「看起来都对」。 引号统一了、空行删了,代码确实更整齐了——审查的人会犹豫「这个改动要不要留」。 一犹豫,审查就慢下来了。 而慢下来的成本,比那 8 行本身高得多。
三、一致性错误:局部对,整体不通
这一类最典型:它改了一个函数的签名,那个文件本身完全正确——三个参数相加,逻辑没问题。
但调用方还按两个参数传,一跑就崩。
Demo 里的报错是 TypeError: add() missing 1 required positional argument: 'c'。
注意这个错不是「它写错了」,是「它没看别处」。
在它眼里,api.py 这个任务已经完成了。
这一类错误的根源是「视野」——它改的时候只看了一个文件,而这个改动的真实影响面在别的文件里。
补的办法只有一个:跑全量测试。
单测过不算过,集成过才算。
顺带说一个相关的坑:
如果签名改了但加了默认值,调用方不会报错,会「静默地按默认值跑」。
这种错比崩溃更难查——因为程序照常出结果,只是结果不是你想要的。
四、三类错误各自的止损办法
| 类别 | 怎么认出来 | 止损办法 |
|---|---|---|
| 事实错误 | 结果怪但不报错 | 跑一遍看结果、查文档 |
| 范围错误 | diff 行数远超预期 | 把范围写进任务、盯 numstat |
| 一致性错误 | 单测过、集成挂 | 跑全量测试、搜全仓库引用 |
这张表最有用的是中间那一列。 三类错误的样子完全不同,所以「怎么认」比「怎么修」更值得记。 而且认出来之后,你会知道该加哪一道防线: 事实错误多,说明该给它文档或者让它跑测试;范围错误多,说明该在任务里写死边界; 一致性错误多,说明该配一条全量测试的命令。 三类错误的共同点是:没有一类能靠「让它更认真」解决。 都得靠外部机制——测试、边界、检查。 这也是后面第 7 章「先约束再放权」的来源。
五、动手 Demo:三类工程错误:记错、改多、断链
这个 Demo 分三段,一类错误一段,全部真跑。 第一段列八条「看着对、其实不对」的调用,逐条执行看结果。 第二段造一个只该改一行的任务,看「顺手」能把 diff 撑到多大。 第三段改一个函数签名,看调用方怎么崩。 三段都只用 git 和 python,没有 Codex—— 因为这三类错误不是 Codex 特有的,是所有「只看局部」的改法都会犯的。
# 第一段:八条真跑
probes = [("round(2.5)", lambda: round(2.5)),
("re.match('a','ab')", lambda: bool(re.match("a", "ab"))),
("{}.get('k')", lambda: {}.get("k")),
("[].pop()", lambda: [].pop()),
("0.1 + 0.2 == 0.3", lambda: 0.1 + 0.2 == 0.3)]
# 第二段:需求只改一处,实际改了多少行
git diff --numstat # 加 N 行、删 M 行
# 第三段:改签名不改调用方
def add(a, b): -> def add(a, b, c):
add(1, 2) -> TypeError: missing 1 required argument
实跑输出:
--------------------------------------------------------------------
三类工程错误:记错、改多、断链
--------------------------------------------------------------------
① 第一类:事实错误——名字对了,语义记错了
--------------------------------------------------------------------
这一类最迷惑人:它写出来的调用是「真 API」,语法也对,跑起来却不是你想要的。
| 写法 | 实际结果 | 常见误记 |
|---|---|---|
| round(2.5) | 2 | 以为 3,其实 2(银行家舍入) |
| round(3.5) | 4 | 以为 4,其实 4(碰巧对) |
| re.match('a', 'ab') | True | 以为要全等,其实只看开头 |
| {}.get('k') | None | 返回 None,不抛异常 |
| {}['k'] | KeyError | 这个才抛 KeyError |
| [].pop() | IndexError | 以为返回 None,其实抛 IndexError |
| 'abc'.find('z') | -1 | 返回 -1,不抛异常 |
| 0.1 + 0.2 == 0.3 | False | 以为 True,其实 False |
表里八条,六条跟直觉不一样。
! 这类错误靠「读代码」发现不了——因为读起来是对的。只能靠跑,或者靠查文档。
② 第二类:范围错误——记对了,但改多了
--------------------------------------------------------------------
只改这一处:加 1 行、删 1 行
顺手格式化之后:加 4 行、删 6 行
需求只有一处改动,diff 里本该是「加 1 删 1」;实际加 4 删 6,合计 10 行。
多出来的 8 行全是「顺手」——换引号、删空行、多一行 import。
! 审查的人要在 10 行里找那 1 行真改动。改动的倍数不是效率,是审查成本。
③ 第三类:一致性错误——局部对,整体断
--------------------------------------------------------------------
改之前:退出码 0,输出 3
改了签名之后:退出码 1
报错:TypeError: add() missing 1 required positional argument: 'c'
api.py 自己是对的——三个参数相加,逻辑没问题。
但它把调用方弄断了。「这个文件改对了」和「这个仓库还能跑」是两件事。
! 三类错误对应的止损办法不一样:事实错误靠查文档,范围错误靠写死边界,一致性错误靠跑全量测试。
第一段的表里,八条有六条跟直觉不一样。
这个比例本身就是结论:「我记得这个 API」这件事,准确率大概七成。
七成听着不低,但一次改动里往往有五六个 API 调用——五个七成乘起来,全对的概率不到两成。
第二段那个 10 行对 1 处,是关键数字。
需求只有一处判断改动,diff 本该是「加 1 删 1」;实际加 4 删 6。
多出来的 8 行,全是「顺手」——换引号、删空行、加 import。
第三段的报错值得读一遍:missing 1 required positional argument: 'c'。
注意它报的是「缺参数」,不是「你改错了」——
这个错在有十个调用方的项目里,会同时出现在十个地方。
三类错误里,第二类最容易被当成「它很勤快」。
因为它确实顺手把代码整理干净了。
但对审查的人来说,那不是勤快,是把 1 行的工作量变成了 10 行的阅读量。
30 秒实验
- 下次看到 diff 行数明显超出预期,先别看内容,先问「为什么这么多」。 十次里有七次是「顺手」。
- 在你自己的代码里找一个「改了签名、加了默认值」的地方,看看有没有调用方在静默地用默认值。 这种错最难查,因为程序不报错。
- 给你的项目加一条命令:一条能跑完全部测试的命令。 没有这条命令,「一致性错误」就永远要等到上线才发现。
常见误区
- 「让它更仔细一点就好了。」 三类错误没有一类能靠「仔细」解决。 仔细不解决「记错」(它自己也不知道记错了),也不解决「视野」(它根本没看别处)。 只能靠外部机制。
- 「diff 大说明它改得认真。」 diff 大说明审查成本高。 一次改动只该解决一个问题——混进来的「顺手」会让「这次改动为什么失败」变得无法定位。
- 「单测都过了,应该没问题。」 单测过只说明「测到的部分对」。 一致性错误恰恰发生在「测试没覆盖的调用方」上。 所以要看的是「有没有全量测试」,不是「单测绿不绿」。
自测三问(点开对答案)
1. 三类错误各自怎么认出来? —— **事实错误:结果怪但不报错**(比如 round(2.5) 给 2);**范围错误:diff 行数远超预期**; **一致性错误:单测过、集成挂**。**认出来才知道加哪道防线。** 2. 为什么「加了默认值的签名改动」比「直接崩」更危险? —— **因为它不报错,会静默地按默认值跑。** **程序照常出结果,只是结果不是你想要的**——这种错可能几个月后才被发现。 3. 三类错误的共同点是什么? —— **没有一类能靠「让它更认真」解决。** **都得靠外部机制:测试、边界、检查。** 这也是第 7 章「先约束再放权」的来源。下一篇 2.5 收这一章:知道两条边界之后,具体该怎么配合——三条方法,都能量出成本。
- ☐ (本篇完,下一篇 2.5 从边界意识到的协作方法)