Skip to content

程序排错与验证 ​

异常信息可以告诉我们程序在哪里失败,但有些问题不能只靠 Traceback 判断:程序可能没有报错,结果却不对;也可能在出错前经过了很多步骤,不容易看出错误数据从哪里产生。

排错时,我们需要复现问题、观察程序状态、找到原因,再验证修改是否符合需求。断点调试可以让程序暂停,日志可以留下执行记录,测试可以重复检查结果。使用 PyCharm、VS Code 或 AI 工具,都需要这套基本方法。

本章继续沿用前两章的排错思路,完成一条完整流程:

  1. 复现问题。
  2. 根据 Traceback、输出或断点收集证据。
  3. 确认变量变化和执行顺序,找到原因。
  4. 根据需求修改代码。
  5. 重新运行,并把关键情况写成可以重复执行的检查。

Traceback 的基本读法见异常信息与定位,日志配置的完整用法见后面的 logging 日志。本章用小车案例把排错方法串起来,PyCharm 的界面操作另见PyCharm 调试参考。

程序排错循环

问题复现与预期 ​

排错时经常遇到三类问题:

  1. 程序抛出异常,但还不清楚错误数据从哪里产生。
  2. 程序可以运行,结果却不符合预期。
  3. 变量值或代码执行顺序和预期不同。

观察程序时,重点回答三个问题:

  • 程序停在什么位置?
  • 当前变量是什么值?
  • 程序按照什么顺序执行?

案例:小车程序排错 ​

需求: 修复小车程序中的平均速度计算和刹车边界问题,使运行结果符合“不行驶时平均速度为 0、小车不允许倒车”的业务规则。

功能分析: 先保留两个可复现的问题,记录预期结果与实际结果;再通过 Traceback、日志和断点确认错误数据产生的位置,最后完成修复和回归检查。

下面的程序模拟一辆小车。小车可以加速、刹车、显示里程和计算平均速度。为了方便计算,约定每次有效操作后都按当前速度行驶 1 小时,查询结果显示的是本次行驶前的状态;实际驾驶不会按这个方式计时。

先确定两条业务规则:没有行驶记录时,平均速度显示为 0;小车不允许倒车,刹车后的速度最低为 0。下面的代码故意保留了两处问题:

python
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 报错

按照前面章节的排查方法,可以先确认:

  • 异常类型是 ZeroDivisionError。
  • 出错位置在 average_speed() 的除法表达式。
  • 还需要继续确认 self.time 为什么是 0。

Traceback 能指出失败位置,但修复之前还要回答:是时间没有更新、查询发生得太早,还是缺少初始状态的处理?先检查运行数据,再决定怎样修改。

运行状态观察 ​

临时输出与日志 ​

只需要检查几个变量时,可以先在 average_speed() 的除法前加入临时输出:

python
def average_speed(self):
    print(f"计算前:{self.odometer=}, {self.time=}")
    return self.odometer / self.time

重新启动并输入 s,报错前会输出:

text
计算前:self.odometer=0, self.time=0

如果需要反复观察同一段流程,可以使用日志。在文件顶部导入模块,在程序入口配置一次输出级别:

python
import logging


# Car 类定义仍放在这里。


if __name__ == "__main__":
    logging.basicConfig(
        level=logging.DEBUG,
        format="%(levelname)s | %(message)s",
    )
    my_car = Car()
    print("我是一辆车!")
    my_car.main()

然后把 brake() 改成下面的观察版本,暂时保留错误逻辑:

python
def brake(self):
    logging.debug("刹车前 speed=%s", self.speed)
    self.speed -= 5
    logging.debug("刹车后 speed=%s", self.speed)

重新启动并输入 b,日志会显示:

text
DEBUG | 刹车前 speed=0
DEBUG | 刹车后 speed=-5

这说明负数在刹车时就已经产生。继续查看 step(),可以发现负速度又被累加到了里程中。日志帮助我们沿着数据变化追查原因,即使程序没有抛出异常也能使用。

无报错逻辑 Bug 溯源:数据污染传导链 vs 根因源头修复

这里仅使用 DEBUG 记录中间状态。日志级别和文件输出在标准库部分展开;异常处理中的 logging.exception() 用来记录完整 Traceback,不能代替这里的状态观察。

断点与单步调试 ​

变量较多,或者需要跟踪函数调用时,可以让程序停下来检查。PyCharm 和 VS Code 都可以完成下面的操作,按钮位置按自己使用的编辑器查找:

  1. 在 average_speed() 的除法语句设置断点,以 Debug 模式启动程序。
  2. 输入 s,暂停后展开 self,查看 time 和 odometer。
  3. 查看调用栈,回到 main(),找到调用 average_speed() 的位置。
  4. 需要追查先后顺序时,重新运行,在处理 action == "S" 的分支暂停,再单步观察。

常用操作先掌握这些:

操作用途
Step Over(单步跳过)执行当前行,函数调用作为一步执行
Step Into(单步进入)进入被调用函数,查看内部过程
Step Out(单步跳出)运行完当前函数,回到调用位置
Resume(继续)继续运行,直到下一个断点、触发配置的异常暂停或程序结束

