2017-08-02 20:42:11

目录

异常状况

异常状况(Condition)的3个级别

  • 错误(errors/exceptions)
    • 程序遇到致命错误,无法继续,调用stop()函数退出执行
    • 错误信息打印到控制台时,带"Error"提示符
  • 警告(warnings)
    • 程序遇到错误,但仍可以继续执行,调用warning()函数显示潜在问题
    • 可以用suppressWarnings()函数屏蔽警告
    • 错误信息打印到控制台时,带"Warning"提示符
  • 提醒(messages)
    • 不一定是错误,调用message()将运行中的重要信息提示给用户
    • 可以用suppressMessage()函数屏蔽提醒信息
    • 信息打印到控制台时,不带异常提示符

例子

  • 错误

    mean(a)
    Error in mean(a) : object 'a' not found
  • 警告

    log(-1)
    Warning in log(-1) : NaNs produced
    [1] NaN
  • 提醒

    library(epiR)
    Package epiR 0.9-87 is loaded
    Type help(epi.about) for summary information

调试

调试的重要性

Fred Brooks: Much of the essence of building a program is in fact the debugging of the specification.

  • 程序的调试(debugging)和编制(composing)同等重要
  • 程序缺陷(defect/bug)通常难以避免,需要通过调试技术定位、修复

调试四步走

  1. 发现缺陷存在
  2. 重复缺陷的发生过程
  3. 定位缺陷位置
  4. 修复缺陷代码

翻检堆栈 (traceback)

  • f1逐级调用低级函数,构成一个调用堆栈(call stack)
  • 当发生错误,可当即traceback()在堆栈中回溯
  • 也可以通过RStudio的错误检查器(Error inspector)查看回溯结果
f1 <- function(x1) f2(x1)
f2 <- function(x2) f3(x2)
f3 <- function(x3) f4(x3)
f4 <- function(x4) f5(x4)
f5 <- function(x5) "A" + x5
f1(1)
 Error in "A" + x5 : 
 non-numeric argument to binary operator 

点"Show Traceback",或traceback()

traceback()
5: f5(x4) at #1
4: f4(x3) at #1
3: f3(x2) at #1
2: f2(x1) at #1
1: f1(1)

动态调试 (debug)

  • 点击"Rerun with Debug":
  • 直接调用debug()函数 (事后要undebug())

包括5个按钮:

  1. 接着(Next, n/F10): 执行函数的下一步
  2. 步进(Step in, s/Shift+F4): 类似Next,但如下一步是个函数,则进入该函数逐行调试
  3. 完成(Finish, f/Shift+F6): 完成执行当前循环块或函数
  4. 继续(Continue, c/Shift+F5): 结束调试并完成执行剩余的代码
  5. 结束(Stop, Q/Shift+F8): 退出调试和执行,返回全局环境

    Error in "A" + x5 : non-numeric argument to binary operator
    Called from: f5(x4)
    Browse[1]> n

设置调试选项

  • 默认遇到错误时,调用的调试选项是
getOption("error")
## NULL
  • 可以改为browser,则遇错自动进入动态调试
options(error = browser)
  • 或改用其他(但记得从备份改回来)
browseOnce <- function() {
  old <- getOption("error")
  function() {
    options(error = old)
    browser()
  }
}
options(error = browseOnce())

recover和dump.frame

  • 动态调试时,通过recover()可进入调用堆栈任一步的环境
Error in "A" + x5 : 
    non-numeric argument to binary operator
Called from: f5(x4)
Browse[1]> recover()

Enter a frame number, or 0 to exit   

1: f1(1)
2: #1: f2(x1)
3: #1: f3(x2)
4: #1: f4(x3)
5: #1: f5(x4)
6: #1: (function () 
{
    .rs.breakOnError(TRUE)
})()
7: .rs.breakOnError(TRUE)
8: eval(substitute(browser(skipCalls = pos), 
    list(pos = (length(sys.frames()) - frame) + 2)), 
    envir = sys.frame(frame))
...
  • dump.frame在当前工作目录下创建一个last.dump.rda文件用于后续调试
  • 在批处理模式中,先编写下面的代码
dump_and_quit <- function() {
  # 调试信息写入last.dump.rda
  dump.frames(to.file = TRUE)
  # 以出错状态退出R
  q(status = 1)
}
options(error = dump_and_quit)
  • 如批处理出现错误,会退出R进程
  • 随后可在交互模式下启用debugger窗口进行调试
load("last.dump.rda")
debugger()

设置断点 (browser)

  • 断点(break point)是常用的调试技术
  • 可以在自己的函数或代码块中机智地设置断点
    • RStudio中,在行号左侧点击一下,可设置一个断点
    • 在代码中插入一行browser
  • 运行时,程序会在断点处暂停
    • browser()插入到第一行,即相当于运行时debug()
    • debug()函数正是这么做的
  • 查看无误后,Shift+F9继续执行
