1. 基本调试策略

作者总结了程序给出不能期望结果的三种情况:

  1. 程序报错, 需要找到出错的地方加以纠正;
  2. 程序正常运行, 输出了结果, 但是结果明显有错;
  3. 最糟糕的是, 程序结果也看起来很正常, 但实际结果是错误的。

模块化思维与编程

为了尽可能保证程序结果正确, 在自己编写新算法时, 要运用模块化思想, 将问题分解为若干个小问题, 使得每一个小问题都比较容易验证结果是否正确, 将每一个小问题编写成一个单独的函数, 这样也可以避免一段很长的程序中许多变量混杂在一起。

在划分模块并编写好程序后, 应该编写一系列的测试函数, 对每个函数进行测试, 保证其在各种情况下的结果是正确的。 最好采纳R的规则化的测试策略进行自动化测试, 在编写R扩展包时就推荐同时提供测试代码。

学会求助社区

如果程序还是有错误, 首先可以求助于搜索引擎、用户社区等。 如果这个问题是常见问题, 往往这样就可以解决问题。
比如 google, github 等。

将问题最小化

如果问题没有解决, 需要将问题最小化: 减少引起错误的程序的复杂程度, 将不必要的代码尽可能用固定的输入数据代替, 使得出错程序很短, 而且错误可重复。 有时会有不可重复的错误, 这样的错误往往很难解决, 超出了一般R用户的能力。

在将问题程序简化并且错误可重复以后, 就要试图定位错误。 一般编程语言都有如下的一些一般性查错(debugging)方法:

在程序中适当位置加上输出命令(语句), 输出怀疑有错的变量值。
某些变成语言提供了跟踪以及条件跟踪命令, 可以在程序运行到某个语句或者触发了某个条件时程序中止, 但允许用户控制逐行运行程序并随时查看变量值, 称为跟踪调试(tracing)。 跟踪调试可以是命令行工具, 也可以是集成在RStudio这样的集成编程环境中的图形界面工具。
在查错时, 科学研究思维照样起作用: 根据错误表现提出可能的原因假设, 制作测试输入数据验证假设, 记录相应输出并分析是否与假设的错误原因吻合。 如此反复直到找到出错原因并加以纠正。

查错并纠正就可能会破坏了成熟的代码, 造成新的错误, 所以最好能有自动化的测试策略, 在每次修改程序后都重新测试程序是否保持输出正确。

测试正确与稳定性

自定义函数应该用各种不同输入测试其正确性和稳定性。

2. 找到出错的函数

不同于python,在程序运行后,R 默认下并不会直接调用traceback() 函数,而只是告知一开始引起出错(比如最开始引起出错的那层函数)的内容:

> fun_2 = function(x) x*2
> fun_1 = function(x) fun_2()
> fun_1()
Error in fun_2() : 缺少参数"x",也没有缺省值

为了在多层次函数调用中找到出错的函数,我们可以使用traceback 函数,结果是所谓的反向追踪(traceback), 一般编程语言中称为调用栈(calling stack)。 这个输出是从下向上堆叠显示, 下层是调用方, 上层是被调用方。

> traceback()
2: fun_2() at #1
1: fun_1()

如果是使用Rstudio,出错程序的右端可以显示Show Traceback以及Rerun with Debug快捷图标, 点击“Show Traceback”图标也可以显示反向追踪结果。
04. 程序调试 - 图1

3. 跟踪调试

参见:https://blog.csdn.net/xiaohukun/article/details/76659515

默认下,R 的debug 模式为error inspector,即上图的追踪效果。点击Rerun with Debug快捷图标,就可以进入R 的browser() 沙盘模式。

在该模式下,这个命令行的环境一般不再是全局环境,而是出现报错的函数的运行环境:
04. 程序调试 - 图2

除此之外,函数定义一般都包含多行,所以一般不在命令行定义函数, 而是把函数定义以及较长的程序写在源程序文件中, 用source命令运行。 用source命令调入运行的程序与在命令行运行的效果基本相同, 这样定义的变量也是全局变量。

在RStudio的编辑窗口中打开.R源程序文件, 在某一程序行行号左端的空白处用鼠标单击, 就可以设定某一行为断点, 在用source命令运行到该处时就可以进入跟踪调试状态。
04. 程序调试 - 图3
R中的断点和绝大多数IDE中相似,都是在代码行号的左侧用鼠标单击一下出现一个红点,在R中即代表在该行代码前加了browser()函数。如果是空心红点则需要运行脚本面板右上方的Source按钮运行脚本,空心红点即会变为实心。当然如果你选中了Source on Save选项,那么每次保存时都会自动将文件source。

进入调试状态后, RStudio界面提供了相应的支持。 这时RStudio的命令行窗格(Console)将会显示用于控制运行的图标, 包括执行下一语句(Next)、跟踪进入要调用的函数运行(Step into)、执行到函数末尾或者循环末尾(Finish)、不再跟踪继续正常运行(Continue)、终止运行(Stop)。

04. 程序调试 - 图4

另外这些功能也可以通过调试命令行命令实现:

输入变量名查看变量值;
用n命令或者换行键逐句运行;
用s命令跟踪进调用的函数内部逐句运行;
用f命令快速执行到循环末尾或者函数末尾;
用c命令恢复正常运行,不再跟踪;
用Q命令强行终止程序运行。