IDE 断点单步调试 4 大核心动作行为与视界流转

源码断点通常停在该行执行之前。原始程序在除法处继续执行会抛出异常,不能期待它正常返回并运行到 step()。需要完整走完这条路径时,先完成下面的修复,再单步验证。

原因定位与代码修复 ​

平均速度除零 ​

输入 S 后,相关代码按下面的顺序执行:

python
elif action == "S":
    print(f"汽车的平均速度为 {self.average_speed()} 公里/小时")

self.step()

结合变量值和调用位置,可以还原出完整过程:

  1. 小车刚创建时,self.time 是 0。
  2. 第一次输入 S,程序先调用 average_speed()。
  3. self.step() 还没有执行,self.time 仍然是 0。
  4. self.odometer / self.time 因除数为 0 抛出异常。

按照“没有行驶记录时平均速度显示为 0”的规则,修改 average_speed():

python
def average_speed(self):
    # 没有行驶记录时,按照本例的业务规则返回 0。
    if self.time == 0:
        return 0
    return self.odometer / self.time

这里处理的是允许出现的初始状态。直接把 self.time 初始化为 1 会凭空增加一小时;把 step() 移到查询之前则会改变查询时机,都不符合当前规则。

如果需求改为“没有行驶记录时显示文字提示”,还需要同步调整返回值和调用处的输出,不能把提示文字继续拼进“公里/小时”的结果中。

刹车后的速度边界 ​

brake() 每次都减去 5,没有检查速度下限;step() 又把负速度累加到了里程中。因此应在速度产生的位置修复:

python
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,再输入 ax 提示无效操作,不增加时间和里程;随后加速正常

时间可以通过临时输出或断点查看。验证时既看显示结果,也检查实际状态,避免只把错误数字藏起来。

断言回归检查 ​

把输入和预期结果写成能重复执行的检查,就是简单的自动化测试。assert 条件 表示要求条件成立;条件为假时,会抛出 AssertionError。

把下面的函数放在 Car 类定义之后,在文件底部入口中先调用 check_car(),再启动交互程序:

python
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,再把入口改成先执行检查、后启动交互程序:

python
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 或异常处理。本章先掌握测试思路,不展开测试框架。

assert 断言守卫机制与自动化回归防护网

AI 辅助排错 ​

已经复现问题但仍不清楚原因,或者需要检查修复方案时,可以请 AI 协助。提问时给出环境、最小可运行代码、复现步骤、完整报错、预期结果和实际结果。先去掉代码和日志中的密码、令牌等敏感信息。

例如,针对小车刹车问题,可以这样提问,并在后面附上上面的完整 Car 代码:

text
环境:Python 3.14。
复现步骤:重新启动程序,第一次输入 b。
预期结果:小车不允许倒车,刹车后速度和里程都为 0。
实际结果:速度和里程都变成 -5,没有异常和 Traceback。
已观察到:brake() 执行前 speed 为 0,执行后为 -5。

请根据代码指出可能原因和对应位置,给出验证原因的方法,
再提出最小修改,以及修改后需要重新检查的正常情况和边界情况。
保持现有菜单和每次有效操作后行驶 1 小时的规则。

收到建议后,按下面的顺序核对:

  1. 检查原因是否与日志或断点看到的事实一致;没有证据的判断先验证。
  2. 阅读修改内容,确认它修复了速度状态,也没有改变其他业务规则。
  3. 执行原来的失败操作,运行回归检查,再核对正常操作。

例如,AI 如果建议显示 abs(self.speed),负速度仍会参与里程计算;如果建议捕获所有异常再返回 0,也无法修复这个没有异常的逻辑错误。

AI 可以协助生成测试,但预期结果要根据需求核对。如果实现和测试都误以为允许倒车,测试通过也不能证明它符合本案例的规则。记录自己实际执行的命令、输入和结果,不把 AI 给出的预计输出当作验证结果。

AI 辅助排错:高效提问协议与三步核验闭环

综合练习 ​

  1. 在原始程序上分别复现首次查询除零、首次刹车得到负数,记录预期结果和实际结果。
  2. 用临时输出或日志定位负速度的来源,再用断点确认首次查询时 step() 尚未执行。
  3. 完成两处修复,运行 check_car(),再按验证表执行交互操作。
  4. 把“没有行驶记录时返回 0”改为“显示没有行驶记录的文字提示”,同步修改调用处和检查,再验证首次输入 s。注意区分“完全没有行驶记录”和“已经经过一小时但速度为 0”。
  5. 请 AI 审查其中一处修改,记录它的判断、你收集的证据和最终验证结果。

小结 ​

本章重点掌握五件事:

  1. 排错先复现问题,写清预期结果和实际结果。
  2. Traceback 帮助定位失败位置,输出、日志和断点帮助确认运行状态。
  3. 修复方案要符合需求,程序没有报错也可能存在逻辑错误。
  4. 修改后重跑失败操作、正常操作和边界情况,用回归检查保留验证过程。
  5. AI 的原因分析和修改建议需要通过实际运行核对。