R语言【checkmate】总介绍

checkmate 2.3.1

Checkmate (r-project.org)

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 包内定义这些函数,构造函数将在构建时调用(因此不会对运行时产生负面影响)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ALittleHigh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值