15 Basic: debug

写代码时,只要出现意料之外的错误(如令人心惊肉跳的Error in xxx,或结果与预期不符),就称之为 bug。相应的,调试或者排除错误的过程就称之为 debug。写代码的功力体现在能否写出高效、简洁且正确的代码,而要实现这个目标就必须具备扎实的 debug 能力。本节的目标就是提升 debug 的能力。

通常 bug 是非常难对付的,因为简单的错误不容易犯,而一旦有 bug,一般都比较棘手。所以,写代码就是一个不断和 bug 作斗争的过程,可以说每一个码农的成长史就是一部 debug 的“血泪史”。

Finding your bug is a process of confirming the many things that you believe are true — until you find one which is not true.

— Norm Matloff

Reference:

  1. Debugging with the RStudio IDE
  2. Debugging in Advanced R 2nd edition

15.1 Two types of bug: obvious one and invisible one

  1. 显性(相对简单)

所有会中止当前执行的代码并提供Error提示的 bug 都属于显性 bug。

  1. 隐性(相对困难)

所有不影响代码正常执行但会导致结果不对的 bug 都属于隐性 bug。例如:

# 求累计和
a <- c(1, 2, 3)
for (i in 1:length(a)) {
  if (i > 1) b <- a[i] + a[i - 1]
}
b
[1] 5

要想发现隐性 bug,前提是必须对代码的执行结果有清晰的预期,否则连代码有 bug 都无法觉察,更不用提如何 debug 了。例如,上述例子中就需要用户能够清楚地知道执行结果是1+2+3=6,所以b5的执行结果显然是不符合预期的,故推断出来有隐性 bug。

15.2 Three steps of debugging

通常 debug 可以包含以下三步:

  1. 运行代码
  2. 在 bug 的前一行停下来
  3. 找到问题的根源

其中,2、3 步是 debug 的核心技巧,也常常一起运用。

此外,在 debug 前,切记先把代码保存至本地。Rstudio 的 debug 模式对于新建但未保存的代码文件(untitled)并不友好。

15.2.1 Step 1: Source your R script

Source (Ctrl+Shift+S):读取并执行当前活动文档中的所有代码。

相比 run current line or selection (Ctrl+Enter),Source 才是 debug 的正确姿势。因为 Run current line or selection 有 3 个缺点:

  1. Run current line or selection 会在 console 输出执行了什么代码以及对应的结果(如果有的话),容易造成 console 信息冗杂,并且容易搞错代码执行顺序;

信息冗杂:

搞错代码执行顺序:

使用 Source 完美避免:

  1. 只有使用 Source,遇到Error才会触发 Rstudio 的自动呈现trackback()信息功能(详见15.3.1.2);
  2. 只有使用 Source 进入 debug 模式才会激活 Rstudio 的 debug toolbar(详见15.4)。

所以,在 debug 时,第一步就是 Source。

15.2.2 Step 2: stop the code just before the occurrence of bug and step 3: locate the malady

当代码中存在 bug 时,不论是显性 bug 还是隐性 bug,debug 时都需要在出错的地方停下来。更准确地说,应该是让 R 运行完 bug 所在行代码的上一行就要停下来,这样就可以查看在 bug 发生的前一刻到底发生了什么,才能最终找到问题的根源。

根据 bug 的位置不同可以将 bug 分成两种情况:

  • bug 既不在 function 内部也不在 control flow 里:
  • bug 在 function 内部或 control flow 里。

之所以要区分这两种情况,是因为 R 在执行时会将代码中所有调用的 function 和 control flow 视作是整体来运行。一旦在这些部分部存在 bug,是无法在这些部分执行的过程中停下来,也就无法查看到底是什么引发了 bug。

15.3 When bug is neither in function nor in control flow

15.3.1 Obvious bug

15.3.1.1 Step 2 of de-obvious-bug outside function and control flow

在使用 Source 的方式运行代码后(debug 的第一步),如果是显性 bug,R 会自动终止运行的代码,相当于自动完成了 debug 的第二步—— bug 所在行因为有 bug 所以没有运行成功,而此行之前的所有代码都顺利运行了,本质上就是在 bug 发生前夕停了下来。就只剩下第三步了——找到问题的根源。

15.3.1.2 Step 3 of de-obvious-bug outside a function and control flow

  1. 定位 bug

