为什么`vapply`比`sapply`更安全?

88
文档说明:

vapply 类似于 sapply,但它有一个预先指定的返回值类型,因此使用起来可以更安全[...]。

您能详细说明一下为什么通常更安全,可能提供例子吗?


P.S.: 我知道答案,我已经倾向于避免使用 sapply。我只是希望在SO上有一个好的答案,这样我就可以引导我的同事了。请不要回答“阅读手册”。


1
它更可预测,使代码不那么模糊,更加健壮。特别是在大型项目中,比如一个大包,这一点非常重要。 - Paul Hiemstra
1
vapply手册中FUN.VALUE的示例对于sapply用户来说非常复杂和令人生畏。 - jsta
3个回答

77

正如已经注意到的那样,vapply 做了两件事:

  • 略微提高了速度
  • 通过提供有限的返回类型检查来提高一致性。

第二点是更大的优势,因为它有助于在错误发生之前捕获错误并导致更健壮的代码。这个返回值检查可以通过使用 sapply 然后使用 stopifnot 单独完成,以确保返回值与您预期的相一致,但是 vapply 更容易(如果更有限,因为自定义错误检查代码可以检查边界内的值等)。

这里是一个使用 vapply 确保结果符合预期的示例。这类似于我刚刚在 PDF 抓取时处理的内容,其中 findD 将使用 在原始文本数据中匹配模式(例如,我会有一个被实体分割的列表,并且一个正则表达式用于在每个实体内匹配地址。偶尔,PDF 被转换成无序的形式,并且一个实体会有两个地址,这会导致问题)。

> input1 <- list( letters[1:5], letters[3:12], letters[c(5,2,4,7,1)] )
> input2 <- list( letters[1:5], letters[3:12], letters[c(2,5,4,7,15,4)] )
> findD <- function(x) x[x=="d"]
> sapply(input1, findD )
[1] "d" "d" "d"
> sapply(input2, findD )
[[1]]
[1] "d"

[[2]]
[1] "d"

[[3]]
[1] "d" "d"

> vapply(input1, findD, "" )
[1] "d" "d" "d"
> vapply(input2, findD, "" )
Error in vapply(input2, findD, "") : values must be length 1,
 but FUN(X[[3]]) result is length 2

由于 input2 的第三个元素中有两个 d,因此 vapply 会产生一个错误。但是 sapply 将输出的类从字符向量更改为列表,这可能会破坏下游代码。

正如我告诉我的学生的那样,成为程序员的一部分就是将你的思维方式从“错误很烦人”转变为“错误是我的朋友”。

零长度输入
另一个相关的问题是,如果输入长度为零,sapply将始终返回空列表,无论输入类型如何。比较:

sapply(1:5, identity)
## [1] 1 2 3 4 5
sapply(integer(), identity)
## list()    
vapply(1:5, identity, integer(1))
## [1] 1 2 3 4 5
vapply(integer(), identity, integer(1))
## integer(0)

使用 vapply 可以保证你获得特定类型的输出,因此你不需要为零长度输入编写额外检查。

基准测试

因为 vapply 已经知道应该期望结果的格式,所以它可能会更快一些。

input1.long <- rep(input1,10000)

library(microbenchmark)
m <- microbenchmark(
  sapply(input1.long, findD ),
  vapply(input1.long, findD, "" )
)
library(ggplot2)
library(taRifx) # autoplot.microbenchmark is moving to the microbenchmark package in the next release so this should be unnecessary soon
autoplot(m)

autoplot


15

vapply相比其他函数会多几个按键,但这可以帮你在后期调试时节省大量时间。如果你调用的函数可能返回不同类型的数据,一定要使用vapply

一个例子是RODBC包中的sqlQuery函数。如果执行查询时出现错误,该函数将返回一个包含消息的character向量。例如,假设你想迭代一个表名向量tnames并从每个表中选择数值列“NumCol”的最大值:

sapply(tnames, 
   function(tname) sqlQuery(cnxn, paste("SELECT MAX(NumCol) FROM", tname))[[1]])
如果所有的表名都是有效的,这将导致一个 "numeric" 向量。但是,如果其中一个表名在数据库中发生更改并且查询失败,结果将被强制转换为 "character" 模式。然而,使用带有 "FUN.VALUE=numeric(1)" 的 vapply 将在此处停止错误,并防止它在后续行中弹出 - 或更糟地,根本不弹出。

14
如果你希望结果始终是某种特定的东西,比如逻辑向量,那么使用vapply可以确保这一点,而sapply则不一定如此。
a<-vapply(NULL, is.factor, FUN.VALUE=logical(1))
b<-sapply(NULL, is.factor)

is.logical(a)
is.logical(b)

4
在这种情况下,我认为最明显的做法是使用 logical(1),因为 FALSE 看起来更像是将选项设置为“关闭”而不是指定类型。 - flying sheep

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接