程序排错与验证
异常信息可以告诉我们程序在哪里失败,但有些问题不能只靠 Traceback 判断:程序可能没有报错,结果却不对;也可能在出错前经过了很多步骤,不容易看出错误数据从哪里产生。
排错时,我们需要复现问题、观察程序状态、找到原因,再验证修改是否符合需求。断点调试可以让程序暂停,日志可以留下执行记录,测试可以重复检查结果。使用 PyCharm、VS Code 或 AI 工具,都需要这套基本方法。
本章继续沿用前两章的排错思路,完成一条完整流程:
- 复现问题。
- 根据 Traceback、输出或断点收集证据。
- 确认变量变化和执行顺序,找到原因。
- 根据需求修改代码。
- 重新运行,并把关键情况写成可以重复执行的检查。
Traceback 的基本读法见异常信息与定位,日志配置的完整用法见后面的 logging 日志。本章用小车案例把排错方法串起来,PyCharm 的界面操作另见PyCharm 调试参考。
问题复现与预期
排错时经常遇到三类问题:
- 程序抛出异常,但还不清楚错误数据从哪里产生。
- 程序可以运行,结果却不符合预期。
- 变量值或代码执行顺序和预期不同。
观察程序时,重点回答三个问题:
- 程序停在什么位置?
- 当前变量是什么值?
- 程序按照什么顺序执行?
案例:小车程序排错
需求: 修复小车程序中的平均速度计算和刹车边界问题,使运行结果符合“不行驶时平均速度为 0、小车不允许倒车”的业务规则。
功能分析: 先保留两个可复现的问题,记录预期结果与实际结果;再通过 Traceback、日志和断点确认错误数据产生的位置,最后完成修复和回归检查。
下面的程序模拟一辆小车。小车可以加速、刹车、显示里程和计算平均速度。为了方便计算,约定每次有效操作后都按当前速度行驶 1 小时,查询结果显示的是本次行驶前的状态;实际驾驶不会按这个方式计时。
先确定两条业务规则:没有行驶记录时,平均速度显示为 0;小车不允许倒车,刹车后的速度最低为 0。下面的代码故意保留了两处问题:
class Car:
def __init__(self, speed=0):
self.speed = speed
self.odometer = 0
self.time = 0
def say_state(self):
print(f"里程 {self.odometer},目前时速 {self.speed}")
def accelerate(self):
self.speed += 5
def brake(self):
self.speed -= 5
def step(self):
# 教学模型:每次有效操作后,按当前速度行驶 1 小时。
self.odometer += self.speed
self.time += 1
print("小车往前开动了一段距离")
def average_speed(self):
# 这里故意没有处理 time 为 0 的情况,供排错练习使用。
return self.odometer / self.time
def main(self):
while True:
action = input(
"我该做什么?[A] 加速,[B] 刹车,"
"[O] 显示里程表,[S] 显示平均速度?"
).upper()
if action not in "ABOS" or len(action) != 1:
print("我不知道该怎么做")
continue
if action == "A":
self.accelerate()
elif action == "B":
self.brake()
elif action == "O":
print(f"车开了 {self.odometer} 公里")
elif action == "S":
print(f"汽车的平均速度为 {self.average_speed()} 公里/小时")
self.step()
self.say_state()
if __name__ == "__main__":
my_car = Car()
print("我是一辆车!")
my_car.main()问题复现步骤
每次复现都重新启动程序,避免上一次操作改变小车状态:
| 操作 | 预期结果 | 当前结果 |
|---|---|---|
启动后首次输入 s | 平均速度为 0 | 抛出 ZeroDivisionError |
重新启动后首次输入 b | 速度和里程都为 0 | 速度和里程都变成 -5,没有报错 |
程序判断操作前会调用 .upper(),因此输入小写字母也能触发对应操作。程序没有退出菜单,结束本次运行时可以按 Ctrl+C;由此产生的 KeyboardInterrupt 表示我们主动中断程序。
Traceback 范围定位
首次输入 s,可以看到除零报错。下图沿用旧版 PyCharm 截图,路径和行号以自己的代码为准:

