当你的 R 代码出现了错误的时候,会发生什么情况呢? 你会怎么做呢? 你使用什么工具来解决这个问题? 本章将教你如何解决意外的问题(调试),并且向你演示函数如何去追踪错误,你如何基于这些反馈信息采取行动(条件处理),并教你如何避免这些常见的问题(防御性编程)。

调试是解决代码中意想不到的问题的神兵利器。 在这一章节中,你将学习能帮你找到错误起因的工具和技术。 你还将学习基本的调试策略、像 traceback()和 browser()这类有用的函数,以及其他一些 RStudio 中的交互工具。

不是所有的问题都是意想不到的。 当编写一个函数时,你通常可以预测潜在的问题(如文件不存在或错误,不匹配的输入类型)。 条件(conditions)会将这些错误以:错误、 警告和消息三种反馈形式告知给开发者。

严重错误(Fatal error)由 stop()发起,并强制终止所有执行的程序。 当一个函数没有办法继续运行时,才会出现错误(Error)。

警告(Warning)是由 warning()产生的,用于显示潜在的问题,比如当某些向量化的输入元素无效的时候,如 log(-1:2)。

消息由 message()产生,是一种提供信息输出的方法,用户可以很容易地忽略掉它们

(?suppressMessages())。 我经常使用信息来让用户知道,对于一些缺失的重要参数,函数都自动选择了什么值代替它。

条件(conditons)通常是突出起眼的,根据的 R 界面,它会使用粗体字或者显示为红色。 你可以很容易地区分它们,因为错误总是以"Error"开头, 警告总是以"Warning message"开头。函数的作者也可以使用 print()或者 cat()与用户交流,但我认为这不是一种好主意,因为这种输出很难进行捕获或者有选择地忽略掉。 print()的输出不是一个条件,所以你无法使用任何有用的条件处理工具来处理它们,而这也是你接下来要学习的。

条件处理工具(Condition handling tools),比如 withCallingHandlers()、 tryCatch()和 try(),允许你在条件发生时采取特定的动作。 例如,如果你要拟合很多模型,那么即使当某个模型无法聚合时,你可能也想要继续拟合其它的模型。 根据Common Lisp 的思想, R 语言提供了一个异常强大的条件处理系统,但是目前没有很好的文档介绍这方面的内容,使用的也没有那么普遍。 这一章会给你介绍最重要的基础内容,但是如果你想了解更多,那么我推荐下面两个文献:

1. 《 A prototype of a condition system for R》By Robert Gentleman 和 Luke Tierney