比如我们在函数中插入了browser 进行调试:

f <- function(x){
  browser()
  for(i in 1:n){
    s <- s + x[i]
  }
}

发现是出现了未定义的变量:

> print(f(1:5))
Called from: f(1:5)
Browse[1]> n
debug在#3: for (i in 1:n) {
    s <- s + x[i]
}
Browse[2]> n
Error in f(1:5) : 找不到对象'n'

在源文件中把出错行改为for(i in 1:length(x)), 再次运行, 发现在运行s <- s + x[i]行时, 遇到“错误: 找不到对象’s’”。 这是忘记初始化引起的。 在for语句前添加s <- 0语句,函数定义变成:

f <- function(x){
  browser()
  s <- 0
  for(i in 1:length(x)){
    s <- s + x[i]
  }
}

再次运行, 在跟踪到循环时, 为了避免繁琐的跟踪过程, 可以用“执行到函数末尾或者循环末尾”快捷图标或命令行的f命令, 或者“Continue”快捷图标或命令行的c命令。 程序不显示错误但是也没有显示结果为NULL而不是我们期望得输入元素之和。 检查可以看出错误是忘记把函数返回值写在函数定义最后。

在调试完毕后,函数不需要修改后, 可以把对browser()的调用删除或注释掉, 或在RStudio 中关闭断点。

另外,我们还可以修改R studio 模式为Break in Code:
04. 程序调试 - 图5
达到遇到错误就直接在其位置进入调试模式的效果。

ps:利用沙盘模式,我们也可以进行简单的调试,比如在函数中使用browser 命令,则我们进入的沙盘环境便是定义的这个函数内部,其中创建的任何变量并不会在函数外部生效。

对函数调试

debug函数用于在一个已存在的的函数的起始处“添加”一个browser()语句。此时,函数处于“调试模式”,每次只要一运行该函数都会进入浏览器模式。需要通过undebug函数将browser() 从该函数中移出,isdebugged 函数用于检查某个函数是否处于“调试”模式。

> test<-function(){
+   print("hello")
+ }
+ 
#进入"调试"模式
> debug(test)
> isdebugged(test)
[1] TRUE

> test()
debugging in: test()
debug at #1: {
    print("hello")
}
Browse[2]> Q

#退出调试模式
> undebug(test)
> isdebugged(test)
[1] FALSE
> test()
[1] "hello"
————————————————
版权声明:本文为CSDN博主「hukun1995」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiaohukun/article/details/76659515

出错调试选项

可以将recover()函数放在任何你需要的地方,一旦R运行到recover函数时,它会暂停并显示当前的调用栈信息,而且你可以选择进入哪一个函数进入调试模式:

> f <- function(x){
+   recover()
+   for(i in 1:length(x)){
+     s <- s + x[i]
+   }
+ }
> f(2)

Enter a frame number, or 0 to exit   

1: f(2)

比较长的程序在调试时如果从开头就跟踪, 比较耗时。可以设置成出错后自动进入跟踪模式, 检查出错时的变量值。只要进行如下设置,可以将recover添加到R函数的全局选项中
options(error=recover)

循环断点

用browser()函数与R的if结构配合可以制作条件断点。 比如, 在调试带有循环的程序时, 发现错误发生在循环内, 如果从循环开始就跟踪运行, 会浪费许多时间。 设已知错误发生在循环变量i等于501的时候, 就可以在循环内插入:

if(i == 501) browser()

这样就可以在更接近出错的位置进入跟踪运行。

4. stop()、warning()、message()

编写程序时应尽可能提前发现不合法的输入和错误的状态。 发现错误时, 可以用stop(s)使程序运行出错停止, 其中s是一个字符型对象, 用来作为显示的出错信息。

发现某些问题后如果不严重, 可以不停止程序运行, 但用warning(s)提交一个警告信息, 其中s是字符型的警告信息。 警告信息的显示可能比实际运行要延迟一些。

> if (x < 5) warning("x 太小了!")
Warning message:
x 太小了!

另外,也可以使用message(),与cat()等相比较, cat()是用户要求的输出, 而message()是程序员对用户的提示,它不算是错误或者警告, 是提示性的信息输出。 message()不会像warning()那样延迟显示。

有些警告信息实际是错误, 用options()的warn参数可以设置警告级别, 如设置warn=2则所有警告当作错误处理。 设置如:

options(warn = 2)

5. 预防性设计

捕获异常

tryCatch(需要捕捉的表达式, error = 错误时触发的函数式, finally = 最终一定要运行的函数式)

比如:

result = tryCatch(
        {expr}, 
        warning = function(w) {warning-handler-code}, 
        error = function(e) { error-handler-code}, 
        finally = {cleanup-code}
        )

自定义异常

类似py 中的raise 语句,R 也提供了相关的语句:

f <- function(x, y){
  stopifnot(is.numeric(x),
            is.numeric(y),
            length(x)==length(y))
  ## 函数体程序语句...
}

04. 程序调试 - 图6

函数stopifnot 可以指定自变量的若干个条件, 当自变量不符合条件时自动出错停止。