Menu

R 常见报错及调试方法

R 经典报错信息的解读指南:object not found、could not find function、non-numeric argument 等,外加 tryCatch、traceback() 和朴实好用的打印调试。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

读懂 R 的错误信息

R 的错误信息分为两部分,两部分都有用:

Error in "10" + 5 : non-numeric argument to binary operator

Error in 后面是调用,也就是出错的那段代码("10" + 5)。冒号后面是条件,说明出了什么问题(non-numeric argument to binary operator)。先看调用:它告诉你问题在哪里,而且很多时候你在引用的代码里就能直接看出问题。再看条件,了解原因。

调试快的人和调试痛苦的人,差别在于两个习惯。第一,认真读错误信息:R 的信息通常很精确,只是措辞简短。第二,调试第一个错误,而不是最后一个:脚本前面的一处失败会引发后面一连串 “object not found” 错误,修好最初那个,它们就全都消失了。本页剩下的部分先解读你最常遇到的错误信息,然后介绍光靠读信息不够时可用的工具。

名称错误:not found

Error: object 'total' not found:R 搜索了它知道的所有环境,没有变量叫这个名字。三种原因几乎涵盖了所有情况:

  • 拼写错误,包括大小写。 R 区分大小写:Total、total 和 TOTAL 是三个不同的名字,R 不会猜你指的是哪个。
  • 定义它的那一行还没运行。 你在脚本里写了 total <- sum(x),但在本次会话中从没执行过。这在重启 R 之后很常见:脚本文件里还有这一行,但会话从来没见过它。从头运行脚本。
  • 环境不对。 在函数内部创建的变量只在那次调用中存在。在函数外部使用它,等于是在要一个已经不存在的东西,应该把这个值返回出来。

Error: could not find function "read_excel":道理相同,只不过换成了函数名。十有八九,这个函数在一个你已经安装、但在本次会话中没有加载的包里:

library(readxl)          # the fix: loading is per-session, installing is per-machine
df <- read_excel("data.xlsx")

如果 library(readxl) 本身报错,说明这个包没有安装,先运行 install.packages("readxl")。如果这个函数是 base R 自带的,那就是你拼错了(lenght() 几乎是每个人的必经之路)。

语法错误:unexpected symbol

Error: unexpected symbol in "..."(以及它的近亲 unexpected ')'、unexpected string constant)表示 R 连代码都解析不了。错误信息指向 R 发现问题的位置,而这个位置往往在真正出错的地方之后。常见的原因:

mean(x na.rm = TRUE)        # missing comma - should be mean(x, na.rm = TRUE)