虽然第二步已经自动完成了,但要执行第三步的前提是要知道 bug 到底在哪一行代码里,此时需要结合Error的信息定位 bug。有两种基本方式:

  • 目测 + 搜索。当Error出在自己写的代码中,幸运的情况下,可以根据Error的提示,轻松找出 bug 的位置,但有时则需要使用 Ctrl+F 唤出代码编辑窗口内的搜索框,然后搜索Error标出的问题代码,来最终定位触发 bug 的代码所在行;
  • traceback()。使用 Source 的方式运行代码时,Rstudio 会自动捕捉Error并呈现traceback()的结果,里面可能会有关于 bug 在哪里的信息。traceback()会在 console 展示引发最近一次Error的 functions 序列(默认倒序),即Error所在行的代码运行过程中使用到的所有 functions,第一个使用的 function 出现在最下面,引发Error的 function 在最上面;

关于traceback的几点细节:

  • 如果你的 Rstudio 在遇到Error时没有自动显示traceback()的信息,请按照下图调整设置:
  • 不是所有的Error都会触发 Rstudio 的自动显示traceback()功能,例如基本语法错误;

但只要是保存在本地的代码文件中有基本语法错误,Rstudio 会在代码编辑窗口有明确的提示,所以不用担心。

  • traceback()会将运行代码直到Error出现的所有中间过程都记录下来,包括 R 在执行代码时的一些内部函数(internal function),所以查看的时候只需要关注包含自己代码的部分即可(进入 debug 模式后 Rstudio 提供的 traceback 信息会自动略掉 internal functions);
  • traceback()给出的信息可能不会那么直接;
  • Run current line or selection 不会自动触发traceback()

如果Errortraceback()提供的信息不够清晰,无法定位 bug 所在行,那么可以采用以下两种形式:

  • 运用分半代码并运行的方式不断缩小范围直到定位 bug 所在行;
scores <- read.fwf(
  file = "F:/Nutstore backup/R/codes/RBA/data/6.1 final data.txt", 
  skip = 5, 
  width = c(3, 14, 5, 9, 9, 10, 11),
  col.names = c(
    "name", "date_of_birth", "class", 
    "score_last", "score_current", 
    "rank_last", "rank_current"
  ), 
  fileEncoding = "UTF-8"
)
head(scores)
rank_change <- scores$rank_currant - scores$rank_last
scores <- cbind(scores, rank_change)
scores <- cbind(
  scores, 
  c_last = scores$score_last - mean(scores$score_last), 
  c_current = scores$score_current - mean(scores$score_current)
)
head(scores)
test1 <- data.frame(
  name = scores$name, 
  date_of_birth = scores$date_of_birth, 
  class = scores$class, 
  score_last = scores$score_last, 
  rank_last = scores$rank_last, 
  c_last = scores$c_last
)
head(test1)

上述代码 Source 后的结果如下:

