checkmate 2.3.1
2023-12-04
你是否曾经使用过一个 R 函数,但却产生了一条不甚有用的错误信息,经过几分钟的调试后才发现,你只是传错了一个参数?
责怪软件包作者懒惰,不做这种标准检查(在 R 语言这样的动态类型语言中),至少有一部分是不公平的,因为 R 语言让这类检查变得繁琐和恼人。过去就是这样的。
在将参数传递给函数时,几乎所有标准类型的用户错误都可以通过简单、可读的一行来捕获,并在发生错误时产生一条内容丰富的错误信息。软件包的很大一部分是用 C 语言编写的,以减少对执行时间开销的担心。
介绍
举个激励性的例子,假设你有一个计算自然数系数的函数,用户可以选择使用斯特林近似法或 R 的阶乘函数(内部使用伽马函数)。这样,就有两个参数:n 和methid。参数 n 显然必须是正的自然数,方法必须是 "斯特林 "或 "阶乘"。下面是为确保满足这些简单要求而需要通过的所有程序的版本:
fact <- function(n, method = "stirling") {
if (length(n) != 1)
stop("Argument 'n' must have length 1")
if (!is.numeric(n))
stop("Argument 'n' must be numeric")
if (is.na(n))
stop("Argument 'n' may not be NA")
if (is.double(n)) {
if (is.nan(n))
stop("Argument 'n' may not be NaN")
if (is.infinite(n))
stop("Argument 'n' must be finite")
if (abs(n - round(n, 0)) > sqrt(.Machine$double.eps))
stop("Argument 'n' must be an integerish value")
n <- as.integer(n)
}
if (n < 0)
stop("Argument 'n' must be >= 0")
if (length(method) != 1)
stop("Argument 'method' must have length 1")
if (!is.character(method) || !method %in% c("stirling", "factorial"))
stop("Argument 'method' must be either 'stirling' or 'factorial'")
if (method == "factorial")
factorial(n)
else
sqrt(2 * pi * n) * (n / exp(1))^n
}
为了便于比较,下面是使用 checkmate 的相同函数:
fact <- function(n, method = "stirling") {
assertCount(n)
assertChoice(method, c("stirling", "factorial"))
if (method == "factorial")
factorial(n)
else
sqrt(2 * pi * n) * (n / exp(1))^n
}
函数总览
这些功能可分为四个功能组,用前缀表示。如果前缀为 assert,则在相应的检查失败时会抛出错误。否则,被检查对象将以隐形方式返回。目前有许多不同的编码风格,但大多数 R 程序员都坚持使用 camelBack 或 underscore_case。因此,checkmate 提供了两种风格的所有函数:assert_count 只是 assertCount 的别名,但允许你保留自己喜欢的风格。
以 test 为前缀的函数系列总是将检查结果作为逻辑值返回。同样,您可以交替使用 test_count 和 testCount。
以 check 开头的函数以字符串形式返回错误信息(否则返回 TRUE),如果需要更多控制,例如希望对返回的错误信息进行 grep 处理,可以使用该函数。
expect 是最后一个函数系列,旨在与 testthat 软件包一起使用。所有执行的检查都会记录到 testthat 报告器中。由于 testthat 使用下划线大小写,因此扩展函数只使用下划线样式。
1标量检查
checkFlag
checkCount
checkNumber
checkInt
checkString
checkScalar
checkScalarNA
2向量检查
checkLogical
checkNumeric
checkDouble
checkInteger
checkIntegerish
checkCharacter
checkComplex
checkFactor
checkList
checkPOSIXct
checkVector
checkAtomic
checkAtomicVector
checkRaw
3属性检查
checkClass
checkMultiClass
checkNames
checkNamed (已弃用)
4复合类型检查
checkArray
checkDataFrame
checkMatrix
5内置类型检查
checkDate
checkEnvironment
checkFunction
checkFormula
checkNull
6集合检查
checkChoice
checkSubset
checkSetEqual
checkDisjunct
checkPermutation
7文档检查
checkFileExists
checkDirectoryExists
checkPathForOutput
checkAccess
8第三方数据类型检查
checkDataTable
checkR6
checkTibble
9强制整数安全检查
asCount
asInt
asInteger
10DSL参数快速检查
qassert
qassertr
11杂项
checkOS
assert
anyMissing
allMissing
anyNaN
wf
灵活性
您可以使用 assert 一次执行多项检查,如果所有检查都失败,则抛出一个断言。下面是一个例子,我们检查 x 是否属于 foo 类或 bar 类:
f <- function(x) {
assert(
checkClass(x, "foo"),
checkClass(x, "bar")
)
}
请注意,assert(, combine = "or")和 assert(, combine = "and")允许控制指定校验的逻辑组合,前者是默认设置。
懒人的参数检查
以下函数允许使用特殊格式规范的特殊语法来定义参数检查。例如,qassert(x, "I+") 断言 x 是一个至少有一个元素且没有缺失值的整数向量。这种针对特定领域的语言非常简单,只需敲几下键盘,就能涵盖各种频繁的参数检查。你可以选择自己最喜欢的。
qassert
qassertr
testthat扩展
要扩展 testthat,需要对 checkmate 软件包进行 IMPORT、DEPEND 或 SUGGEST。下面是一个最简单的示例:
library(testthat)
library(checkmate) # for testthat extensions
test_check("mypkg")
现在您已准备就绪,可以在测试中使用 30 多种新的期望值。
test_that("checkmate is a sweet extension for testthat", {
x = runif(100)
expect_numeric(x, len = 100, any.missing = FALSE, lower = 0, upper = 1)
# or, equivalent, using the lazy style:
qexpect(x, "N100[0,1]")
})
运行速度
与自己在 R 中编写繁琐的校验(例如小节开头的阶乘示例)相比,R 在对标量进行校验时有时会更快一些。这初看起来很奇怪,因为 checkmate 主要是用 C 语言编写的,速度应该相当快。然而,基础软件包中的许多函数并不是常规函数,而是基元函数。原语直接跳转到 C 代码,而checkmate程序则必须使用慢得多的 .Call 接口。因此,只使用基本函数就可以编写(非常简单的)校验,在某些情况下,其性能略优于 checkmate。但是,如果更进一步,将自定义检查封装到一个函数中,以方便重复使用,则往往会失去性能增益(见基准 1)。
对于较大的对象,趋势已经发生了转变,因为 checkmate 可以避免许多不必要的中间变量。还要注意的是,qassert/qtest/qexpect 中的 fast/lazy 实现通常更快一些,因为只需评估两个参数(对象和规则)就能确定要执行的检查集。
下面是一些(可能不具代表性的)基准测试。但也要注意,这个基准是在 knitr 内部执行的,这通常是导致测量执行时间出现异常值的原因。最好自己运行基准,以获得无偏见的结果。
基准1:断言x是一个标识
library(checkmate)
library(ggplot2)
library(microbenchmark)
x = TRUE
r = function(x, na.ok = FALSE) { stopifnot(is.logical(x), length(x) == 1, na.ok || !is.na(x)) }
cm = function(x) assertFlag(x)
cmq = function(x) qassert(x, "B1")
mb = microbenchmark(r(x), cm(x), cmq(x))
## Warning in microbenchmark(r(x), cm(x), cmq(x)): less accurate nanosecond times
## to avoid potential integer overflows
print(mb)
## Unit: nanoseconds
## expr min lq mean median uq max neval cld
## r(x) 1681 1763 15945.72 1804 1927 1399576 100 a
## cm(x) 1148 1189 5100.40 1230 1312 315946 100 a
## cmq(x) 738 779 4769.53 820 861 347393 100 a
autoplot(mb)
基准2:断言 x 是长度为 1000 的数值,没有缺失值或 NaN 值
x = runif(1000)
r = function(x) stopifnot(is.numeric(x), length(x) == 1000, all(!is.na(x) & x >= 0 & x <= 1))
cm = function(x) assertNumeric(x, len = 1000, any.missing = FALSE, lower = 0, upper = 1)
cmq = function(x) qassert(x, "N1000[0,1]")
mb = microbenchmark(r(x), cm(x), cmq(x))
print(mb)
## Unit: microseconds
## expr min lq mean median uq max neval cld
## r(x) 9.348 10.127 25.04075 10.332 10.947 1415.976 100 a
## cm(x) 3.444 3.526 9.25657 3.649 3.731 491.672 100 a
## cmq(x) 2.952 3.034 6.39272 3.075 3.157 327.631 100 a
autoplot(mb)
基准3:断言 x 是一个字符向量,没有缺失值或空字符串
x = sample(letters, 10000, replace = TRUE)
r = function(x) stopifnot(is.character(x), !any(is.na(x)), all(nchar(x) > 0))
cm = function(x) assertCharacter(x, any.missing = FALSE, min.chars = 1)
cmq = function(x) qassert(x, "S+[1,]")
mb = microbenchmark(r(x), cm(x), cmq(x))
print(mb)
## Unit: microseconds
## expr min lq mean median uq max neval cld
## r(x) 136.120 144.6890 160.46949 146.739 148.9325 1432.130 100 a
## cm(x) 124.394 124.6400 130.54482 124.845 125.2140 450.918 100 b
## cmq(x) 58.917 59.1425 64.98541 61.869 61.9920 397.372 100 c
autoplot(mb)
基准4:测试 x 是否为无缺失值的数据帧
N = 10000
x = data.frame(a = runif(N), b = sample(letters[1:5], N, replace = TRUE), c = sample(c(FALSE, TRUE), N, replace = TRUE))
r = function(x) is.data.frame(x) && !any(sapply(x, function(x) any(is.na(x))))
cm = function(x) testDataFrame(x, any.missing = FALSE)
cmq = function(x) qtest(x, "D")
mb = microbenchmark(r(x), cm(x), cmq(x))
print(mb)
## Unit: microseconds
## expr min lq mean median uq max neval cld
## r(x) 57.851 64.001 76.54167 64.3290 64.8005 1229.795 100 a
## cm(x) 22.796 23.042 27.60776 23.1855 23.2880 350.140 100 b
## cmq(x) 18.819 18.901 23.92596 18.9830 19.0650 477.240 100 b
autoplot(mb)
# checkmate tries to stop as early as possible
x$a[1] = NA
mb = microbenchmark(r(x), cm(x), cmq(x))
print(mb)
## Unit: nanoseconds
## expr min lq mean median uq max neval cld
## r(x) 47109 52377.5 52865.40 52521 52849 68634 100 a
## cm(x) 3034 3198.0 3454.25 3321 3444 14268 100 b
## cmq(x) 410 492.0 626.89 574 656 5904 100 c
autoplot(mb)
基准5:断言 x 是一个没有缺失值的递增整数序列
N = 10000
x.altrep = seq_len(N) # this is an ALTREP in R version >= 3.5.0
x.sexp = c(x.altrep) # this is a regular SEXP OTOH
r = function(x) stopifnot(is.integer(x), !any(is.na(x)), !is.unsorted(x))
cm = function(x) assertInteger(x, any.missing = FALSE, sorted = TRUE)
mb = microbenchmark(r(x.sexp), cm(x.sexp), r(x.altrep), cm(x.altrep))
print(mb)
## Unit: microseconds
## expr min lq mean median uq max neval cld
## r(x.sexp) 25.133 28.1465 38.50228 28.5155 30.2785 961.942 100 a
## cm(x.sexp) 11.029 11.1725 11.94658 11.2750 11.3980 73.800 100 b
## r(x.altrep) 27.757 30.8935 31.96606 31.2830 33.5585 40.262 100 a
## cm(x.altrep) 1.845 1.9270 6.83511 2.0500 2.1730 480.397 100 b
autoplot(mb)
Checkmate扩展
要扩展 checkmate,必须编写自定义的 check* 函数。例如,要检查正方形矩阵,可以重新使用 checkmate 的部分功能,并通过附加功能扩展检查:
checkSquareMatrix = function(x, mode = NULL) {
# check functions must return TRUE on success
# and a custom error message otherwise
res = checkMatrix(x, mode = mode)
if (!isTRUE(res))
return(res)
if (nrow(x) != ncol(x))
return("Must be square")
return(TRUE)
}
# a quick test:
X = matrix(1:9, nrow = 3)
checkSquareMatrix(X)
## [1] TRUE
checkSquareMatrix(X, mode = "character")
## [1] "Must store characters"
checkSquareMatrix(X[1:2, ])
## [1] "Must be square"
可以使用构造函数 makeAssertionFunction、makeTestFunction 和 makeExpectationFunction 创建相应的检查函数:
# For assertions:
assert_square_matrix = assertSquareMatrix = makeAssertionFunction(checkSquareMatrix)
print(assertSquareMatrix)
## function (x, mode = NULL, .var.name = checkmate::vname(x), add = NULL)
## {
## if (missing(x))
## stop(sprintf("argument \"%s\" is missing, with no default",
## .var.name))
## res = checkSquareMatrix(x, mode)
## checkmate::makeAssertion(x, res, .var.name, add)
## }
# For tests:
test_square_matrix = testSquareMatrix = makeTestFunction(checkSquareMatrix)
print(testSquareMatrix)
## function (x, mode = NULL)
## {
## isTRUE(checkSquareMatrix(x, mode))
## }
# For expectations:
expect_square_matrix = makeExpectationFunction(checkSquareMatrix)
print(expect_square_matrix)
## function (x, mode = NULL, info = NULL, label = vname(x))
## {
## if (missing(x))
## stop(sprintf("Argument '%s' is missing", label))
## res = checkSquareMatrix(x, mode)
## makeExpectation(x, res, info, label)
## }
请注意,所有附加参数 .var.name、add、info 和 label 都会自动与自定义校验函数的函数参数结合在一起。另外请注意,如果在 R 包内定义这些函数,构造函数将在构建时调用(因此不会对运行时产生负面影响)。