f <- function(x){
    if (1 %in% x){
        browser()
        return("There is 1")
    }else{
        return("There is no 1")
    }
}
f(1:4)
Called from: f(1:4)
Browse[1]> x
[1] 1 2 3 4
Browse[1]> n
debug at #4: return("There is 1")
Browse[2]> n
[1] "There is 1"

调试警告

  • 默认只对错误进行调试
  • 如要对警告也进行调试,需要调整设置,从而使用常规的调试工具
    • 将警告转为错误: options(warn=2)
    • 自己写一个函数
    message2error <- function(code) {
        withCallingHandlers(code, message = function(e) stop(e))
    }
    f <- function() message("yup")
    message2error(f())
    Error in message("yup") : yup

异常处置

try: 忽略错误

出现错误默认整体退出,不返回结果

f <- function(x){
    log(x)
    2
}
f("1")
Error in log(x) : non-numeric argument 
    to mathematical function

try捕捉到的错误结果属于"try-error"类

class(try(log("1")))
[1] "try-error"

try()可捕捉错误信息,并继续执行代码

f <- function(x, ...){
    try(log(x), ...)
    2
}
f("1")
Error in log(x) : non-numeric argument 
    to mathematical function
[1] 2

try(..., silent=TRUE)隐藏错误信息

f("1", silent=TRUE)
[1] 2

tryCatch: 处置错误

tryCatch(expr, ..., finally)
  • tryCatch不但能捕捉错误,还提供了一个异常处置框架
  • 将异常状况匹配给不同的处置程序(handler)进行处理

举个例子:

show_cond <- function(expr, ...){
    tryCatch(expr, 
        error=function(cond) "error", 
        warning=function(cond) "warn",
        message=function(cond) "msg",
        finally=cat("The output: "))
}
show_cond(log(10))

The output: [1] 2.302585

show_cond(message("!"))

The output: [1] "msg"

show_cond(log(-1))

The output: [1] "warn"

show_cond(log("1"))

The output: [1] "error"

withCallingHandlers

  • withCallingHandlers是在产生异常的语境被调用的
  • tryCatch是在tryCatch的语境被调用的
f1 <- function() f2()
f2 <- function() stop("Stop!")
tryCatch(f2(), error=function(e) 
    print(sys.calls()))
[[1]] tryCatch(f2(), error = function(e) 
    print(sys.calls()))
[[2]] tryCatchList(expr, classes, 
    parentenv, handlers)
[[3]] tryCatchOne(expr, names, parentenv, 
    handlers[[1L]])
[[4]] value[[3L]](cond)
withCallingHandlers(f2(), error=function(e)
    print(sys.calls()))
[[1]] withCallingHandlers(f2(), 
    error = function(e) print(sys.calls()))
[[2]] f2()
[[3]] stop("Stop!")
[[4]] .handleSimpleError(function (e) 
print(sys.calls()), "Stop!", quote(f2()))
[[5]] h(simpleError(msg, call))
Error in f2() : Stop!

扩展意外类型

condition <- function(subclass, message, 
    call = sys.call(-1), ...) {
    structure(
        class = c(subclass, "condition"),
        list(message = message, call = call),
        ...
    )
}
  • 除了错误、警告、提醒外,也可以自定义意外类型
  • 这些自定义意外必须是三个基本意外类型的子类
err.neg <- condition(
    c("neg_error", "error"), "Cannot be negative!")
wrn.zero <- condition(
    c("zero_warn", "warning"), "Cannot be zero!")
msg.greet <- condition(
    c("greet_msg", "message"), "Bingo!")
  • 自定义函数newLog,捕捉扩展错误类型
newLog <- function(x){
    if (x<0) stop(err.neg)
    else if (x==0) warning(wrn.zero)
    else message(msg.greet)
    return(log(x))
}
tryCatch(newLog(1))
Bingo![1] 0
tryCatch(newLog(-1))
Error: Cannot be negative!
tryCatch(newLog(bb))
Error in newLog(bb) : object 'bb' not found

防御式编程

何为防御式编程