Error并未直接指向代码中具体哪一行。可以将上述 20 行代码分为前 10 行和后 10 行,先运行前 10 行,如果报错就说明 bug 在前 10 行,反之则 bug 在后 10 行。然后再把报错的这一半再次切半并按顺序运行。不断重复下去就一定能定位到 bug 所在行。

  • 进入 debug 模式使用 Rstudio 提供的“Step into the current function call”按钮(详见15.4.1.1)。
  1. 定位 bug 的原因(关键

当将 bug 定位到具体某一行(称之为起点行)后,就需要确定引发 bug 的根源。知道为什么会有 bug,才能想出解决 bug 的方案。这一步的核心操作是break it down into small parts,大体可分为以下两个子步骤:

  1. 分别检查起点行中所有可以查看的独立部分,并确定到底是哪一个独立部分中的哪一个 object 引发了bug
  2. 如果问题的根源并不在该 object 里,则需要以倒序的方式,检查涉及到该 object 的所有前序代码(同样也是分别检查所有可以查看的独立部分),看问题到底出在哪。

15.3.2 Invisible bug

相比于会自动实现第二步的显性 bug,隐性 bug 无法通过 Source 自动停止,所以需要用户先自行感知到代码里有隐性 bug,再尝试定位 bug。这个过程中使用的技术还是和显性 bug 一样,只不过会更难。

15.4 When bug is in either function or control flow

当 bug 在 function 内部或 control flow 里时,只有进入 debug 模式才能够将被视作是整体来执行的 function 或 control flow 暂停下来。此时不管是显性 bug 还是隐性 bug,暂停代码的方式都是使用 debug 模式,其余操作都一样,只不过隐性 bug 会更难处理,所以本小节只讲隐性 bug。

15.4.1 Invisible bug

15.4.1.1 Step 2 of de-invisible-bug inside function or control flow

不同于显性 bug,隐性 bug 无法通过traceback()来定位,因为它并不会报Error

容易看出,隐性 bug 是不会自己在出错的地方停下来,需要用户自己来决定在哪里停下来,而知道在哪里停下来其实也就是寻找问题根源的过程,所以针对隐性 bug,debug 的第二步和第三步并没有特别清晰的界限。

对于隐性 bug 而言,debug 的第二步是通过进入 debug 模式来实现的,方式是在代码中键入browser(),保存并 Source 即可进入 debug 模式。进入 debug 模式后,代码编辑窗口显示如下:

其中绿色箭头和高亮表示 R 停在了这一行,这一行还未执行(browser()行除外,因为使用browser()进入 debug 模式时,意味着browser()这一行已经被执行了)。

Environment 窗口显示如下:

会展示当前的 environment 及已经运行并产生的所有 objects。除此之外,还展示了trackback()的信息,并折叠了 internal functions。

Console 窗口显示如下:

多了两样东西:

  • Browse[1]提示:

看到这个提示就知道此时已经进入了 debug 模式。

  • 一排可以交互的 tool bar,共五个按钮:
  • (F10 或 n+ Enter) Next

  • (Shift + F4 或 s+ Enter) Step into the current function call

  • (Shift + F7 或 f+ Enter) Execute remainder of the current function or loop

  • (Shift + F5 或 c+ Enter) Continue execution until the next breakpoint is encountered

  • (Shift + F8 或 Q+ Enter) Exit debug mode

请注意,以上交互按钮只有使用 Source 时才会出现!

nextcontinue function or loopcontinue的演示:

step in的演示:

如果完全不知道 bug 的精确或粗略位置,在实施 debug 的第二步时可以直接把browser()放在代码首行;如果对于 bug 的位置有初步的判断,在实施 debug 的第二步时就可以把browser()放在该位置之前,这样进入 debug 模式的时候,自然就是在 bug 发生前夕停下来。

15.4.1.2 Step 3 of de-invisible-bug inside function or control flow

Debug 模式实际上就是用来暂停 function 或 control flow 的一种手段。进入 debug 模式以后,才能实施 debug 的第三步并解决 function 或 control flow 内部的隐性 bug。例如:

除了手动在代码中插入browser()之外,Rstudio 还提供 breakpoint 功能,在代码编辑窗口的行数左侧单击即可。但不推荐这么用,因为在某些场景下,breakpoint 会失效,并不如browser()稳定,而且更重要的是,不如browser()灵活(详见15.5)。

15.5 browser() tips

15.5.1 Always place browser() inside a function

如果browser()不在 function 内部,进入 debug 模式后,next(F10)会直接把browser()后续的所有非 control flow 的代码全部运行完:

next(F10)只对 control flow 起作用:

但如果browser()在 function 内部,调用该 function 时才会进入 debug 模式,此时就会可以一行一行地运行该 function 内部所有位于browser()以下的代码。所以,通过browser()进入 debug 模式来暂停代码的方式,其设计的初衷就是为了方便用户去解决存在于 function 或 control flow 中的 bug。除此之外,所有既不在 function 又不在 control flow 里的 bug,用户都可以通过手动逐行运行代码的方式实现对代码的绝对控制,完全可以精确地在 bug 出现前夕停下来,也就不需要进入 debug 模式了。换言之,browser()是 define function 时 debug 的利器。

15.5.2 Conditional browser()

browser()本身是一个 function,所以可以和print()cat()以及if结构搭配使用,实现在指定条件下触发 debug 模式的效果。

x <- matrix(runif(50), 10, 5)
n <- 0
for (i in 1:nrow(x)) {
  for (j in 1:ncol(x)) {
    if (x[j, i] > 0.95) 
      n <- n + 1
  }
}
Error in x[j, i]: subscript out of bounds

15.6 View() tips

debug 的第三步中break it down into small parts,即将某行代码分解成更小的独立部分然后分别查看,是整个 debug 过程的核心操作。查看方式有两种:

  1. 将这些更小部分的代码复制到 console 并执行,适合查看执行结果较为简短的代码;
  2. 通过View()在代码编辑窗口查看,适合查看执行结果较庞杂的代码。

所有能正常执行的代码都可以通过View()查看,所以相比于在 console 中查看,View()是更加普适的方法。

View()各类 structure types:

  • matrix 和 data.frame

View()可以非常方便地查看 matrix、data.frame。

  • vector

View()无法便利地查看 vector ,长 vector 会被折叠且无法展开,如果需要像 matrix 或 data.frame 一样查看 vector,可以使用View(as.matrix())的组合。

  • array

View()也无法便利地查看维数超过 2 的 array,需要划分成可单独查看的矩阵。

  • list

View()只能查看 list 的结构,需要根据具体情况使用取子集的方式查看元素。

15.7 Frequently made mistakes

  1. 写多余的代码;
a <- 1
b <- a + 1
a <- 1
d <- a + b
  1. 拼写错误;

忘记区分大小写

a <- C(1, 2, 3)
Error in `contrasts<-`(`*tmp*`, how.many, value = contr): contrasts can be applied only to factors with 2 or more levels

Function 名拼错

b <- maen(c(1, 2, 3))
Error in maen(c(1, 2, 3)): could not find function "maen"
  1. 忘记基本语法规范;

在需要使用 character object 时,没有用""

read.table(
  "F:/Nutstore backup/R/codes/RBA/data/txt_example.txt", 
  col.names = c(studentA, studentB, studentC)
)
Error: object 'studentA' not found

在调用 function 时,function 的名字后误用[],而不是()

a <- c[1, 2, 3]
Error in c[1, 2, 3]: object of type 'builtin' is not subsettable

在调用 function 时,function 的输入参数是 vector,但没有用,隔开

read.table(
  "F:/Nutstore backup/R/codes/RBA/data/txt_example.txt", 
  col.names = c("x" "y" "z")
)
Error in parse(text = input): <text>:3:21: unexpected string constant
2:   "F:/Nutstore backup/R/codes/RBA/data/txt_example.txt", 
3:   col.names = c("x" "y"
                       ^

在调用 function 时,function 的某个 argument 是 vector,但没有用c()

read.table(
  "F:/Nutstore backup/R/codes/RBA/data/txt_example.txt", 
  col.names = "x", "y", "z"
)
Error in !header: invalid argument type

在调用 function 时,使用了 function 中没有的 argument

read.table(
  "F:/Nutstore backup/R/codes/RBA/data/txt_example.txt", 
  width = c(2, 2)
)
Error in read.table("F:/Nutstore backup/R/codes/RBA/data/txt_example.txt", : unused argument (width = c(2, 2))

在 subsetting 时用了(),而不是[]

a(1) <- 2
Error in a(1) <- 2: target of assignment expands to non-language object
  1. 其他错误。

在没有 names 相关属性时使用 names 的信息 subsetting,或有 names 相关属性时 names 信息拼写错误

# 以 list object 为例
# 没有 names
l1 <- list(
  c(1, 2, 3), 
  c(4, 5, 6)
)
mean(l1$x)
Warning in mean.default(l1$x): argument is not numeric or
logical: returning NA
[1] NA
# 有 names
l1 <- list(
  x = c(1, 2, 3), 
  y = c(4, 5, 6)
)
mean(l1$X)
Warning in mean.default(l1$X): argument is not numeric or
logical: returning NA
[1] NA

总结一下,初学容易碰到的错误提示的意思及可能的原因如下表:

错误提示 意思 可能的原因
Error: unexpected xxx in "xxx" 在执行某代码片段时碰到了意外的符号 使用了非法的变量名,使用了中文逗号/括号,使用控制流没有采用规定的语法格式
Error: object 'xxx' not found
Error in xxx() : could not find function "xxx"
在 global Environment 中找不到某 object,在加载的所有 package 中找不到某 function 没有创建该 object/function,或者该 object/function 的 name 拼写错误,或者没有加载或安装该 function 所属的 package
Error in xxx : subscript out of bounds 下标越界,在根据位置信息 subsetting 时,使用了范围之外的位置 可能的原因较多
Error in file(file, "rt") : cannot open the connection
In addition: Warning message:
In file(file, "rt") : cannot open file 'xxx':
No such file or directory
无法打开某文件,没有该文件或文件路径 文件路径有拼写错误,或使用的是相对路径,但 working directory 发生了改变,而文件路径没有相应变化

高手代码鉴赏:抛硬币

可以使用 debug 模式查看为什么一楼的方案是正确的。

15.8 Recap

  1. debug 一律用 Source;
  2. 进入 debug 模式一律用browser(),请勿使用 Return with Debug;
  3. 隐性 bug 如果无法准确定位,可能需要从首行代码开始逐行排除;
  4. debug 最重要的技巧是在定位引发 bug 的代码后将其拆分成独立的部分,分别查看;
  5. debug 完毕记得删除所有以 debug 为目的插入的代码。