16.4 调试
所有不那么简单的软件都会有错误3。当问题发生后,你要能找到它们发生在哪里,并怀着希望去寻找一种能修正这些错误的方法。如果这些是你自己的代码,那么尤其如此。如果问题发生在一个简单脚本中,你通常可以访问所有的变量,因此找到问题并不难。
3航天飞机中的软件以在420 000行代码中只包含一个错误而闻名,但是它采用非常正式的开发方法、代码评审以及广泛的测试,这些成本可不低。
问题往往被深深地掩藏在多个函数嵌套调用中的某处。在这种情况下,你需要一个策略,能在调用栈的每一层检查程序的状态(“调用栈”是个行话,它就是函数的调用列表,从中可以看到你的代码在哪一层被调用)。
当错误发生时,traceback
函数能告诉你最后一个错误的发生位置。首先,让我们定义一些可能会发生错误的函数:
outer_fn <- function(x) inner_fn(x)
inner_fn <- function(x) exp(x)
现在,让我们使用一个错误的输入调用outer_fn
(然后再调用inner_fn
):
outer_fn(list(1))
## Error: non-numeric argument to mathematical function
traceback
现在能告诉我们错误发生前所调用的函数(请看图16-2)。
图16-2:使用traceback
查看调用栈
一般来说,如果它不是一个明显的bug,我们不知道调用栈在哪里出现问题。一个合理的策略是在抛出错误的函数的开始,如果需要,沿着栈往上追溯。要做到这一点,我们需要一种方法来在抛出错误的地方停止执行代码。方法之一是在错误点之前添加browser
函数(因为使用了traceback
,所以我们知道错误发生之处):
inner_fn <- function(x)
{
browser() # 在这里停止执行
exp(x)
}
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
函数来逐行调试函数。不过,使用inner
和outer
这些单行函数有点无聊,所以我们会以另一种方式进行测试。包含在learningr
包中的buggy_count
是plyr
包中的count
函数的错误版本,当你给它传递一个因子时它会很隐晦地出错。在命令行下只需按下Enter键即可一直运行此函数,直到找到问题:
debug(buggy_count)
x <- factor(sample(c("male", "female"), 20, replace = TRUE))
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
:
undebug(buggy_count)
可以使用debugonce
作为替代方案,它只在函数第一次被调用时调用调试器4。
4Open Analytics的Tobias Verbeke曾经打趣说:“debugonce
函数太乐观了,我想如果有debugtwice
可能会更好。”