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:
15.1 Two types of bug: obvious one and invisible one
- 显性(相对简单)
所有会中止当前执行的代码并提供Error
提示的 bug 都属于显性 bug。
- 隐性(相对困难)
所有不影响代码正常执行但会导致结果不对的 bug 都属于隐性 bug。例如:
要想发现隐性 bug,前提是必须对代码的执行结果有清晰的预期,否则连代码有 bug 都无法觉察,更不用提如何 debug 了。例如,上述例子中就需要用户能够清楚地知道执行结果是1+2+3=6
,所以b
为5
的执行结果显然是不符合预期的,故推断出来有隐性 bug。
15.2 Three steps of debugging
通常 debug 可以包含以下三步:
- 运行代码
- 在 bug 的前一行停下来
- 找到问题的根源
其中,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 个缺点:
- Run current line or selection 会在 console 输出执行了什么代码以及对应的结果(如果有的话),容易造成 console 信息冗杂,并且容易搞错代码执行顺序;
信息冗杂:
搞错代码执行顺序:
使用 Source 完美避免:
- 只有使用 Source,遇到
Error
才会触发 Rstudio 的自动呈现trackback()
信息功能(详见15.3.1.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
- 定位 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()
。
如果Error
和traceback()
提供的信息不够清晰,无法定位 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)。
- 定位 bug 的原因(关键)
当将 bug 定位到具体某一行(称之为起点行)后,就需要确定引发 bug 的根源。知道为什么会有 bug,才能想出解决 bug 的方案。这一步的核心操作是break it down into small parts,大体可分为以下两个子步骤:
- 分别检查起点行中所有可以查看的独立部分,并确定到底是哪一个独立部分中的哪一个 object 引发了
bug
; - 如果问题的根源并不在该 object 里,则需要以倒序的方式,检查涉及到该 object 的所有前序代码(同样也是分别检查所有可以查看的独立部分),看问题到底出在哪。
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 时才会出现!
的演示:
如果完全不知道 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.6 View()
tips
debug 的第三步中break it down into small parts,即将某行代码分解成更小的独立部分然后分别查看,是整个 debug 过程的核心操作。查看方式有两种:
- 将这些更小部分的代码复制到 console 并执行,适合查看执行结果较为简短的代码;
- 通过
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
- 写多余的代码;
a <- 1
b <- a + 1
a <- 1
d <- a + b
- 拼写错误;
忘记区分大小写
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 名拼错
- 忘记基本语法规范;
在需要使用 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 的名字后误用[]
,而不是()
在调用 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 时用了()
,而不是[]
- 其他错误。
在没有 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
- debug 一律用 Source;
- 进入 debug 模式一律用
browser()
,请勿使用 Return with Debug; - 隐性 bug 如果无法准确定位,可能需要从首行代码开始逐行排除;
- debug 最重要的技巧是在定位引发 bug 的代码后将其拆分成独立的部分,分别查看;
- debug 完毕记得删除所有以 debug 为目的插入的代码。