(《 R 的条件系统原型》http://homepage.stat.uiowa.edu/~luke/R/exceptions/simpcond.html)。

它描述了一个早期版本的 R 语言条件系统。 虽然自从该文档写成以来, 具体现实应用已发生了一些变化,但是它解释了各个部分如何协调提供了一种很好的概述,以及进行这些设计的动机。

2. 《 Beyond Exception Handling: Conditions and Restarts》by Peter Seibel

(《超越异常处理:条件和重启》 )(http://www.gigamonkeys.com/book/beyond-exceptionhandling-conditionsand-restarts.html)。

它描述了 Lisp 的异常处理,与 R 语言使用的方法非常相似。 它提供了有用的动机以及更复杂的例子。 我为这些例子提供了 R 语言的版本,可以查看 http://adv-r.had.co.nz/beyond-exceptionhandling.html。

本章最后以防御式编程的讨论进行总结:如何在常见的错误发生之前避免它们。在短期来说,你可能需要花更多的时间写代码,但从长远来看,你会节省时间,因为错误消息将会提供更多的信息,让你更快地找出问题的根源。 防御式编程的基本原则是快速失败(fail fast),一旦出现问题就要发起错误。 在 R 中,这通过三种 特定模式来实现: 检查输入的正确性, 避免非标准计算,以及避免函数可以返回不同类型的输出。

小测验

想跳过这一章吗? 去吧,如果你能回答下面的问题的话。 答案在本章末尾。

1. 怎样找到一个错误发生在哪里?

2. browser()函数是做什么的? 列出五个你可以在 browser()环境里使用的键盘

命令。

3. 在代码块中,你使用什么函数忽略错误?

4. 为什么要使用自定义的 S3 类来创建错误?

本章概要

Debugging techniques概述了发现和解决错误的基本方法。

Debugging tools介绍通过R 函数 和Rstudio 的特性帮助你正确定位发生错误的地方。

Condition handling 介绍了了如何在你自己的代码中捕获条件(严重错误、警告和消息)。 这使得你创建的代码更加健壮,以及在错误存在的时候可以得到更多信息。

Defensive programming介绍了防御式编程的一些重要技术,这些技术可用于防止错误发生。

调试技术

“寻找错误是这样一种过程:确认很多你认为是正确的事情——直到你发现哪个是不正确的。” —Norm Matloff

调试代码是具有挑战性的。 许多错误是蹊跷的,很难被找到。 确实,如果错误是显而易见的,那么你可能第一时间就已经能够避免错误了。 如果你的技术不错,那么你可以仅仅使用print()来有效地调试问题,当然有其它工具更是锦上添花。 在本节中,我们将讨论R和RStudio提供的一些有用的工具,并概述一下调试的基本过程。

下面的调试过程绝不简单,希望在你调试的时候,能够帮助你理清思路。 有四个步骤:

1. 意识到程序出现了错误

如果你正在读这一章,那么你可能已经完成了这一步。 这是格外重要的一步:如果你不知道错误的存在,那么你是不可能修复错误的。 这是为什么在编写高质量代码的时候,自动化测试工具很重要的原因之一。 但自动化测试超出了本书的范围,不过你可以在

http://r-pkgs.had.co.nz/tests.html上关于它的内容。

2. 使错误可以重复出现

一旦你确认程序有错误,你就需要让它可以复现。 没有这个过程,错误会变得很难隔离它的影响,并且没办法确认你是否已经成功地修复了错误。

通常,我们会从一个大的有错误的代码块开始,然后慢慢地缩小范围,最后得到一段导致错误的、尽可能小的代码片段。 二分查找(binary search)在这个过程中特别有用。 要进行二分查找,你将反复删减一半的代码,直到你定位到了错误代码段。 这个过程是很快的,因为每进行一步,你需要查看的代码数量都会减少一半。

如果产生错误需要很长的时间,那么研究一下如何能更快地复现错误也是值得的。 你能越快地这样做,就能越快地找出错误原因。

当你创建了一个最小的例子,你可能会发现有些相似的输入并不会引发错误。 请把它们记录下来:它们在诊断错误原因的时候会很有用。

如果你使用自动化测试,这当然也是非常不错的选择。 如果现有测试用例的覆盖率较低,那么可以增加一些相似测试,以确保程序可以一直长期良好运行。 这样会减少新错误出现的几率。

3. 找出错误在哪

如果你够幸运,那么下文中介绍的工具就能帮助你快速识别导致错误的代码。 但是通常,你将会不得不更多地考虑这个问题。 采取科学的方法是一个好主意。 提出假设、设计实验来验证,并且记录测试的结果。 这看起来需要做很多工作,但是系统化的方法将最终会节省你的时间。 我经常浪费很多时间并依靠直觉来解决错误,假如我当时一开始就选择使用系统化的方法的话,结果将会更好。

4. 修改和测试

一旦你找出了错误,你就需要研究如何修复它,并确保修复工作是有效的。 而这又是自动化测试非常有用的地方。 因为它不仅会帮助你确认错误已修正,也会确保在这个过程中没有引入任何新的错误。 而在缺乏自动化测试的情况下,则需要仔细记录正确的输出,并且对比之前导致错误的输入。

调试工具

为了实现调试策略,你需要工具。 在本节中,你将了解R语言和RStudio IDE提供的工具。 RStudio集成的调试工具能够以友好的方式提示开发者代码中存在的问题。接下来,我将向你展示R语言和RStudio两种方式,这样你就可以在任何环境中进行工作了。 你也可以参考官方的RStudio调试文档(Debugging with RStudio相关的),它会介绍最新版本RStudio中的调试工具。 目前,主要有三种调试工具:

  • RStudio的错误检查器以及能列出导致错误的函数调用序列的traceback()。
  • RStudio的Rerun with Debug工具,对应error = browser,可以在错误发生的地方,打开一个交互式会话。
  • 在RStudio的任意断点位置打开一个交互式会话的browser()函数。

下面,我将逐个地解释每个工具。 在编写新函数的时候,你没必要使用这些工具。 如果你发现自己经常在新的代码调用它们,那么你可能需要重新考虑一下你的方法是否合适。 我们应该在小块代码上进行交互式工作,而不是试图一次性编写一个很大的函数。 如果你从小型代码做起,那么你就可以快速识别为什么导致函数运行不正常的原因。 但是如果你一开始就写大段大段的代码,那么你可能很难找出导致问题的原因。

确定调用顺序

第一种工具是函数调用栈——导致错误的函数调用序列。 这里有一个简单的例子:你可以看到,f()调用了g(),g()调用了h(),h()调用了i(),i()函数对一个数字和一个字符串做加法,然后提示一个错误:

f <- function(a) g(a)
g <- function(b) h(b)
h <- function(c) i(c)
i <- function(d) "a" + d
f(10)

当我们在Rstudio中运行这段代码的时候,可以看到:

在错误消息的右边会出现两个选项:"Show Traceback"和"Rerun with Debug"。 如果你点击"Show Traceback",你将看到:

如果你没有使用Rstudio,那么你可以使用traceback()得到同样的信息:

traceback()
# 4: i(c) at exceptions-example.R#3
# 3: h(b) at exceptions-example.R#2
# 2: g(a) at exceptions-example.R#1
# 1: f(10)

从下到上查看调用栈:初始调用是f(),跟着调用了g(),然后调用了h(),最后是i(),而i()会引发错误。 如果使用source()函数调用加载R函数的代码,那么traceback还将以filename.r#linenumber的形式显示函数的位置。 在Rstudio中只需通过鼠标点击,就可以在RStudio编辑器中定位到相应的代码行。

有时,上述信息已经有足够让你跟踪错误并进行修复了。 但是,多数情况下,这还是不够的。 traceback()显示了错误发生的地方,但是并没有解释是什么原因导致地。 而下一个有用的工具是交互式调试器,它可以让你暂停函数执行以及交互地研究函数的状态。

浏览错误

进入交互式调试器的最简单方法是通过RStudio的"Rerun with Debug"(重新运行与调试)工具。 这样会重新运行发生错误的命令,可在错误发生的地方暂停执行。现在以一种交互式的状态进入函数内部,与在函数内定义的任何对象进行交互。 你会在编辑器中看到相应的代码(下一条将运行的语句会高亮),在当前环境中的对象会显示在"Environment"面板中,调用栈则显示在"Traceback"面板中,而且可以在控制台中运行任意行R代码。

像和普通R函数一样,这里有一些你可以在调试模式下使用的特殊命令。 你可以通过RStudio的工具栏或者键盘来访问它们:

Next,n:在函数中执行下一步。 如果你有一个名为n的变量,则要小心;你需要使用print(n)来打印它的信息。

Step into 或s:与next类似,但如果下一步是一个函数,那么它将进入该函数,这样你就可以执行函数的每一行代码。

Finish,或f:执行完当前的循环或者函数。

Continue,c:离开交互式调试并继续进行函数的执行过程。 如果你修复了有问题的状态并想检查函数是否能正确执行,那么这个命令是很有用的。

Stop,Q:停止调试,终止函数,并返回到工作空间。 一旦你找出了问题,然后准备修复错误并重新加载代码,你就可以使用这个命令了。

另外,还有两个比较少用到的命令,它们没有显示在工具栏上:

Enter:重复上一条命令。 我发现很容易就不小心激活了这个命令,所以我使用

options(browserNLdisabled = TRUE)来关掉它。

Where:打印当前调用的堆栈跟踪信息(相当于是在交互状态下的 traceback())。

要想在不使用RStudio的情况下使用这种调试风格,可以使用 error 选项,它指出了发生错误

时运行了哪个函数。 与 Rstudio 调试函数 browser()最相似:它将在发生错误的环境中,启动一个交互式控制台。 使用 options(error = browser)将其打开,重新运行前面的命令,然后使用 options(error = NULL)回到默认的错误行为。 你还可以使用 browseOnce()函数让这个过程自动化, browseOnce()函数的定义如下:

browseOnce <- function() {old <- getOption("error")function() {options(error = old)browser()}
}
options(error = browseOnce())f <- function() stop("!")
# Enters browser
f()
# Runs normally
f()

另外,还有两个你可以使用 error 选项的有用函数:

recover 是 browser 的小改进,因为它允许你进入调用栈中任何调用所处的环境。

这是非常有用的,因为错误的根源是经常一些调用。

dump.frames 相当于非交互式代码的 recover。 它在当前工作目录中创建了一个

last.dump.rda 文件。 然后,在后面的交互式 R 的会话中,你加载该文件,并使用debugger(),进入一个与 recover()具有相同接口的交互式调试器,可以实现批处理代码的交互式调试。

# In batch R process ----
dump_and_quit <- function() {# Save debugging info to file last.dump.rdadump.frames(to.file = TRUE)# Quit R with error statusq(status = 1)
}
options(error = dump_and_quit)# In a later interactive session ----
load("last.dump.rda")
debugger()

使用 options(error = NULL)把错误行为(error behavior)重置为默认状态。 然后错误(error)将打印信息,并且终止函数的执行。

浏览任意代码

当碰到错误进入交互式控制台以后,你可以使用Rstudio断点或browser()进入任意的代码位置。 你可以在Rstudio的行号左侧点击或者按下Shift + F9,来设置断点。 同样地,你也可以在想要暂停的地方添加browser()。 断点的行为类似于browser(),但它们更容易设置(一次点击,而不是按下9个键),并且也不用担心意外风险当你的源代码里添加了browser()语句。

断点的两个小缺点:

1. 在少数情况下,断点是不能工作的:阅读断点故障排除(http://www.rstudio.com/ide/docs/debugging/breakpoint-troubleshooting)来获取更多的细节信息。

2. RStudio目前不支持条件断点,但是你却总是可以把browser()放在一个if语句中。 (译者注:即把browser()放在if语句内部,可以实现有条件地暂停。)

除了自己添加browser()以外,还有另外两个函数会将它添加到代码中:

debug()会在指定函数的第一行插入一个browser()语句。 undebug()则会删除它。 或者,你可以使用debugonce(),它只在函数下一次运行时执行一次browser()。

utils::setBreakpoint()也有类似的作用,但它不使用函数的名字作为参数,而是使用文件名和行号,然后为你找到合适的函数。

这两个函数都是trace()的特例,trace()可以在现有函数的任意位置插入任意代码。 当你调试没有使用source()加载过的代码时,trace()偶尔会有用。 从一个函数中移除trace(),可以使用untrace()。 在每一个函数中,你只能执行一个trace(),但是一个trace()可以调用多个函数。

调用栈:traceback()、where()和recover()

不幸的是,由traceback()、browser() + where打印出来的调用栈,与recover()打印出来的调用栈是不一致的。 下面的表格说明了由这三种工具显示出来的一个简单的嵌套调用的调用栈。

注意:打印结果编号在traceback()和where之间是不同的,而recover()是以相反的顺序来显示调用的,并且省略了对stop()的调用。 RStudio与traceback()以相同的顺序显示调用,但是省略了编号。

其它类型的错误

除了生成错误或返回不正确的结果以外,函数的失败还有其它的方式。

  • 函数可能产生未预料的警告。 跟踪警告的最简单的方法是使用options(warn = 2)将它们转换成错误,并使用常规的调试工具。 当你这么做的时候,你就会在调用栈中看到一些额外的调用,如doWithOneRestart()、withOneRestart()、withRestarts()和.signalSimpleWarning()。 可以忽略掉这些:它们是用来把警告变成错误的内部函数。
  • 函数可能生成未预料的消息。 没有内置的工具来帮助解决这个问题,但是我们可以创建一个:
message2error <- function(code) {
withCallingHandlers(code, message = function(e) stop(e))
}
f <- function() g()
g <- function() message("Hi!")
g()
# Error in message("Hi!"): Hi!
message2error(g())
traceback()# 10: stop(e) at #2
# 9: (function (e) stop(e))(list(message = "Hi!\n", call = message("Hi!")))
# 8: signalCondition(cond)
# 7: doWithOneRestart(return(expr), restart)
# 6: withOneRestart(expr, restarts[[1L]])
# 5: withRestarts()
# 4: message("Hi!") at #1
# 3: g()
# 2: withCallingHandlers(code, message = function(e) stop(e)) at #2
# 1: message2error(g())

正如警告一样,你需要忽略一些traceback的调用(如前面的两条和后面的七条)。

  • 函数可能永远不会返回。 这种情况是特别难以自动调试的,但是有时,终止函数并查看调用栈,可能会得到一些信息。 否则,就使用上面描述的基本的调试策略。
  • 最糟糕的情况是,你的代码可能会导致R完全崩溃出,使你没有办法进入交互式的代码调试状态。 这表明在底层的C语言代码中存在错误。 这是很难调试的。 有时,一些交互式调试工具,比如gdb,可能会有用,但是描述如何使用它超出了本书的范围。

如果崩溃是由基本R代码导致的,那么请给R-help发送一个可重现的例子。 如果崩溃发生在一个包中,那么可以联系包的维护人员。 如果这是由你自己的C或C++代码导致的,那么你需要使用大量的print()语句来定位错误所在的位置,然后你需要使用更多的print()语句来找出哪些数据结构没有你期望的属性。

条件处理

未预料的错误需要交互式调试来找出发生了什么错误。 但是,有些错误是可以预料的,你需要自动处理它们。 在R语言中,当你为不同的数据集拟合许多模型的时候,可预料的错误出现得最频繁,比如自助法重复(bootstrap replicates)。 有时,模型可能拟合失败,并且得到一个错误,但是你并不想停止一切。 相反,你想拟合尽可能多的模型,然后在拟合完成之后才执行诊断。

在R语言中,有三种程序化地处理条件的工具(也包括错误):

即使程序发生了错误,try()也能允许继续执行代码。

tryCatch()允许你指定处理(handler)函数,控制着当某种条件发生时,应该做什么。

withCallingHandlers()是tryCatch()的一种变体,它在不同的上下文中运行处理函数。 我们很少会需要它,但是留意一下它也是很有用的。 下面更详细地描述了上述这些工具。

使用try()忽略错误

即使程序发生了错误,try()可以允许继续执行。 例如,通常,如果你运行一个函数,它抛出了一个错误,那么它会立即终止,并且不会返回值:

f1 <- function(x) {
log(x)
10
}
f1("x")
#> Error:

然而,如果你把产生错误的语句包围在try()中,那么错误消息将打印出来,但是代码仍然会继续执行:

f2 <- function(x) {
try(log(x))
10
}
f2()
#> [1] 10

你可以使用try(..., silent = TRUE)屏蔽消息。 要把更大的代码块传入try()中,需要把代码包围在{}中:

try({
a <- 1
b <- "x"
a + b
})

你也可以捕获try()函数的输出。 如果成功,它将是代码块(就像一个函数)的最后被计算的结果。 如果不成功,它将是一个"try-error"类的(不可见的)对象:

success <- try(1 + 2)
failure <- try("a" + "b")
str(success)
#> num 3
str(failure)
#> Class 'try-error' atomic [1:1] Error in "a" + "b" :
#>
#> ..- attr(*, "condition")=List of 2
#> .. ..$ message: chr ""
#> .. ..$ call : language "a" + "b"
#> .. ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

当你将一个函数应用于列表中的多个元素时,try()特别有用:

elements <- list(1:10, c(-1, 10), c(T, F), letters)
results <- lapply(elements, log)
#> Warning: NaNs
#> Error:
results <- lapply(elements, function(x) try(log(x)))
#> Warning: NaNs

没有内置函数来测试try-error类,所以我们要定义一个。 然后,你可以使用sapply()很容易地找到错误的位置,并且提取成功信息或者看看导致失败的输入。

is.error <- function(x) inherits(x, "try-error")
succeeded <- !sapply(results, is.error)
# look at successful results
str(results[succeeded])
#> List of 3
#> $ : num [1:10] 0 0.693 1.099 1.386 1.609 ...
#> $ : num [1:2] NaN 2.3
#> $ : num [1:2] 0 -Inf
# look at inputs that failed
str(elements[!succeeded])
#> List of 1
#> $ : chr [1:26] "a" "b" "c" "d" ...

另一个有用的try()用法是如果一个表达式失败,那么就使用默认值。 在try块之外,简单地指定一个默认值,然后再运行有风险的代码:

default <- NULL
try(default <- read.csv("possibly-bad-input.csv"), silent = TRUE)

另外,使用plyr::failwith()会是得这种模式更容易实现,具体会在function operators章节讨论。

使用tryCatch()处理条件

tryCatch()是一种处理条件的通用工具:除了错误以外,你还可以对警告、消息和中断采取不同的行动。 在前面,你已经看到过错误(由stop()产生)、警告(warning())和消息(message()),但是中断(interrupt)是新的概念。 中断不能由程序员直接产生,但是,当用户通过按Ctrl + Break、Escape或Ctrl + C(依赖于平台),试图终止执行的时候,中断就会产生。

使用tryCatch(),你可以把条件映射到处理函数,它们是一些在条件发生时调用的函数,这些函数被作为输入传入tryCatch()。 如果发生了一个条件,那么tryCatch()将调用第一个名字与条件类之一匹配的处理程序。 仅有的有用的内置名称是:错误(error)、警告(warning)、信息(message)、中断(interrupt)和万能(catch-all)条件。 处理函数可以做任何事情,但是通常它要么返回一个值或者创建一个内容更丰富的错误消息。 例如,下面的show_condition()函数设置了处理函数,以返回发生的条件类型:

show_condition <- function(code) {
tryCatch(code,
error = function(c) "error",
warning = function(c) "warning",
message = function(c) "message"
)
}
show_condition(stop("!"))
#> [1] "error"
show_condition(warning("?!"))
#> [1] "warning"
show_condition(message("?"))
#> [1] "message"
# If no condition is captured, tryCatch returns the value of the input
show_condition(10)
#> [1] 10

你可以使用tryCatch()来实现try()。 一种简单的实例如下所示,真实的情况是错误信息要比没有使用tryCatch()的情况复杂多了。 注意conditionMessage()用于提取与原始错误相关的信息。

try2 <- function(code, silent = FALSE) {
tryCatch(code, error = function(c) {
msg <- conditionMessage(c)
if (!silent) message("Error: ", c)
invisible(structure(msg, class = "try-error"))
})
}
try2(1)
#> [1] 1
try2(stop("Hi"))
#> Error: Error in doTryCatch(return(expr), name, parentenv, handler): Hi
try2(stop("Hi"), silent = TRUE)

与条件发生时返回默认值一样,处理函数可以用来生成内容更加丰富的错误消息。 例如,以下函数包装了read.csv()函数,它通过修改存储在错误条件对象(error condition object)中的消息,会将文件名添加到任何错误中:

read.csv2 <- function(file, ...) {
tryCatch(read.csv(file, ...), error = function(c) {
c$message <- paste0(c$message, " ( in ", file, ")")
stop(c)
})
}
read.csv("code/dummy.csv")
#> Error: 'row.names'
read.csv2("code/dummy.csv")
#> Error: 'row.names' ( in code/dummy.csv)

当用户试图中止运行代码的时候,如果你想采取特殊行动,那么捕获中断可能是有用的。 不过要小心,这样很容易就会创建无限循环(除非你强制停止(kill)R)!

# Don't let the user interrupt the code
i <- 1
while(i < 3) {
tryCatch({
Sys.sleep(0.5)
message("Try to escape")
}, interrupt = function(x) {
message("Try again!")
i <<- i + 1
})
}

tryCatch()还有另一个参数:finally。 它指定了一块要执行的代码(而不是一个函数),不管前面的表达式是成功还是失败,这段代码永远都会被执行。 这可以用于清理工作如删除文件、关闭连接等等)。 这在功能上相当于使用了on.exit(),但是它可以包装更小的代码块,而不是整个函数。

withCallingHandlers()

withCallingHandlers()是tryCatch()的一种替代。 这两个函数之间有两种主要的区别:

  • tryCatch()的处理函数的返回值是由tryCatch()返回的,而withCallingHandlers()处理函数的返回值将被忽略:
f <- function() stop("!")
tryCatch(f(), error = function(e) 1)
#> [1] 1
withCallingHandlers(f(), error = function(e) 1)
#> Error: !
  • withCallingHandlers()中的处理函数,是在产生了条件的调用的上下文中被调用的;而tryCatch()的处理函数是在tryCatch()的上下文中被调用的。 这里,我们使用了sys.calls()函数来显示这些情况,该函数相当于是运行时(run-time)版本的traceback()函数——它列出了引出当前函数的所有调用。
f <- function() g()
g <- function() h()
h <- function() stop("!")
tryCatch(f(), error = function(e) print(sys.calls()))
# [[1]] tryCatch(f(), error = function(e) print(sys.calls()))
# [[2]] tryCatchList(expr, classes, parentenv, handlers)
# [[3]] tryCatchOne(expr, names, parentenv, handlers[[1L]])
# [[4]] value[[3L]](cond)
withCallingHandlers(f(), error = function(e) print(sys.calls()))
# [[1]] withCallingHandlers(f(), error = function(e) print(sys.calls()))
# [[2]] f()
# [[3]] g()
# [[4]] h()
# [[5]] stop("!")
# [[6]] .handleSimpleError(function (e) print(sys.calls()), "!", quote(h()))
# [[7]] h(simpleError(msg, call))

这也会影响调用on.exit()的顺序。 这些细微的差别很少会有用,除非你想精确地捕获错误,并将其传递给另一个函数。 大多数情况下,你不应该使用withCallingHandlers()。

自定义信号类(Custom signal classes)

在R语言中进行错误处理的挑战之一是,大多数函数只是使用一个字符串来调用stop()。 这意味着如果你想找出某个特定的错误是否发生了,那么你必须看看错误消息的文本。 这是很容易出错的,不仅因为错误的文本随着时间的推移可能会发生改变,还因为许多错误消息经过了转换,所以消息可能跟你所期望的是完全不同的。

R语言有一种鲜为人知并且很少有人使用的特性,以解决这个问题。 condition是S3类,因此,如果你想区分不同类型的错误,那么你可以定义自己的类。 每个发生条件信号的函数,stop()、warning()和message(),都可以传入一个字符串列表,或者一个自定义的S3条件对象。 自定义条件对象并不是经常使用的,但是非常有用,因为它让用户可以用不同的方式来应对不同的错误。 例如,"意料之中的"("expected")错误(如对某些输入数据集,模型未能聚合),可以被默默地忽略掉,而未预料的错误(如没有可用的磁盘空间)则可以传送给用户。

R语言没有为条件提供内置的构造函数,但是我们可以很容易地添加一个。 条件必须包含消息和调用组件,还可能包含其它有用的组件。 当创建一个新条件时,它应该总是继承自condition类,错误(error)、警告(warning)或消息(message)中的任意一个。

condition <- function(subclass, message, call = sys.call(-1), ...) {
structure(
class = c(subclass, "condition"),
list(message = message, call = call),
...
)
}
is.condition <- function(x) inherits(x, "condition")

你可以使用signalCondition()产生任意条件信号,但是除非你实例化自定义信号处理函数(custom signal handler)(使用了tryCatch()或withCallingHandlers()),否则什么都不会发生。 相反,更合适的是使用stop()、warning()或message()来触发平常的处理。 如果你的条件的类与函数不匹配,那么R语言并不会报错,但是在实际的代码中应该避免这种情况。

c <- condition(c("my_error", "error"), message = "This is an error")
signalCondition(c)
# NULL
stop(c)
# Error: This is an error
warning(c)
# Warning message: This is an error
message(c)
# This is an error

你可以使用tryCatch()为不同的错误类型采取不同的行动。 在这个例子中,我们创建了一个方便的custom_stop()函数,它让我们使用任意类来产生错误条件信号。 在一个真正的应用程序中,最好有单独的S3构造函数,这样你就可以写入文档,详细地描述错误类(error class)。

custom_stop <- function(subclass, message, call = sys.call(-1), ...) {
c <- condition(c(subclass, "error"), message, call = call, ...)
stop(c)
}
my_log <- function(x) {
if (!is.numeric(x))
custom_stop("invalid_class", "my_log() needs numeric input")
if (any(x < 0))
custom_stop("invalid_value", "my_log() needs positive inputs")
log(x)
}
tryCatch(
my_log("a"),
invalid_class = function(c) "class",
invalid_value = function(c) "value"
)
#> [1] "class"

注意,当对tryCatch()使用多个处理函数和自定义类的时候,第一个匹配的处理程序会被调用,而不是匹配最好的。 由于这个原因,你需要确保把最针对性的处理函数放在第一位:

tryCatch(customStop("my_error", "!"),
error = function(c) "error",
my_error = function(c) "my_error"
)
#> [1] "error"
tryCatch(custom_stop("my_error", "!"),
my_error = function(c) "my_error",
error = function(c) "error"
)
#> [1] "my_error"

练习

比较以下的message2error()函数的两种实现。 在这种情况下,withCallingHandlers()的主要优点是什么? (提示:仔细看看traceback。)

message2error <- function(code) {
withCallingHandlers(code, message = function(e) stop(e))
}
message2error <- function(code) {
tryCatch(code, message = function(e) stop(e))
}

防御式编程

防御性编程(Defensive programming)是一种艺术,即使发生了未预料的错误,它也可以使代码以一种良好定义的方式,进入失败状态。 防御性编程的一个重要原则是快速失败(fail fast):一旦发现了错误,就立即发出错误信号。 这对代码的作者来说,意味着更多的工作,但是对用户来说,它可以使调试变得更容易,因为能很早就能发现错误,而不是等到未预料的输入已经传入了几个函数之后。

在R语言中,通过三种方式实现"快速失败"的原则:

  • 严格限制你接受的参数。 例如,如果你的函数的输入参数不是向量化的,那么确保检查一下输入是不是标量。 你可以使用stopifnot()、assertthat包(hadley/assertthat),或者简单的if语句和stop()来实现。
  • 避免使用非标准计算的函数,例如subset、transform和with。 以交互的方式使用时,这些函数可以节省时间,这是因为它们会做出一些假设,以减少键盘输入;而当它们失败了的时候,通常不会提供信息丰富的错误消息。 你可以在metaprogramming章节了解更多关于非标准计算的知识。
  • 避免根据输入的不同,而返回不同类型输出的函数。 最大的两个违背这一条的函数是[和sapply()。 每当在函数内部对数据框进行取子集操作时,你应该总是设置drop = FALSE,否则你会不小心地把单列数据框转化成向量。 同样,永远不要在函数内部使用sapply():永远使用更严格的vapply(),如果输入不正确,那么它将抛出一个错误;甚至对零长度的输入,它也会返回正确的输出类型。

交互式分析和编程之间有一定的矛盾。 当你进行交互式工作的时候,你希望R能按照你的想法进行工作。 如果代码出错了,那么你希望能马上发现问题,这样你就可以进行修复。 当你进行编程的时候,你希望函数对哪怕是轻微的错误或遗漏都能产生错误。 在编写函数时候,请在心里记住这一点。 如果你编写的函数是用来使交互式数据分析更加便利的,那么可以随意猜测数据分析师的想法,并能自动地从小错误中进行恢复。 如果你编写的函数是用于编程的,那么一定要严格。 永远不要猜测调用者需要什么。

练习

  • 下面的col_means()函数的目的是计算数据框中每个数值列的均值(列内均值,不是整个数据框)。
col_means <- function(df) {
numeric <- sapply(df, is.numeric)
numeric_cols <- df[, numeric]
data.frame(lapply(numeric_cols, mean))
}

然而,这个函数对于异常的输入,并不是那么具有健壮性(robust)。 看看下面的结果,确定哪些是不正确的,然后修改col_means()使它变得更健壮。 (提示:在col_means()函数内部,有两个函数调用特别容易出现问题)。

col_means(mtcars)
col_means(mtcars[, 0])
col_means(mtcars[0, ])
col_means(mtcars[, "mpg", drop = F])
col_means(1:10)
col_means(as.matrix(mtcars))
col_means(as.list(mtcars))
mtcars2 <- mtcars
mtcars2[-1] <- lapply(mtcars2[-1], as.character)
col_means(mtcars2)
  • 以下函数"滞后"(lag)一个向量,它返回这样的一个向量:相对于原始的x,它把x推后n个位置,前面以NA来填充。 改进该函数,使得它具备以下功能: (1)如果n不是是一个向量,那么返回一个有用的错误消息; (2)当n是0或者比x长的时候,它能有合理的行为。
lag <- function(x, n = 1L) {
xlen <- length(x)
c(rep(NA, n), x[seq_len(xlen - n)])
}

调试、条件处理和防御式编程相关推荐

  1. C/C++ 踩过的坑和防御式编程

    相信你或多或少地用过或者了解过 C/C++,尽管今天越来越少地人直接使用它,但今天软件世界大多数软件都构筑于它,包括编译器和操作系统.因此掌握一些 C/C++ 技能的重要性不言而喻. 这场 Chat ...

  2. 编程范式:函数式编程防御式编程响应式编程契约式编程流式编程

    不长的编码生涯,看到无数概念和词汇:面向对象编程.过程式编程.指令式编程.函数式编程.防御式编程.流式编程.响应式编程.契约式编程.进攻式编程.声明式编程--有种生无可恋的感觉. 本文试图加以汇总和整 ...

  3. Defensive Programming 防御式编程(Defensive Programming)

    Defensive Programming 防御式编程(Defensive Programming)是提高软件质量技术的有益辅助手段 怎么理解呢?防御式编程思想的理解可以参考防御式驾驶: 在防御式驾驶 ...

  4. 《代码大全2》第8章 防御式编程

    目录 前言 8.1 保护程序免遭非法输入数据的破坏 8.1.1 三种方式处理"垃圾进" 8.2.2 思考:程序输出时也应该增加防御 8.2.3 保留"证据" 8 ...

  5. 软件构造-犯错的艺术——健壮性与正确性,异常,防御式编程,debugging与test的思考与总结...

    健壮性与正确性 健壮性与正确性是不同的--一个倾向于使程序尽可能保持运行,即使遇到错误,一个倾向于使程序尽可能正确,不在意保持运行 异常 异常分为两种--checked exception与unche ...

  6. 6-3 断言与防御式编程

    一.ADT的设计 静态检查.动态检查.使用不可变的类型.值.引用等都有助于减少bug. bug是不可能完全避免的,要将bug限定在一个小范围内,使得程序尽早出问题.例如下图,前置条件要求x>=0 ...

  7. Cocosd-x”设计模式“之五 :防御式编程”模式“

    这一篇将来学习防御式编程模式,其实它并不是一种标准的设计模式,使用它主要是为了提高程序的健壮性,其实这是软件开发中一个我们必须熟悉的模式,因为在程序代码中,很多地方往往存在一定的不确定性,如果我们对于 ...

  8. 华山论剑之契约式编程与防御式编程

    背景 事情的来由还要从几十几亿年前的一次星球大爆炸说起,sorry,背错台词了,是从几天前讨论接口返回数据和几个月前讨论课件本地数据结构说起,简单的说,就是碰到约定好的内容出现异常,是我们在程序中内部 ...

  9. 契约式编程与防御式编程

    背景 事情的来由还要从几十几亿年前的一次星球大爆炸说起,sorry,背错台词了,是从几天前讨论接口返回数据和几个月前讨论课件本地数据结构说起,简单的说,就是碰到约定好的内容出现异常,是我们在程序中内部 ...

最新文章

  1. python 链表中倒数第k个节点
  2. JdbcTemplate中的query方法(代码)
  3. Win10 ancona傻瓜安装tensorflow-gpu,ancona傻瓜安装pytorch-gpu
  4. 副主任护师主要英语和计算机吗,有没有晋升副主任护师的
  5. Linux系统下xampp集成环境安装
  6. Java面试官:Kafka集群管理
  7. git21天打卡day4-查看仓库地址
  8. gx works2 存储器空间或桌面堆栈不足_2020福清市gx螺旋输送机价格厂家发货-衡泰...
  9. 1196 骨牌铺放(宁波大学oj)
  10. redis 系列16 持久化 RDB
  11. [R语言绘图]绘图样式设置(符号、线条、颜色、文本属性)
  12. 工程项目管理系统java程序,基于jsp的工程项目管理系统-JavaEE实现工程项目管理系统 - java项目源码...
  13. 用计算机怎么按e,在计算器上e的多少次方怎样按
  14. oracle dbms_lob trim,ORACLE LOB处理
  15. 坐标转换—高斯正反算(附测量助理最新版软件下载)
  16. Spring Data JPA 查询方法的命名语法与参数
  17. 电脑截图快捷键有哪些?5大截图方法总结!(2023版)
  18. java编一个漏斗_java – 漏斗分析计算,你如何计算漏斗?
  19. 快速过熊掌号2.0新手任务了解熊掌号!
  20. 解决 git clone fatal: unable to access ‘https://github.com...‘: 的一种方法

热门文章

  1. python版jpeg合成pdf两种方法
  2. 推特热议,一图胜千言!
  3. mysql中计算金额_使用MySQL计算单个表中借方和贷方的余额
  4. 依巴谷星表中的毕星团认证杯B题
  5. 基于 PHP 实现的微信小程序 pdf 文件的预览服务
  6. 基于JAVA中医药科普网站计算机毕业设计源码+系统+数据库+lw文档+部署
  7. 永磁同步风力发电机并网逆变器设计 风机并网:发电机采用PMSG,变换器采用:双PWM变流器
  8. Shell脚本写一个应用监控程序
  9. SQL Server数据库性能优化(三)之 硬件瓶颈分析
  10. 电赛滚球控制系统树莓派代码