(一)沿 traceback 找到失败的动作
1. 先读错误,再还原调用过程
先保存这份错误示例。任务是把每行的分钟数相加,第二行却写成了“很多”:
# 项目文件:debug_demo/trace_failure.py
# 错误示例:第二行不能转成整数,用它观察异常从哪里产生、经过哪些调用。
def parse_minutes(text):
return int(text)
def total_minutes(lines):
total = 0
for line in lines:
total += parse_minutes(line)
return total
def main():
print(total_minutes(["20", "很多", "45"]))
if __name__ == "__main__":
main()# 在 debug_demo 目录运行,让解释器显示未被接住的异常。
python trace_failure.py
最后一行是 ValueError: invalid literal for int() with base 10: '很多'。先读出“整数转换失败”和实际值 '很多',再向上找到 return int(text)。这里是产生错误的动作;继续向上,可以看到它由 total_minutes 的加法调用,再由 main 发起。traceback 从较外层的调用列到较内层,最后给出异常类型和说明。
当第二行失败时,total 已经是 20,但这一轮的加法尚未完成,第三行也没有读取。把注意力放在 print 或总和初值上,不能解释这次转换失败。
上一章的 raise RecordError(...) from error 会同时保留“第几条记录错误”和底层转换原因。看到异常链时,先定位各段最后的类型与说明,再把它们连成“哪个业务动作调用了哪个底层动作”。
2. 保留一次可重复运行的失败输入
排查文件导入时,先记下启动命令、解释器版本、输入文件和实际结果。输入里有很多书,可以从副本中逐步删掉无关记录;删完后仍出现同一个错误,剩下的内容就更容易逐项观察。
本例从三行缩成 ["很多"],仍然在 int 处失败。需要的是数字时,修正数据;需要向读者说明哪一行错误时,沿用第五章的记录检查和异常链。把它改成 except Exception: pass 会跳过这条记录,却不能完成“统计所有有效输入并反馈坏记录”的任务。
记录失败时还要区分结果:程序在什么动作抛出异常;程序正常结束却给出错误答案;程序一直等待;同一输入两次得到不同结果。后面几种情况没有现成的错误行,需要检查执行中的状态。