subset 是解决这个问题的基础函数。但是,与所有使用非标准评估的基本 R 函数一样,subset 不会执行完全卫生的代码扩展。所以subset() 在非全局范围内(例如在 lapply 循环中)使用时会计算错误的变量。
作为一个例子,这里我们在两个地方定义了变量var,首先在全局范围内,值为40,然后在局部范围内,值为30。这里使用local() 是为了简单起见,但是这在函数内部的行为是等效的。直观地说,我们希望subset 在评估中使用值30。然而,在执行以下代码时,我们看到使用了值 40(因此不返回任何行)。
var <- 40
local({
var <- 30
dfs <- list(mtcars, mtcars)
lapply(dfs, subset, mpg > var)
})
#> [[1]]
#> [1] mpg cyl disp hp drat wt qsec vs am gear carb
#> <0 rows> (or 0-length row.names)
#>
#> [[2]]
#> [1] mpg cyl disp hp drat wt qsec vs am gear carb
#> <0 rows> (or 0-length row.names)
这是因为subset() 中使用的parent.frame() 是lapply() 主体内的环境,而不是本地块。因为所有环境最终都从全局环境继承,所以变量var 的值是40。
通过准引用(在rlang package 中实现)的卫生变量扩展解决了这个问题。我们可以使用在所有上下文中都能正常工作的整洁评估来定义子集的变体。该代码源自base::subset.data.frame(),并与base::subset.data.frame() 的代码基本相同。
subset2 <- function (x, subset, select, drop = FALSE, ...) {
r <- if (missing(subset))
rep_len(TRUE, nrow(x))
else {
r <- rlang::eval_tidy(rlang::enquo(subset), x)
if (!is.logical(r))
stop("'subset' must be logical")
r & !is.na(r)
}
vars <- if (missing(select))
TRUE
else {
nl <- as.list(seq_along(x))
names(nl) <- names(x)
rlang::eval_tidy(rlang::enquo(select), nl)
}
x[r, vars, drop = drop]
}
此版本的子集与base::subset.data.frame() 的行为相同。
subset2(mtcars, gear > 4, disp:wt)
#> disp hp drat wt
#> Porsche 914-2 120.3 91 4.43 2.140
#> Lotus Europa 95.1 113 3.77 1.513
#> Ford Pantera L 351.0 264 4.22 3.170
#> Ferrari Dino 145.0 175 3.62 2.770
#> Maserati Bora 301.0 335 3.54 3.570
但是subset2() 没有子集的范围问题。在我们之前的示例中,值30 用于var,正如我们对词法作用域规则所期望的那样。
local({
var <- 30
dfs <- list(mtcars, mtcars)
lapply(dfs, subset2, mpg > var)
})
#> [[1]]
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Fiat 128 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1
#> Honda Civic 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2
#> Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1
#> Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.90 1 1 5 2
#>
#> [[2]]
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Fiat 128 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1
#> Honda Civic 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2
#> Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1
#> Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.90 1 1 5 2
这允许在所有上下文中稳健地使用非标准评估,而不仅仅是像以前的方法那样在顶级上下文中使用。
这使得使用非标准评估的函数更加有用。之前虽然它们很适合交互式使用,但在编写函数和包时需要使用更详细的标准评估函数。现在无需修改代码即可在所有上下文中使用相同的功能!
有关非标准评估的更多详细信息,请参阅 Lionel Henry 的 Tidy evaluation (hygienic fexprs) presentation、rlang vignette on tidy evaluation 和 programming with dplyr 小插图。