为什么 `substitute` 命令在多行工作,但单行无效?

8

我试图回答这个很好的问题,它涉及创建一个非标准的评估函数以对数据表对象进行分组汇总。Akrun提供了一个很棒的答案,我在此简化如下:

akrun <- function(data, var, group){
 var <- substitute(var)
 group <- substitute(group)
 data[, sum(eval(var)), by = group]
}

library(data.table)
mt = as.data.table(mtcars)
akrun(mt, cyl, mpg)
#    group    V1
# 1:     6 138.2
# 2:     4 293.3
# 3:     8 211.4

我也在考虑一个答案,几乎是相同的答案,但将substitute与其余内容内联。我的结果会导致错误:

gregor = function(data, var, group) {
  data[, sum(eval(substitute(var))), by = substitute(group)]
} 

gregor(mt, mpg, cyl)
# Error in `[.data.table`(data, , sum(eval(substitute(var))), by = substitute(group)) : 
#  'by' or 'keyby' must evaluate to vector or list of vectors 
#  (where 'list' includes data.table and data.frame which are lists, too) 

外表看起来,我的函数只是Akrun的简单替换。为什么它不起作用?


请注意,这两种替换都会引起问题,如下所示:

gregor_1 = function(data, var, group) {
  var = substitute(var)
  data[,sum(eval(var)), 
       by = substitute(group)]
} 
gregor_1(mt, mpg, cyl)
# Same error as above


gregor_2 = function(data, var, group) {
  group = substitute(group)
  data[,sum(eval(substitute(var))), 
       by = group]
} 
gregor_2(mt, mpg, cyl)
# Error in eval(substitute(var)) : object 'mpg' not found 

1
我现在没有时间(或者可能没有能力)深入研究这个问题,但是似乎在 data.table 调用中,无论你传递给 substitute 的变量是什么,它们都保持为符号(例如,如果你将其包装在 rleidas.integer 中,则警告对像我这样的新手来说更加清晰)。这是正确的吗?因此,在数据表调用内部使用 substitute 时,它不会查找函数环境吗?另外,我对所有与 data.table 相关的事情都很陌生,所以如果这已经很明显了,我很抱歉。 - Andrew
1
我认为这是一个不错的贡献。Akrun最初的建议也涉及环境,我尝试了像substitute(var, env = parent.frame(environment()))这样的版本,但没有成功。但这似乎很可能是正确的方向。 - Gregor Thomas
嘿,Gregor!(来自UW STATR的Michael)。我最近一直在考虑编写用于data.table程序的函数,并且已经开始创建使用字符向量作为参数而不是未引用符号的函数。通常情况下,使用data.table编写函数存在一些潜在问题,其中函数可能会利用基础的data.table对象通过创建临时列来避免复制整个表。这可能导致列名冲突和更改输入表的键。我想看到更多关于最佳实践的讨论。 - Michael
请查看我的答案 https://dev59.com/PLfna4cB1Zd3GeqPozyP#60344884。 - Michael
3个回答

7
substitute 的文档中,你可以了解到它是如何决定要替换什么内容的,默认情况下会搜索调用它的环境。如果你在 data.table 框架内(即在 [] 内部)调用 substitute ,它将无法找到符号,因为它们不在 data.table 评估环境中,而是在调用 [ 的环境中。
你可以“反转”函数调用的顺序,以获得想要的行为:
library(data.table)

foo <- function(dt, group, var) {
    eval(substitute(dt[, sum(var), by = group]))
}

foo(as.data.table(mtcars), cyl, mpg)
   cyl    V1
1:   6 138.2
2:   4 293.3
3:   8 211.4

3

data.table 使用非标准计算表达式(NSE)是因为需要在选择是否评估by参数之前对其进行分析/操作(例如,如果您给它一个符号,它不会对其进行评估)。

由此带来的后果是,如果需要评估该参数,则应在正确的环境中进行评估,这是函数的责任。 data.table 在数据中评估其by参数,而不是在调用环境中评估。

在大多数情况下,您不会看到问题,因为如果找不到符号,它将在父环境中进行评估,但substitute()更加敏感。

请参见下面的示例:

fun <- function(x){
  standard_eval(x)
  non_standard_eval_safe(x)
  non_standard_eval_not_safe(x)
}

standard_eval          <- function(expr) print(expr)

non_standard_eval_safe <- function(expr) {
  expr <- bquote(print(.(substitute(expr)))) # will be quote(print(x)) in our example
  eval.parent(expr)
}

non_standard_eval_not_safe <- function(expr) {
  expr <- bquote(print(.(substitute(expr))))  # will be quote(print(x)) in our example
  eval(expr)
}

standard_eval(1+1)          
#> [1] 2

non_standard_eval_safe(1+1)
#> [1] 2

non_standard_eval_not_safe(1+1)
#> [1] 2

fun(1+1)
#> [1] 2
#> [1] 2
#> Error in print(x): object 'x' not found


本示例由 reprex包 (v0.3.0) 于2020-02-20创建。


3

看起来 substitute 在数据表中的工作方式与它在其他情况下的工作方式不同,但可以使用 rlang 包中的 enexpr 代替 substitute

library(data.table)
library(rlang)

gregor_rlang = function(data, var, group) {
  data[, sum(eval(enexpr(var))), by = .(group = eval(enexpr(group)))]
} 

gregor_rlang(mt, mpg, cyl)
##    group    V1
## 1:     6 138.2
## 2:     4 293.3
## 3:     8 211.4

环境

这个问题似乎与环境有关,因为只有在我们明确给定substitute应该使用的环境中才能正常工作。

gregor_pf = function(data, val, group) {
  data[, sum(eval(substitute(val, parent.env(environment())))), 
    by = c(deparse(substitute(group)))]
} 
gregor_pf(mt, mpg, cyl)
##      cyl    V1
## 1:     6 138.2
## 2:     4 293.3
## 3:     8 211.4

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