按照前面章节的排查方法,可以先确认:
- 异常类型是
ZeroDivisionError。 - 出错位置在
average_speed()的除法表达式。 - 还需要继续确认
self.time为什么是0。
Traceback 能指出失败位置,但修复之前还要回答:是时间没有更新、查询发生得太早,还是缺少初始状态的处理?先检查运行数据,再决定怎样修改。
运行状态观察
临时输出与日志
只需要检查几个变量时,可以先在 average_speed() 的除法前加入临时输出:
def average_speed(self):
print(f"计算前:{self.odometer=}, {self.time=}")
return self.odometer / self.time重新启动并输入 s,报错前会输出:
计算前:self.odometer=0, self.time=0如果需要反复观察同一段流程,可以使用日志。在文件顶部导入模块,在程序入口配置一次输出级别:
import logging
# Car 类定义仍放在这里。
if __name__ == "__main__":
logging.basicConfig(
level=logging.DEBUG,
format="%(levelname)s | %(message)s",
)
my_car = Car()
print("我是一辆车!")
my_car.main()然后把 brake() 改成下面的观察版本,暂时保留错误逻辑:
def brake(self):
logging.debug("刹车前 speed=%s", self.speed)
self.speed -= 5
logging.debug("刹车后 speed=%s", self.speed)重新启动并输入 b,日志会显示:
DEBUG | 刹车前 speed=0
DEBUG | 刹车后 speed=-5这说明负数在刹车时就已经产生。继续查看 step(),可以发现负速度又被累加到了里程中。日志帮助我们沿着数据变化追查原因,即使程序没有抛出异常也能使用。
这里仅使用 DEBUG 记录中间状态。日志级别和文件输出在标准库部分展开;异常处理中的 logging.exception() 用来记录完整 Traceback,不能代替这里的状态观察。
断点与单步调试
变量较多,或者需要跟踪函数调用时,可以让程序停下来检查。PyCharm 和 VS Code 都可以完成下面的操作,按钮位置按自己使用的编辑器查找:
- 在
average_speed()的除法语句设置断点,以 Debug 模式启动程序。 - 输入
s,暂停后展开self,查看time和odometer。 - 查看调用栈,回到
main(),找到调用average_speed()的位置。 - 需要追查先后顺序时,重新运行,在处理
action == "S"的分支暂停,再单步观察。
常用操作先掌握这些:
| 操作 | 用途 |
|---|---|
| Step Over(单步跳过) | 执行当前行,函数调用作为一步执行 |
| Step Into(单步进入) | 进入被调用函数,查看内部过程 |
| Step Out(单步跳出) | 运行完当前函数,回到调用位置 |
| Resume(继续) | 继续运行,直到下一个断点、触发配置的异常暂停或程序结束 |
源码断点通常停在该行执行之前。原始程序在除法处继续执行会抛出异常,不能期待它正常返回并运行到 step()。需要完整走完这条路径时,先完成下面的修复,再单步验证。
原因定位与代码修复
平均速度除零
输入 S 后,相关代码按下面的顺序执行:
elif action == "S":
print(f"汽车的平均速度为 {self.average_speed()} 公里/小时")
self.step()结合变量值和调用位置,可以还原出完整过程:
- 小车刚创建时,
self.time是0。 - 第一次输入
S,程序先调用average_speed()。 self.step()还没有执行,self.time仍然是0。self.odometer / self.time因除数为0抛出异常。
按照“没有行驶记录时平均速度显示为 0”的规则,修改 average_speed():
def average_speed(self):
# 没有行驶记录时,按照本例的业务规则返回 0。
if self.time == 0:
return 0
return self.odometer / self.time这里处理的是允许出现的初始状态。直接把 self.time 初始化为 1 会凭空增加一小时;把 step() 移到查询之前则会改变查询时机,都不符合当前规则。
如果需求改为“没有行驶记录时显示文字提示”,还需要同步调整返回值和调用处的输出,不能把提示文字继续拼进“公里/小时”的结果中。
刹车后的速度边界
brake() 每次都减去 5,没有检查速度下限;step() 又把负速度累加到了里程中。因此应在速度产生的位置修复:
def brake(self):
self.speed = max(0, self.speed - 5)max() 取两个数中较大的一个,让刹车后的速度最低为 0。只修改显示文字,或者只限制里程,都会留下错误的速度状态。
修复后删除已经完成任务的临时输出。需要保留的调试日志仍使用 DEBUG,日常运行可以把入口日志级别调整为 INFO。
修复验证与回归检查
完整流程验证
修改完成后,先重跑原来的复现步骤,再检查正常功能。每一行都从重新启动开始:
| 输入顺序 | 应观察到的结果 |
|---|---|
s | 平均速度显示为 0;本次操作后时间为 1、里程为 0 |
a、s | 加速后速度为 5、里程为 5;查询显示平均速度 5.0,查询操作结束后里程为 10、时间为 2 |
b、b | 速度和里程一直为 0,时间依次变成 1、2 |
x,再输入 a | x 提示无效操作,不增加时间和里程;随后加速正常 |
时间可以通过临时输出或断点查看。验证时既看显示结果,也检查实际状态,避免只把错误数字藏起来。
断言回归检查
把输入和预期结果写成能重复执行的检查,就是简单的自动化测试。assert 条件 表示要求条件成立;条件为假时,会抛出 AssertionError。
把下面的函数放在 Car 类定义之后,在文件底部入口中先调用 check_car(),再启动交互程序:
def check_car():
car = Car()
assert car.average_speed() == 0, "没有行驶记录时应返回 0"
assert car.time == 0, "计算平均速度不应改变时间"
car.accelerate()
car.step()
assert car.speed == 5
assert car.odometer == 5
assert car.time == 1
assert car.average_speed() == 5
car.brake()
car.step()
car.brake()
car.step()
assert car.speed == 0, "连续刹车后速度不能为负"
assert car.odometer == 5, "停车后里程不能减少"
assert car.time == 3
assert car.average_speed() == 5 / 3
slow_car = Car(speed=3)
slow_car.brake()
assert slow_car.speed == 0, "速度低于 5 时刹车也应停在 0"
print("小车检查通过")修复完成后,在文件顶部导入 logging,再把入口改成先执行检查、后启动交互程序:
if __name__ == "__main__":
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s | %(message)s",
)
check_car()
my_car = Car()
print("我是一辆车!")
my_car.main()这组检查应该在原始代码上失败、修复后通过。修复一个问题后保留对应检查,以后修改代码时重新运行,确认旧问题没有再次出现,这就是回归检查。
函数检查不经过 input(),因此不能代替上一小节的完整操作。反过来,只看到菜单能正常显示,也不能说明每个计算结果都正确。
断言的使用范围
这里的 assert 用于开发期检查,使用普通的 Python 运行方式即可。python3 -O 会移除断言,不能用于运行这些检查;正式的用户输入校验仍然使用 if、raise 或异常处理。本章先掌握测试思路,不展开测试框架。
AI 辅助排错
已经复现问题但仍不清楚原因,或者需要检查修复方案时,可以请 AI 协助。提问时给出环境、最小可运行代码、复现步骤、完整报错、预期结果和实际结果。先去掉代码和日志中的密码、令牌等敏感信息。
例如,针对小车刹车问题,可以这样提问,并在后面附上上面的完整 Car 代码:
环境:Python 3.14。
复现步骤:重新启动程序,第一次输入 b。
预期结果:小车不允许倒车,刹车后速度和里程都为 0。
实际结果:速度和里程都变成 -5,没有异常和 Traceback。
已观察到:brake() 执行前 speed 为 0,执行后为 -5。
请根据代码指出可能原因和对应位置,给出验证原因的方法,
再提出最小修改,以及修改后需要重新检查的正常情况和边界情况。
保持现有菜单和每次有效操作后行驶 1 小时的规则。收到建议后,按下面的顺序核对:
- 检查原因是否与日志或断点看到的事实一致;没有证据的判断先验证。
- 阅读修改内容,确认它修复了速度状态,也没有改变其他业务规则。
- 执行原来的失败操作,运行回归检查,再核对正常操作。
例如,AI 如果建议显示 abs(self.speed),负速度仍会参与里程计算;如果建议捕获所有异常再返回 0,也无法修复这个没有异常的逻辑错误。
AI 可以协助生成测试,但预期结果要根据需求核对。如果实现和测试都误以为允许倒车,测试通过也不能证明它符合本案例的规则。记录自己实际执行的命令、输入和结果,不把 AI 给出的预计输出当作验证结果。
综合练习
- 在原始程序上分别复现首次查询除零、首次刹车得到负数,记录预期结果和实际结果。
- 用临时输出或日志定位负速度的来源,再用断点确认首次查询时
step()尚未执行。 - 完成两处修复,运行
check_car(),再按验证表执行交互操作。 - 把“没有行驶记录时返回
0”改为“显示没有行驶记录的文字提示”,同步修改调用处和检查,再验证首次输入s。注意区分“完全没有行驶记录”和“已经经过一小时但速度为0”。 - 请 AI 审查其中一处修改,记录它的判断、你收集的证据和最终验证结果。
小结
本章重点掌握五件事:
- 排错先复现问题,写清预期结果和实际结果。
- Traceback 帮助定位失败位置,输出、日志和断点帮助确认运行状态。
- 修复方案要符合需求,程序没有报错也可能存在逻辑错误。
- 修改后重跑失败操作、正常操作和边界情况,用回归检查保留验证过程。
- AI 的原因分析和修改建议需要通过实际运行核对。