16.4 调试

所有不那么简单的软件都会有错误3。当问题发生后,你要能找到它们发生在哪里,并怀着希望去寻找一种能修正这些错误的方法。如果这些是你自己的代码,那么尤其如此。如果问题发生在一个简单脚本中,你通常可以访问所有的变量,因此找到问题并不难。

3航天飞机中的软件以在420 000行代码中只包含一个错误而闻名,但是它采用非常正式的开发方法、代码评审以及广泛的测试,这些成本可不低。

问题往往被深深地掩藏在多个函数嵌套调用中的某处。在这种情况下,你需要一个策略,能在调用栈的每一层检查程序的状态(“调用栈”是个行话,它就是函数的调用列表,从中可以看到你的代码在哪一层被调用)。

当错误发生时,traceback函数能告诉你最后一个错误的发生位置。首先,让我们定义一些可能会发生错误的函数:

  1. outer_fn <- function(x) inner_fn(x)
  2. inner_fn <- function(x) exp(x)

现在,让我们使用一个错误的输入调用outer_fn(然后再调用inner_fn):

  1. outer_fn(list(1))
  2. ## Error: non-numeric argument to mathematical function

traceback现在能告诉我们错误发生前所调用的函数(请看图16-2)。

图像说明文字

图16-2:使用traceback查看调用栈

一般来说,如果它不是一个明显的bug,我们不知道调用栈在哪里出现问题。一个合理的策略是在抛出错误的函数的开始,如果需要,沿着栈往上追溯。要做到这一点,我们需要一种方法来在抛出错误的地方停止执行代码。方法之一是在错误点之前添加browser函数(因为使用了traceback,所以我们知道错误发生之处):

  1. inner_fn <- function(x)
  2. {
  3. browser() # 在这里停止执行
  4. exp(x)
  5. }

browser会在到达时暂停执行,这让我们有时间来检查程序。在大多数情况下,我们会调用ls.str来查看当前所有的变量值。在本例中,我们看到x是一个列表而不是一个数值向量,这导致了exp失败。

找出错误的另一种策略是设置全局的error选项。这种策略最适于错误位于其他人写的包里面时,因为在那里很难调用browser。(你可以使用fixInNamespace函数在安装包里面改变函数,此变化会持续到你把R关闭为止。)

error选项能接受不带参数的函数,并且会在错误抛出时被调用。举个简单的例子,我们可以将其设置为发生错误后打印一条消息,如图16-3所示。

图像说明文字

图16-3:覆盖全局error选项

尽管显示一条表示同情的消息算是对错误的一点安慰,但这对解决问题并没有什么帮助。更为有用的方法是R中自带的recover函数。在错误抛出后,recover可以让你跳进调用栈中的任何函数(如图16-4所示)。

图像说明文字

图16-4:使用error = recover的调用栈

你也可以通过debug函数来逐行调试函数。不过,使用innerouter这些单行函数有点无聊,所以我们会以另一种方式进行测试。包含在learningr包中的buggy_countplyr包中的count函数的错误版本,当你给它传递一个因子时它会很隐晦地出错。在命令行下只需按下Enter键即可一直运行此函数,直到找到问题:

  1. debug(buggy_count)
  2. x <- factor(sample(c("male", "female"), 20, replace = TRUE))
  3. buggy_count(x)

count(以及经过我们扩展的buggy_count函数)接受一个数据框或向量作为第一个参数。如果df的参数是一个向量,那么该函数会将它插入到数据框中。

图16-5显示当了我们到达这部分代码时会发生什么。当df是一个因子时,我们希望它被放在一个数据框里面。不幸的是,is.vector对因子会返回FALSE,步进也将被忽略。因子不是向量,因为它们除了名字外还拥有属性。代码中真正需要包含的(并且是在正确的plyr版本中)是调用is.atomic,它对于因子和其他向量类型都会返回TRUE,和numeric一样。

图像说明文字

图16-5 . 调试buggy_count函数

要退出调试器只需在命令行键入Q。因为debug函数的作用,调试器将在每一个函数被调用时启动。要关闭调试器,只需调用undebug

  1. undebug(buggy_count)

可以使用debugonce作为替代方案,它只在函数第一次被调用时调用调试器4。

4Open Analytics的Tobias Verbeke曾经打趣说:“debugonce函数太乐观了,我想如果有debugtwice可能会更好。”