【问题标题】:R - How to extract object names from expressionR - 如何从表达式中提取对象名称
【发布时间】:2020-12-22 22:23:46
【问题描述】:

给定一个 rlang 表达式:

expr1 <- rlang::expr({
  d <- a + b
})

如何检索表达式中引用的对象的名称?

> extractObjects(expr1)
[1] "d" "a" "b"

更好的是,如何检索对象名称并按“必需”(输入)和“创建”(输出)对它们进行分类?

> extractObjects(expr1)
$created
[1] "d"

$required
[1] "a" "b"

【问题讨论】:

    标签: r expression metaprogramming rlang


    【解决方案1】:

    基本函数all.vars 这样做:

    〉all.vars(expr1)
    [1] "d" "a" "b"
    

    或者,您可以使用all.names 获取表达式中的所有名称,而不仅仅是那些不用作调用或运算符的名称:

    〉all.names(expr1)
    [1] "{"  "<-" "d"  "+"  "a"  "b"
    

    不要被误导:这个结果是正确的! 所有这些出现在表达式中,而不仅仅是abd

    但这可能不是你想要的。

    事实上,我假设您想要的内容对应于抽象语法树 (AST) 中的叶子标记 — 换句话说,除了函数调用(和运算符,它们也是函数调用)之外的所有内容。

    您的表达式的语法树如下所示:1

       {
       |
       <-
       /\
      d  +
        / \
       a   b
    

    获取这些信息意味着走 AST:

    leaf_nodes = function (expr) {
        if(is.call(expr)) {
            unlist(lapply(as.list(expr)[-1L], leaf_nodes))
        } else {
            as.character(expr)
        }
    }
    
    〉leaf_nodes(expr1)
    [1] "d" "a" "b"
    

    借助 AST 表示,我们还可以找到输入和输出:

    is_assignment = function (expr) {
        is.call(expr) && as.character(expr[[1L]]) %in% c('=', '<-', '<<-', 'assign')
    }
    
    vars_in_assign = function (expr) {
        if (is.call(expr) && identical(expr[[1L]], quote(`{`))) {
            vars_in_assign(expr[[2L]])
        } else if (is_assignment(expr)) {
            list(created = all.vars(expr[[2L]]), required = all.vars(expr[[3L]]))
        } else {
            stop('Expression is not an assignment')
        }
    }
    
     〉vars_in_assign(expr1)
    $created
    [1] "d"
    
    $required
    [1] "a" "b"
    

    请注意,此函数不能很好地处理复杂的分配(例如 d[x] &lt;- a + bf(d) &lt;- a + b 之类的东西。


    1lobstr::ast 以不同的方式显示语法树,即 as

    █─`{`
    └─█─`<-`
      ├─d
      └─█─`+`
        ├─a
        └─b

    ……但上面的表示在 R 之外更传统,我觉得它更直观。

    【讨论】:

    • 感谢康拉德! all.names 函数是我正在寻找的。它实际上有这个“函数”参数,所以你可以删除它们:&gt; all.names(expr = expr1, functions = FALSE) [1] "d" "a" "b" 这成功了,尽管输入/输出对象的分类是一个有趣的功能。
    • @StephGC 这太疯狂了,我太久没有阅读文档了,以至于我忘记了这个用法。请注意,还有all.vars(expr),与all.names(expr, functions = FALSE, unique = TRUE) 的作用相同。
    • 不错!确实让它更干净:)
    • @StephGC 好的,检查我的答案的更新。它现在还包含一个分离输入和输出的基本实现。
    【解决方案2】:

    另一种解决方法是extract the abstract symbolic tree explicitly:

    getAST <- function(ee) purrr::map_if(as.list(ee), is.call, getAST)
    
    str(getAST(expr1))
    #  List of 2
    #   $ : symbol {
    #   $ :List of 3
    #    ..$ : symbol <-
    #    ..$ : symbol d
    #    ..$ :List of 3
    #    .. ..$ : symbol +
    #    .. ..$ : symbol a
    #    .. ..$ : symbol b
    

    然后遍历 AST 找到分配:

    extractObjects <- function(ast)
    {
        ## Ensure that there is at least one node
        if( length(ast) == 0 ) stop("Provide an AST")
    
        ## If we are working with the assigment
        if( identical(ast[[1]], as.name("<-")) ) {
            ## Separate the LHS and RHS
            list(created = as.character(ast[[2]]),
                 required = sapply(unlist(ast[[3]]), as.character))
        } else {
            ## Otherwise recurse to find all assignments
            rc <- purrr::map(ast[-1], extractObjects)
    
            ## If there was only one assignment, simplify reporting
            if( length(rc) == 1 ) purrr::flatten(rc)
            else rc
        }
    }
    
    extractObjects( getAST(expr1) )
    # $created
    # [1] "d"
    #
    # $required
    # [1] "+" "a" "b"
    

    如果需要,您可以filter math operators out

    【讨论】:

    • 谢谢阿特姆!这确实适用于分类,但是对于更复杂的表达式,我认为需要更多条件:expr2 &lt;- rlang::expr({ if (a == b) { d &lt;- 5 } else if (a == c) { d &lt;- 2 } else { d &lt;- 0 } }) 然后在这种情况下,if() 中的内容也将被归类为“必需”。
    【解决方案3】:

    这是一个有趣的问题。我认为从概念上讲,在所有可能的表达式中可能不清楚输入和输出到底是什么。如果您查看所谓的抽象语法树 (AST),您可以使用 lobstr::ast() 对其进行可视化,它看起来像这样。

    所以在简单的情况下,当你总是有 LHS &lt;- operations on RHS variables 时,如果你遍历 AST,你总是会在 &lt;- 运算符之后得到 LST。如果您分配z &lt;- rlang::expr(d &lt;- a+b),那么z 的行为就像一个列表,例如,您可以执行以下操作:

    z <- rlang::expr(d <- a+b)
    
    for (i in 1:length(z)) {
      if (is.symbol(z[[i]])) {
        print(paste("Element", i, "of z:", z[[i]], "is of type", typeof(z[[i]])))
        if (grepl("[[:alnum:]]", z[[i]])) {print(paste("Seems like", z[[i]], "is a variable"))}
      } else {
        for (j in 1:length(z[[i]])){
          print(paste("Element", j, paste0("of z[[",i,"]]:"), z[[i]][[j]], "is of type", typeof(z[[i]][[j]])))
          if (grepl("[[:alnum:]]", z[[i]][[j]])) {print(paste("Seems like", z[[i]][[j]], "is a variable"))}
        }
      }
    }
    #> [1] "Element 1 of z: <- is of type symbol"
    #> [1] "Element 2 of z: d is of type symbol"
    #> [1] "Seems like d is a variable"
    #> [1] "Element 1 of z[[3]]: + is of type symbol"
    #> [1] "Element 2 of z[[3]]: a is of type symbol"
    #> [1] "Seems like a is a variable"
    #> [1] "Element 3 of z[[3]]: b is of type symbol"
    #> [1] "Seems like b is a variable"
    

    由 reprex 包 (v0.3.0) 于 2020-09-03 创建

    如您所见,这些树很快就会变得复杂和嵌套。因此,在您的示例中的简单情况下,假设变量使用字母数字表示,我们可以确定“对象”(如您所说)是什么以及运算符是什么(与 [[:alnum:]] 正则表达式不匹配)。如您所见,该类型不能用于区分对象和运算符,因为它始终是symbol(顺便说一句,下面的zlanguagez[[3]] 也是如此,这就是为什么我们可以判断z[[i]] 是否为symbol 与否,如果不是,请更深入地挖掘)。然后,您可以(后果自负)尝试将在&lt;- 之后立即出现的对象分类为“输出”,其余为“输入”,但我对此没有太大信心,尤其是对于更复杂的表达式.

    简而言之,这一切都是推测性的。

    【讨论】:

    • 在这里使用正则表达式不起作用,因为对象名称在 R 中可以是完全任意的。foo + bar #! 是一个有效的 R 名称,当被反引号包围时。
    • 嗨@KonradRudolph - 我同意,我提供的示例非常“狭窄”,我宁愿探索这个主题而不是提供一个可靠的答案,但它不适合评论。跨度>
    • 您好 Valeri,感谢您的意见。是的,我确实实现了这样的东西,但果然,它打破了包含例如if(a ==b)... 的更复杂的表达式,因为 if() 中的内容也将被归类为输入。一个有趣的问题。我在高级 R (adv-r.hadley.nz/…) 中看到了在这种情况下查找“输出”的一些技巧,也许也可以从中得出“输入”的解决方案。
    • 我认为这里的其他答案无论如何都比我的建议更强大和更有帮助,希望@KonradRudolph 的递归解决方案也能在调用嵌套在调用内部的更复杂的情况下工作。但是,在更一般的情况下,我会小心什么是输入,什么是输出。一个作业的输出可以是下一个作业的输入。更不用说你有像a &lt;- b &lt;- 1 这样的东西,事情会变得更加混乱,例如vars_in_assign 返回a 是创建的,而b 是必需的,这是有问题的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-21
    • 2014-02-13
    • 1970-01-01
    相关资源
    最近更新 更多