-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path02-basic-15-debug.Rmd
More file actions
432 lines (276 loc) · 21 KB
/
Copy path02-basic-15-debug.Rmd
File metadata and controls
432 lines (276 loc) · 21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# Basic: debug {#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](https://support.posit.co/hc/en-us/articles/205612627-Debugging-with-RStudio)
2. [Debugging in Advanced R 2nd edition](https://adv-r.hadley.nz/debugging.html)
## Two types of bug: obvious one and invisible one
1. 显性(相对简单)
所有会中止当前执行的代码并提供`Error`提示的 bug 都属于显性 bug。
2. 隐性(相对困难)
所有不影响代码正常执行但会导致结果不对的 bug 都属于隐性 bug。例如:
```{r comment = ""}
# 求累计和
a <- c(1, 2, 3)
for (i in 1:length(a)) {
if (i > 1) b <- a[i] + a[i - 1]
}
b
```
要想发现隐性 bug,前提是必须对代码的执行结果有清晰的预期,否则连代码有 bug 都无法觉察,更不用提如何 debug 了。例如,上述例子中就需要用户能够清楚地知道执行结果是`1+2+3=6`,所以`b`为`5`的执行结果显然是不符合预期的,故推断出来有隐性 bug。
## Three steps of debugging
通常 debug 可以包含以下三步:
1. 运行代码
2. 在 bug 的前一行停下来
3. 找到问题的根源
其中,2、3 步是 debug 的核心技巧,也常常一起运用。
此外,在 debug 前,**切记先把代码保存至本地**。Rstudio 的 debug 模式对于新建但未保存的代码文件(untitled)并不友好。
### 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 信息冗杂,并且容易搞错代码执行顺序;
信息冗杂:
<video src="images/debug run current line console contamination.mp4" width="100%" controls="controls"></video>
搞错代码执行顺序:
<video src="images/debug run current line wrong sequence.mp4" width="100%" controls="controls"></video>
使用 Source 完美避免:
<video src="images/debug source.mp4" width="100%" controls="controls"></video>
2. 只有使用 Source,遇到`Error`才会触发 Rstudio 的自动呈现`trackback()`信息功能(详见\@ref(Step-3-of-de-obvious-bug-outside-function-and-control-flow));
3. 只有使用 Source 进入 debug 模式才会激活 Rstudio 的 debug toolbar(详见\@ref(When-bug-is-in-either-function-or-control-flow))。
所以,在 debug 时,第一步就是 Source。
### 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。
## When bug is neither in function nor in control flow
### Obvious bug
#### Step 2 of de-obvious-bug outside function and control flow
在使用 Source 的方式运行代码后(debug 的第一步),如果是显性 bug,R 会自动终止运行的代码,相当于自动完成了 debug 的第二步—— bug 所在行因为有 bug 所以没有运行成功,而此行之前的所有代码都顺利运行了,本质上就是在 bug 发生前夕停了下来。就只剩下第三步了——找到问题的根源。
#### Step 3 of de-obvious-bug outside a function and control flow{#Step-3-of-de-obvious-bug-outside-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 在最上面;
<video src="images/debug traceback.mp4" width="100%" controls="controls"></video>
**关于`traceback`的几点细节:**
- 如果你的 Rstudio 在遇到`Error`时没有自动显示`traceback()`的信息,请按照下图调整设置:
<img style = "display: block; margin: auto;" src="images/Rstudio debug setting 1.png" width="70%"/>
- 不是所有的`Error`都会触发 Rstudio 的自动显示`traceback()`功能,例如基本语法错误;
<img style = "display: block; margin: auto;" src="images/debug traceback no auto display.png" width="70%"/>
但只要是保存在本地的代码文件中有基本语法错误,Rstudio 会在代码编辑窗口有明确的提示,所以不用担心。
<img style = "display: block; margin: auto;" src="images/debug syntactic error.png" width="100%"/>
- `traceback()`会将运行代码直到`Error`出现的所有中间过程都记录下来,包括 R 在执行代码时的一些内部函数(internal function),所以查看的时候只需要关注包含自己代码的部分即可(进入 debug 模式后 Rstudio 提供的 traceback 信息会自动略掉 internal functions);
<img style = "display: block; margin: auto;" src="images/debug traceback function stack own code.png" width="100%"/>
- `traceback()`给出的信息可能不会那么直接;
<img style = "display: block; margin: auto;" src="images/debug traceback failure.png" width="100%"/>
- Run current line or selection 不会自动触发`traceback()`。
<img style = "display: block; margin: auto;" src="images/debug run current line no auto-traceback.png" width="100%"/>
如果`Error`和`traceback()`提供的信息不够清晰,无法定位 bug 所在行,那么可以采用以下两种形式:
- 运用分半代码并运行的方式不断缩小范围直到定位 bug 所在行;
```{r split_code_into_chunks, eval = FALSE, error = TRUE}
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 后的结果如下:
<img style = "display: block; margin: auto;" src="images/debug split code into chunks.png" alt="" width="80%"/>
`Error`并未直接指向代码中具体哪一行。可以将上述 20 行代码分为前 10 行和后 10 行,先运行前 10 行,如果报错就说明 bug 在前 10 行,反之则 bug 在后 10 行。然后再把报错的这一半再次切半并按顺序运行。不断重复下去就一定能定位到 bug 所在行。
- 进入 debug 模式使用 Rstudio 提供的“Step into the current function call”按钮(详见\@ref(Step-2-of-de-invisible-bug-inside-function-or-control-flow))。
2. 定位 bug 的原因(**关键**)
当将 bug 定位到具体某一行(称之为**起点行**)后,就需要确定引发 bug 的根源。知道为什么会有 bug,才能想出解决 bug 的方案。这一步的核心操作是**break it down into small parts**,大体可分为以下两个子步骤:
a. 分别检查**起点行**中所有可以查看的独立部分,并确定到底是哪一个独立部分中的哪一个 object 引发了`bug`;
b. 如果问题的根源并不在该 object 里,则需要以倒序的方式,检查涉及到该 object 的所有前序代码(同样也是分别检查所有可以查看的独立部分),看问题到底出在哪。
<video src="images/debug solve bug.mp4" width="100%" controls="controls"></video>
### Invisible bug
相比于会自动实现第二步的显性 bug,隐性 bug 无法通过 Source 自动停止,所以需要用户先自行感知到代码里有隐性 bug,再尝试定位 bug。这个过程中使用的技术还是和显性 bug 一样,只不过会更难。
<video src="images/debug implicit bug.mp4" width="100%" controls="controls"></video>
## When bug is in either function or control flow{#When-bug-is-in-either-function-or-control-flow}
当 bug 在 function 内部或 control flow 里时,只有进入 debug 模式才能够将被视作是整体来执行的 function 或 control flow 暂停下来。此时不管是显性 bug 还是隐性 bug,暂停代码的方式都是使用 debug 模式,其余操作都一样,只不过隐性 bug 会更难处理,所以本小节只讲隐性 bug。
### Invisible bug
#### Step 2 of de-invisible-bug inside function or control flow{#Step-2-of-de-invisible-bug-inside-function-or-control-flow}
不同于显性 bug,隐性 bug 无法通过`traceback()`来定位,因为它并不会报`Error`。
<img style = "display: block; margin: auto;" src="images/debug implicit bug.png" width="100%"/>
容易看出,隐性 bug 是不会自己在出错的地方停下来,需要用户自己来决定在哪里停下来,而知道在哪里停下来其实也就是寻找问题根源的过程,所以针对隐性 bug,debug 的第二步和第三步并没有特别清晰的界限。
对于隐性 bug 而言,debug 的第二步是通过进入 debug 模式来实现的,方式是在代码中键入`browser()`,保存并 Source 即可进入 debug 模式。进入 debug 模式后,代码编辑窗口显示如下:
<img style = "display: block; margin: auto;" src="images/debug browser 1.png" width="100%"/>
其中绿色箭头和高亮表示 R 停在了这一行,这一行还未执行(`browser()`行除外,因为使用`browser()`进入 debug 模式时,意味着`browser()`这一行已经被执行了)。
Environment 窗口显示如下:
<img style = "display: block; margin: auto;" src="images/debug rstudio traceback panel.png" width="100%"/>
会展示当前的 environment 及已经运行并产生的所有 objects。除此之外,还展示了`trackback()`的信息,并折叠了 internal functions。
Console 窗口显示如下:
<img style = "display: block; margin: auto;" src="images/debug browser 2.png" width="100%"/>
多了两样东西:
- `Browse[1]`提示:<img style = "display: block; margin: auto;" src="images/debug console browe sign.png" width="20%"/>
看到这个提示就知道此时已经进入了 debug 模式。
- 一排可以交互的 tool bar,共五个按钮:<img style = "display: block; margin: auto;" src="images/debug 5 buttons.png" width="50%"/>
- (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 时才会出现!**
<img src="images/debug icon next.png" alt="next" height="25"/><img src="images/debug icon cf.png" alt="continue function or loop" height="25"/><img src="images/debug icon c.png" alt="continue" height="25"/>的演示:
<video src="images/debug buttons 1.mp4" width="100%" controls="controls"></video>
<img src="images/debug icon step in.png" alt="step in" height="30"/>的演示:
<video src="images/debug buttons 2.mp4" width="100%" controls="controls"></video>
如果完全不知道 bug 的精确或粗略位置,在实施 debug 的第二步时可以直接把`browser()`放在代码首行;如果对于 bug 的位置有初步的判断,在实施 debug 的第二步时就可以把`browser()`放在该位置之前,这样进入 debug 模式的时候,自然就是在 bug 发生前夕停下来。
#### Step 3 of de-invisible-bug inside function or control flow
Debug 模式实际上就是用来暂停 function 或 control flow 的一种手段。进入 debug 模式以后,才能实施 debug 的第三步并解决 function 或 control flow 内部的隐性 bug。例如:
<video src="images/debug a complete example.mp4" width="100%" controls="controls"></video>
除了手动在代码中插入`browser()`之外,Rstudio 还提供 breakpoint 功能,在代码编辑窗口的行数左侧单击即可。但不推荐这么用,因为在某些场景下,breakpoint 会失效,并不如`browser()`稳定,而且更重要的是,不如`browser()`灵活(详见\@ref(browser-tips))。
## `browser()` tips {#browser-tips}
### Always place `browser()` inside a function
如果`browser()`不在 function 内部,进入 debug 模式后,next(F10)会直接把`browser()`后续的所有非 control flow 的代码全部运行完:
<video src="images/debug browser in linear code.mp4" width="100%" controls="controls"></video>
next(F10)只对 control flow 起作用:
<video src="images/debug browser in linear code stop inside control flow.mp4" width="100%" controls="controls"></video>
但如果`browser()`在 function 内部,调用该 function 时才会进入 debug 模式,此时就会可以一行一行地运行该 function 内部所有位于`browser()`以下的代码。所以,通过`browser()`进入 debug 模式来暂停代码的方式,其设计的初衷就是为了方便用户去解决存在于 function 或 control flow 中的 bug。除此之外,所有既不在 function 又不在 control flow 里的 bug,用户都可以通过手动逐行运行代码的方式实现对代码的绝对控制,完全可以精确地在 bug 出现前夕停下来,也就不需要进入 debug 模式了。换言之,`browser()`是 define function 时 debug 的利器。
### Conditional `browser()`
`browser()`本身是一个 function,所以可以和`print()`或`cat()`以及`if`结构搭配使用,实现在指定条件下触发 debug 模式的效果。
```{r error = TRUE, comment = ""}
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
}
}
```
<video src="images/debug a complete example 2 conditional debug.mp4" width="100%" controls="controls"></video>
## `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())`的组合。
<img style = "display: block; margin: auto;" src="images/debug view vector.png" width="100%"/>
<img style = "display: block; margin: auto;" src="images/debug view vector in matrix form.png" width="100%"/>
- array
`View()`也无法便利地查看维数超过 2 的 array,需要划分成可单独查看的矩阵。
<img style = "display: block; margin: auto;" src="images/debug view array.png" width="100%"/>
<img style = "display: block; margin: auto;" src="images/debug view array in matrix form.png" width="100%"/>
- list
`View()`只能查看 list 的结构,需要根据具体情况使用取子集的方式查看元素。
<img style = "display: block; margin: auto;" src="images/debug view list.png" width="100%"/>
<img style = "display: block; margin: auto;" src="images/debug view list by subsetting.png" width="100%"/>
## Frequently made mistakes
1. 写多余的代码;
```{r}
a <- 1
b <- a + 1
a <- 1
d <- a + b
```
2. 拼写错误;
忘记区分大小写
```{r error = TRUE, comment = ""}
a <- C(1, 2, 3)
```
Function 名拼错
```{r error = TRUE, comment = ""}
b <- maen(c(1, 2, 3))
```
3. 忘记基本语法规范;
在需要使用 character object 时,没有用`""`
```{r error = TRUE, comment = ""}
read.table(
"F:/Nutstore backup/R/codes/RBA/data/txt_example.txt",
col.names = c(studentA, studentB, studentC)
)
```
在调用 function 时,function 的名字后误用`[]`,而不是`()`
```{r error = TRUE, comment = ""}
a <- c[1, 2, 3]
```
在调用 function 时,function 的输入参数是 vector,但没有用`,`隔开
```{r error = TRUE, comment = ""}
read.table(
"F:/Nutstore backup/R/codes/RBA/data/txt_example.txt",
col.names = c("x" "y" "z")
)
```
在调用 function 时,function 的某个 argument 是 vector,但没有用`c()`
```{r error = TRUE, comment = ""}
read.table(
"F:/Nutstore backup/R/codes/RBA/data/txt_example.txt",
col.names = "x", "y", "z"
)
```
在调用 function 时,使用了 function 中没有的 argument
```{r error = TRUE, comment = ""}
read.table(
"F:/Nutstore backup/R/codes/RBA/data/txt_example.txt",
width = c(2, 2)
)
```
在 subsetting 时用了`()`,而不是`[]`
```{r error = TRUE, comment = ""}
a(1) <- 2
```
4. 其他错误。
在没有 names 相关属性时使用 names 的信息 subsetting,或有 names 相关属性时 names 信息拼写错误
```{r error = TRUE, comment = ""}
# 以 list object 为例
# 没有 names
l1 <- list(
c(1, 2, 3),
c(4, 5, 6)
)
mean(l1$x)
# 有 names
l1 <- list(
x = c(1, 2, 3),
y = c(4, 5, 6)
)
mean(l1$X)
```
总结一下,初学容易碰到的错误提示的意思及可能的原因如下表:
| 错误提示 | 意思 | 可能的原因 |
|----------|----------|------------|
| `Error: unexpected xxx in "xxx"` | 在执行某代码片段时碰到了意外的符号 | 使用了非法的变量名,使用了中文逗号/括号,使用控制流没有采用规定的语法格式 |
| `Error: object 'xxx' not found` <br> `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`<br>`In addition: Warning message:`<br>`In file(file, "rt") : cannot open file 'xxx':` <br> `No such file or directory` | 无法打开某文件,没有该文件或文件路径 | 文件路径有拼写错误,或使用的是相对路径,但 working directory 发生了改变,而文件路径没有相应变化 |
高手代码鉴赏:[抛硬币](https://d.cosx.org/d/107768-107768)。
可以使用 debug 模式查看为什么一楼的方案是正确的。
## Recap
1. debug 一律用 Source;
2. 进入 debug 模式一律用`browser()`,请勿使用 Return with Debug;
3. 隐性 bug 如果无法准确定位,可能需要从首行代码开始逐行排除;
4. debug 最重要的技巧是在定位引发 bug 的代码后将其拆分成独立的部分,分别查看;
5. debug 完毕记得删除所有以 debug 为目的插入的代码。