2.41. 调试

在摄像头上失败的脚本大多以三种方式之一失败:抛出异常、产生错误的值,或者卡死。每一种都有一套不同的应对工具。

2.41.1. 阅读回溯信息

当脚本抛出异常而没有任何代码处理它时,REPL 或 IDE 会打印一段 回溯信息(traceback) —— 一份从最外层脚本一直到抛出异常那一行的调用链记录。

回溯信息要自下而上阅读:

  • 最底下一行给出异常类及其消息(ValueError: invalid literal for int()...)。

  • 它上方的每一个 File "...", line N, in <name> 块都是一个 帧(frame) —— 越往上走就深入一层调用。

  • 最顶部的帧是脚本开始的地方;最底部的帧是错误触发的地方。

先读最底下,弄清楚 出了什么 问题,再往上走,看看脚本是 如何 走到这一步的。行号准确指向脚本中的源代码位置。

2.41.3. 探查一个对象

两个内置函数能回答“我能拿这东西做什么”:

  • dir() —— 返回某个对象上定义的所有名称的列表:方法、属性、双下划线方法,应有尽有。

  • help() —— 打印函数、方法或类的文档字符串(在 CPython 上还包括签名)。

把它们配合使用: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 模块。它会给每条消息附上一个 级别debuginfowarningerror),并让一个单一的设置就能在生产环境中让安静的那些消息静默:

import logging

log = logging.getLogger("main")
log.info("starting up")
log.debug("loaded config: %s", config)
log.warning("falling back to defaults")

把日志记录器的级别设为 logging.WARNING,能让 infodebug 调用几乎不耗费任何成本(消息字符串根本不会被构建),而无需注释掉任何行。这使得 logging 成为 永久性 诊断的正确工具;而原始的 print 用于 用完即弃 的诊断就够了。