墨菲定律(Murphy's Law): 凡是可能出错的事,准会出错。

  • 防御性编程(Defensive programming)是防御式设计的一种具体体现,它是为了保证,对程序的不可预见的使用不会造成程序功能上的损坏
    • 它可以被看作是为了减少或消除墨菲定律效力的努力
    • 防御式编程主要用于可能被滥用,恶作剧或无意地造成灾难性影响的程序上。
  • 防御式编程的核心是"快速失败、抛出错误"
    • 在可能出现问题的地方预防性地创建测试环境
      • 及时捕捉错误,并
      • 执行控制损害代码
    • 从一开始就编写正确的代码,而不是将错误积累到测试环节去纠正

防御式编程的通用原则

  • 使用好的编码风格和合理的设计
  • 避免闪电式编程
  • 不要相信任何人,包括自己
  • 编码的目标是清晰,不只是简洁
  • 不要让任何人做他们不该做的修补工作 (作用域)
  • 编译时开启所有警告开关
  • 使用静态分析工具
  • 使用安全的数据结构
  • 检查所有的返回值
  • 重视所有稀有资源,审慎地管理它们的获取和释放
  • 在声明时对变量初始化
  • 尽可能推迟变量的声明
  • 使用标准化语言工具,写标准化语言
  • 使用好的诊断信息日志工具
  • 审慎地使用强制转换
  • 细则
    • 提供默认行为
    • 遵从语言习惯
    • 检查数值上下限
    • 正确设置常量
  • 约束
    • 前置条件:对参数作限定
    • 后置条件:对结果作判断
    • 不变条件:每当程序执行到达特定点(循环中、方法调用等)时为真的条件,防止逻辑错误;
    • 断言:任何关于程序在给定位置状态的陈述;
  • 约束的内容
    • 检查所有的数组访问是否都在边界内
    • 确保函数参数有效
    • 在函数结果返回之前对其进行充分检查
  • 移除约束

R的防御技巧1: 严格限制输入

  • match.arg + if/stopstopifnot
f <- function(x, fun=c("mean", "median")){
    # 确保x是纯数值
    stopifnot(all(sapply(x, is.numeric)))
    # 确保fun只能是列表中规定的三个
    fun <- match.arg(fun)
    switch(fun, mean=lapply(x, mean), 
        median=lapply(x, median), 
        var=lapply(x, var))
}
f(1:10, "sum")
Error in match.arg(fun) : 'arg' should be 
    one of “mean”, “median”, “var”
  • 断言(assertthat包)
f <- function(x, fun=c("mean", "median")){
    library(assertthat)
    assert_that(all(sapply(x, is.numeric)))
    fun <- match.arg(fun)
    switch(fun, mean=lapply(x, mean), 
        median=lapply(x, median), 
        var=lapply(x, var))
}
f(list(1:10, 11:20, "a"), "mean")
Error: Elements 3 of sapply(x, is.numeric) 
    are not true

R的防御技巧2: 避免使用非标准求值函数

  • 常用的非标准求值(non-standard evaluation)函数:subsettransformwith
    • 有时候用substitutequoteparseeval等函数自定义非标准求值函数
  • 非标准求值函数常常在交互模式下使用
  • 但在编写自定义函数时、批处理模式下,非标准求值函数往往无法获得明确错误提示,难以调试
f1 <- function(df, cond)
    df[cond,]
f1(mtcars, mtcars$mpg>33)
                mpg cyl disp hp drat ... carb
Toyota Corolla 33.9   4 71.1 65 4.22 ...    1
f2 <- function(df, cond)
    subset(df, cond)
f2(mtcars, mpg>33)
Error in eval(e, x, parent.frame()) : 
    object 'mpg' not found

R的防御技巧3: 避免输出类型变异

  • 意外降维: [

    getCol <- function(df, i) df[,i]
    getCol(mtcars[,1], 1)
    Error in df[, i] : incorrect number of dimensions
    sapply(mtcars[1:8, 1], mean)
    [1] 21.0 21.0 22.8 21.4 18.7 18.1 14.3 24.4
  • 结构变异: sapply

    sapply(integer(), identity)
    list()
  • 避免降维: drop=FALSEsimplify=FALSE

    getCol <- function(df, i) df[, i, drop=FALSE]
    getCol(mtcars[, 1, drop=FALSE], 1)
    sapply(mtcars[1:8, 1, drop=FALSE], mean)
  • 保障数据结构: 用vapply代替sapply

    vapply(integer(), identity, 0L)
    integer(0)
    vapply(list(c(1, 2), c(1, 3, 1)), 
        function(v) which(v==1), 0)
    Error in vapply(list(c(1, 2), c(1, 3, 1)), 
        function(v) which(v == 1),  : 
      values must be length 1,
     but FUN(X[[2]]) result is length 2

其他考虑

健壮性/鲁棒性 (robustness)

  • 健壮性: 系统在执行过程中处理错误,以及算法在遭遇输入、运算等异常时继续正常运行的能力
    • 检查参数和输出的合法性
    • tryCatch捕捉错误并容错执行
rlog <- function(x) lapply(x,log); rlog(list(1:4,"a"))
Error in FUN(X[[i]], ...) : non-numeric 
    argument to mathematical function
rlog <- function(x) lapply(x, function(v) 
    tryCatch(log(v), error=function(cdt) NULL))
rlog(list(1:4, "a"))
[[1]]
[1] 0.0000000 0.6931472 1.0986123 1.3862944

[[2]]
[1] NULL

权衡防御性编程的得失利弊

    • 提高程序健壮性
    • 降低出错概率,利于维护
    • 增加编程难度
    • 增加额外开销
  • 交互模式下,往往即时发现问题并立即修改
    • 放宽检查,以求尽快获得结果
  • 脚本模式下,往往要求捕捉预见到的/意外的错误供调试
    • 收紧检查,严格控制