name <- "Ada                # unclosed quote - swallows the following lines
total <- sum(c(1, 2, 3)     # unclosed paren - the error fires lines later

当被标出的那一行看起来没问题时,错误几乎总是在它上面:文件中更早的地方有一个没闭合的引号、括号或花括号。能高亮配对括号的代码编辑器几秒钟就能找到它们。

类型和索引错误解读

non-numeric argument to binary operator:你对一个不是数字的东西做了数学运算,通常是一个以文本形式到来的数字(导入数据是典型来源:一列中只要混进一个单词,整列就会被读成 character,详见数据类型)。出错的版本:

x <- "10"
x + 5
# Error in x + 5 : non-numeric argument to binary operator

修正方法是先转换,再计算:

subscript out of bounds:你用 [[ ]] 去取第 n 个位置,但对象的元素少于 n 个:

scores <- list(ada = 92, grace = 88)
scores[[3]]
# Error in scores[[3]] : subscript out of bounds

索引前先检查 length(),或者更好的做法是按名称取(scores[["grace"]]),这样顺序变了也不会出问题。注意两者的不对称:单层方括号更宽容,对向量用 [ ] 越界取值会悄悄返回 NA 而不是报错,这是把一个明显的 bug 换成了一个隐蔽的 bug。

$ operator is invalid for atomic vectors:$ 是列表和数据框专用的。对具名向量要用方括号:

([[ ]] 返回单纯的值;[ ] 会保留名称。)这个错误往往意味着上游某处返回了一个向量,而你以为它是数据框。应该去检查这个假设,而不只是换个运算符。

argument is of length zero:if () 收到了一个什么都没有的条件,几乎总是从缺失的列表元素或什么都没返回的函数里混进来的 NULL:

threshold <- NULL
if (threshold > 5) print("big")
# Error in if (threshold > 5) print("big") : argument is of length zero

给判断加上保护。注意 && 一旦知道答案就会停止求值,所以比较永远不会在 NULL 上运行:

(一个相关的错误 missing value where TRUE/FALSE needed,是同样的问题,只是把 NULL 换成了 NA,对应的保护是 is.na(),详见缺失值。)

replacement has length zero:同一种毛病的赋值版本:x[2] <- numeric(0) 试图用零个值填充一个位置。产生右侧值的那段代码返回了空结果,要调试的是它,而不是这次赋值。

警告不是错误,危险就在这里

错误会停止执行,警告不会。R 会完成计算,交给你一个结果,然后再提出它的疑虑。这个结果有时没问题,有时却悄悄出了错:

两行都执行完了。第一行把较短的向量循环使用,并警告 longer object length is not a multiple of shorter object length,而把长度为 2 的向量循环套用到长度为 3 的向量上,几乎从来不是任何人的本意。第二行警告 NAs introduced by coercion,得到的向量中间缺了一个值,这会让后续每个 mean() 都返回 NA。把这两种警告都当作需要调查的 bug,而不是可以略过的噪音。在脚本中,你可以用 options(warn = 2) 强制执行这一原则,它会把每个警告都升级为错误,让什么都溜不过去。

用 tryCatch() 处理失败

有时错误是预料之中的,比如几百个文件中有一个损坏了,或者有一行数据有问题,你希望处理掉它然后继续,而不是让脚本中断。tryCatch() 用处理函数把有风险的表达式包起来:

工作原理:如果主代码块成功,它的值就是结果。如果出错,就改为运行 error = 处理函数,而处理函数的返回值(这里是 NA)成为结果,脚本继续运行。conditionMessage(e) 可以取回原始错误信息用于记录日志。finally = 无论成功还是失败都会运行,关闭连接这类清理工作就该放在这里,在上面的例子中你可以看到它在每个结果之前都打印了一次。

还有一个 warning = 处理函数:tryCatch(as.numeric(x), warning = function(w) NA) 会捕获上一节中的强制转换警告,而不是放任它过去。有一点要提醒:一个返回备用值却不记录任何信息的处理函数,是在隐藏失败,而不是处理失败。一定要记录 conditionMessage(),将来的你会需要它。

定位失败:traceback()、browser() 和朴实的打印

当错误来自层层嵌套的函数调用深处时,仅凭错误信息看不出是哪条调用链把你带到了那里。出错后立刻运行 traceback():

f <- function(x) g(x)
g <- function(x) stop("boom")

f(1)
# Error in g(x) : boom
traceback()
# 2: g(x)
# 1: f(1)

它会打印出错那一刻的调用栈:一端是你的调用,另一端是出错的调用。它必须是你接下来运行的第一个命令;一旦发生另一个错误,调用栈就会被丢弃。

想实时查看,可以用 browser():它会在你放置它的地方暂停执行,让你进入函数内部的交互式提示符,在那里检查变量,用 n 单步执行,用 c 继续,用 Q 退出。debug(f) 不用改代码就能做到同样的事:它给 f 做个标记,让它下次被调用时在 browser 中打开(用 undebug(f) 撤销)。

还有一种没人会写进演讲幻灯片、但人人都在用的技巧:打印。在关键位置撒上一些 print() 或 cat(),运行,看看实际情况从哪里开始和你的预期不符。这种做法完全正当,速度快,在脚本中往往是最实用的工具。它最好的搭档是 str(),它能回答大概一半 R 错误背后的那个问题:“这个对象到底是什么?”:

一份紧凑的读数:它是一个列表,有两个元素,一个是整数向量,一个是带有这些列和类型的数据框。当 $ 失败或数学运算行为异常时,先对这个对象运行 str(),再去推测原因,答案通常就在眼前(“……哦,原来它是一个长度为 1 的列表,里面装着我的数据框”)。

要点总结

  • 读错误信息:Error in 后面的部分说明在哪里,冒号后面的部分说明为什么。修第一个错误,而不是最显眼的那个。
  • object not found 意味着拼写错误、代码还没运行,或者变量只存在于某个函数内部。could not find function 几乎总是少了 library() 调用。
  • unexpected symbol 表示语法无法解析,而真正的错误往往是被标出位置之前没闭合的引号或括号。
  • 经典的类型和索引错误(non-numeric argument、subscript out of bounds、$ on atomic vectors、argument is of length zero)都指向你对对象的某个错误假设;str() 一次调用就能验证这个假设。
  • 警告不会停止执行,正因如此才值得重视:结果可能悄悄出了错。
  • tryCatch(error =, warning =, finally =) 处理预料之中的失败而不中断脚本(一定要记录 conditionMessage());出错后立刻运行 traceback() 可以看到调用链;browser()/debug() 在函数内部暂停;print() 调试是踏实的好办法。

下一篇:许多“错误”其实根本不是错误,而是源自一个只有两个字母的值:NA,以及缺失值如何贯穿 R 的每一次计算。

常见问题

R 中 "object 'x' not found" 是什么意思?

R 查找名为 x 的变量,但在它搜索的所有环境中都没有这个名字。原因按可能性排序:名字拼错了(R 区分大小写,Total 不等于 total);创建 x 的那一行在本次会话中还没运行;或者 x 是在函数内部创建的,而你想在函数外部使用它。

R 中 "could not find function" 是什么意思?

这个函数在某个包里,而你在本次会话中还没有加载这个包。安装包每台电脑只需一次;library() 每次会话都要运行一次,忘记调用 library() 是最常见的原因。如果 library() 本身报错,说明这个包没有安装。函数名拼错也会产生同样的错误。

在 R 中怎么用 tryCatch 处理错误?

把有风险的表达式包起来:tryCatch(expr, error = function(e) fallback, warning = function(w) fallback, finally = cleanup)。如果 expr 失败,就运行对应的处理函数,脚本不会中断,处理函数的返回值就是结果。在处理函数里用 conditionMessage(e) 可以拿到原始错误信息,方便记录日志。

R 中 traceback() 有什么作用?

在出错后立刻运行 traceback(),它会打印出错时正在执行的函数调用链:一端是你的调用,另一端是出错的调用。它不会修复任何东西,但会告诉你该去哪里找问题;当错误来自层层嵌套的函数深处时,找到位置就解决了大半。

Coddy 编程语言插图

和 Coddy 一起学编程

开始