2.41. 调试¶
在摄像头上失败的脚本大多以三种方式之一失败:抛出异常、产生错误的值,或者卡死。每一种都有一套不同的应对工具。
2.41.1. 阅读回溯信息¶
当脚本抛出异常而没有任何代码处理它时,REPL 或 IDE 会打印一段 回溯信息(traceback) —— 一份从最外层脚本一直到抛出异常那一行的调用链记录。
回溯信息要自下而上阅读:
最底下一行给出异常类及其消息(
ValueError: invalid literal for int()...)。它上方的每一个
File "...", line N, in <name>块都是一个 帧(frame) —— 越往上走就深入一层调用。最顶部的帧是脚本开始的地方;最底部的帧是错误触发的地方。
先读最底下,弄清楚 出了什么 问题,再往上走,看看脚本是 如何 走到这一步的。行号准确指向脚本中的源代码位置。
2.41.2. Print 调试¶
弄清脚本在做什么最快的办法就是打印出可疑的值。三个内置函数能让打印更有用:
repr()—— 返回某个值的开发者风格字符串。print(repr(value))能把"5"与5、None与"None"区分开,而单纯的print()做不到。type()—— 返回某个值的类。print(type(value))就是用来查明那个“本该是 int”的变量是不是偷偷变成了字符串的方法。len()—— 序列或集合的长度。出乎意料地有相当大一部分 bug 是差一错误或大小不匹配问题。
print("got:", repr(value), "type:", type(value), "len:", len(value))
在你关心的每个分支里都放一句打印 —— if 的两个分支、每个 except 块、你怀疑一次都没执行的循环体。代价不过是一行输出;而收获是弄清你 以为 在运行的代码路径是不是真正在运行的那条。
2.41.3. 探查一个对象¶
两个内置函数能回答“我能拿这东西做什么”:
把它们配合使用:dir 找到名称,help 解释它的作用。
2.41.3.1. 用 dir 查找名称¶
>>> dir([1, 2, 3])
['__add__', '__class__', '__contains__', '__delitem__',
'__eq__', '__ge__', ..., 'append', 'clear', 'copy',
'count', 'extend', 'index', 'insert', 'pop', 'remove',
'reverse', 'sort']
列表开头的一大块是从每个对象继承来的双下划线方法;值得扫一眼的名称通常在它们之后。dir 对任何东西都管用 —— 类、实例、模块、内置类型:
>>> import json
>>> dir(json)
['__name__', 'dump', 'dumps', 'load', 'loads']
第二种形式就是无需离开 REPL 就能查明某个模块实际暴露了哪些顶层名称的方法。
2.41.3.2. 用 help 查询它¶
一旦 dir 浮现出一个候选项,help 就来描述它:
>>> help(str.split)
split(sep=None, maxsplit=-1)
Return a list of the words in the string, ...
在 MicroPython 上,help 比 CPython 上更精简 —— 有时只有签名,有时是一行文档字符串,有时对内置 C 函数则什么都没有。但在 IDE 工具提示不在手边时,它仍是一个快速的提醒手段。
2.41.4. 当出现卡死时¶
一个不返回的脚本比一个抛出异常的脚本更难诊断。常见的罪魁祸首有:
条件永远不会变为假的
while循环。每次迭代打印一下循环变量;如果值没有变化,那循环体就有 bug。一个阻塞调用在等待永远不会到来的输入 —— 从空队列读取、没有终点的休眠。在该调用前后各放一句打印,看脚本卡在哪一行。
无限递归。当它最终触发时(伴随
RecursionError),回溯信息通常正好指向它。
对卡死脚本最有效的补救办法是 IDE 的 stop 按钮,它会通过 USB 向脚本发送一个 KeyboardInterrupt。该中断会以回溯信息的形式浮现在当前正在运行的那一行 —— 往往正是那个不返回的行。
备注
如果某个卡死抵挡住了所有诊断 —— 脚本看起来没问题、中断回溯指向某个内置函数或固件代码而非你的脚本、或者同样的代码在先前的固件版本上能正常工作 —— 那么原因可能是固件 bug 而非脚本 bug。把脚本精简成仍会卡死的最小可复现样例,然后到 OpenMV 论坛 提交报告。请附上固件版本、运行所在的开发板,以及精简后的脚本。
2.41.5. 发布前移除诊断代码¶
开发过程中有策略地打印很棒;但生产脚本里残留上百个打印调用会让输出杂乱不堪,还会占用本可用于真正工作的堆内存。一旦 bug 修复了,就把打印移除(或者用一个可以关闭的调试开关把它们守护起来)。
对于那些应当长期保留在代码路径中的诊断信息,请从 print() 切换到 logging 模块。它会给每条消息附上一个 级别(debug、info、warning、error),并让一个单一的设置就能在生产环境中让安静的那些消息静默:
import logging
log = logging.getLogger("main")
log.info("starting up")
log.debug("loaded config: %s", config)
log.warning("falling back to defaults")
把日志记录器的级别设为 logging.WARNING,能让 info 和 debug 调用几乎不耗费任何成本(消息字符串根本不会被构建),而无需注释掉任何行。这使得 logging 成为 永久性 诊断的正确工具;而原始的 print 用于 用完即弃 的诊断就够了。