R语言中的!!操作符是什么意思?

5
请问,有人能解释一下我们为什么需要来自 rlang 的 !!!!!{{}} 运算符吗?我尝试了解关于引用展开的更多内容more,但没有收获。
我在 Stack 上看到了几篇关于双大括号运算符的帖子,并理解了我们在将数据框的变量(或其他对象的子对象)传递给函数时使用 {{。但是,在阅读了关于引用/取消引用的内容后,我对所有这些运算符及其用法感到非常困惑。
我们为什么需要它,为什么某些函数没有不使用它读取参数,最后,它们实际上如何工作?
如果您能用最简单的方式回答我的问题(也许附带示例),我将不胜感激。

5
为什么我们需要它们?因为tidyverse强烈依赖非标准评估。作为不使用tidyverse的人,我从未使用过其中任何一个。 - Roland
5
在基本的 R 语言中,!! 表示对 "逻辑运算符" 进行双重否定(!!! 表示三重否定)。而 rlang 和其他 tidyverse 包则采用了该符号来进行非标准评估变量的操作。 - r2evans
3
这可能是一个重复的问题,但是“!”符号使得搜索变得更加困难,因为它会让搜索引擎认为这是两个不同的词。 - Dirk Eddelbuettel
2
如果您在阅读准引用章节后仍无法理解它们,我不确定还能说什么。也许使用dplyr指南进行编程会有所帮助:https://dplyr.tidyverse.org/articles/programming.html。 - MrFlick
@MrFlick 嗯,谢谢!语言障碍是我有时无法从文档中获取信息的原因,因为它们使用非常技术化和仅面向程序员的语言编写。 - rg4s
2个回答

5
!!{{ 运算符都是占位符,用于标记一个变量已被引用。只有在使用 tidyverse 进行编程时才需要使用它们。 tidyverse 喜欢利用非标准评估(NSE)来减少重复的工作量。最常见的应用场景是对 "data.frame" 类型进行操作,其中表达式/符号在搜索其他作用域之前,在数据框的上下文中进行评估。 为了让这个过程正常运行,一些特殊函数(比如 dplyr 包中的函数)需要传入被引用的参数。引用一个表达式意味着保存组成该表达式的符号并防止其被评估(在 tidyverse 的环境中,它们使用“quosures”,它类似于一个带有引用到表达式产生环境的引用表达式)。 尽管 NSE 对于交互式使用非常方便,但编程起来难度较大。 让我们考虑 dplyr::select
 library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
 
 iris <- as_tibble(iris)
 
 my_select <- function(.data, col) {
   select(.data, col) 
 }
 
 select(iris, Species)
#> # A tibble: 150 × 1
#>    Species
#>    <fct>  
#>  1 setosa 
#>  2 setosa 
#>  3 setosa 
#>  4 setosa 
#>  5 setosa 
#>  6 setosa 
#>  7 setosa 
#>  8 setosa 
#>  9 setosa 
#> 10 setosa 
#> # … with 140 more rows
 my_select(iris, Species)
#> Error: object 'Species' not found

我们遇到了一个错误,因为在my_select的范围内, col参数是通过标准求值进行评估的, 无法找到名为Species的变量。

如果我们试图在全局环境中创建一个变量,我们会发现这个函数能够工作, 但它并不符合tidyverse的启发式策略。实际上,它们会生成一条注释来告诉您这是模棱两可的用法。

 Species <- "Sepal.Width"
 my_select(iris, Species)
#> Note: Using an external vector in selections is ambiguous.
#> ℹ Use `all_of(col)` instead of `col` to silence this message.
#> ℹ See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
#> This message is displayed once per session.
#> # A tibble: 150 × 1
#>    Sepal.Width
#>          <dbl>
#>  1         3.5
#>  2         3  
#>  3         3.2
#>  4         3.1
#>  5         3.6
#>  6         3.9
#>  7         3.4
#>  8         3.4
#>  9         2.9
#> 10         3.1
#> # … with 140 more rows

为了解决这个问题,我们需要使用enquo()来防止评估,并使用!!或简单地使用{{进行取消引用。
 my_select2 <- function(.data, col) {
   col_quo <- enquo(col)
   select(.data, !!col_quo) #attempting to find whatever symbols were passed to `col` arugment
 }
 #' `{{` enables the user to skip using the `enquo()` step.
 my_select3 <- function(.data, col) {
   select(.data, {{col}}) 
 }
 
 my_select2(iris, Species)
#> # A tibble: 150 × 1
#>    Species
#>    <fct>  
#>  1 setosa 
#>  2 setosa 
#>  3 setosa 
#>  4 setosa 
#>  5 setosa 
#>  6 setosa 
#>  7 setosa 
#>  8 setosa 
#>  9 setosa 
#> 10 setosa 
#> # … with 140 more rows
 my_select3(iris, Species)
#> # A tibble: 150 × 1
#>    Species
#>    <fct>  
#>  1 setosa 
#>  2 setosa 
#>  3 setosa 
#>  4 setosa 
#>  5 setosa 
#>  6 setosa 
#>  7 setosa 
#>  8 setosa 
#>  9 setosa 
#> 10 setosa 
#> # … with 140 more rows

总之,如果您正在尝试以编程方式应用NSE程序或对语言进行某种类型的编程,则只需要使用!!{{!!!用于将某些类型的列表/向量拼接到一些引用表达式的参数中。
 library(rlang)
 quo_let <- quo(paste(!!!LETTERS))
 quo_let
#> <quosure>
#> expr: ^paste("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L",
#>           "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y",
#>           "Z")
#> env:  global
 eval_tidy(quo_let)
#> [1] "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z"

本文创建于 2021-08-30,使用了 reprex 包(v2.0.1)


1
Justin,非常感谢你介绍NSE。我会接受你的答案,但这并不意味着它比Artem的更好。你们的回答互相补充,因此它们描绘了使用某些运算符的真正简单而全面的画面。 - rg4s

3

非标准评估(NSE)经常与tidyverse/dplyr一起使用,但大多数人在加载软件包时每天都会遇到它。

a <- "rlang"

print(a)               # Standard evaluation: the expression a is replace by its value
# [1] "rlang"

library(a)             # Non-standard evaluation: the expression a is used as-is
# Error in library(a) : there is no package called ‘a’

那么,如何加载动态指定的软件包?这里,我们将使用引用语法进行演示。(在实际代码中,我建议改为library(a, character.only=TRUE)。)

在基础 R 中,您可以使用 bquote() 动态构造一个表达式并对其求值。

myexpr <- bquote(library(.(a)))      # myexpr will now be library("rlang")
eval(myexpr)                         # rlang is now loaded

rlang 提供了额外的工具来操作表达式。通常,它们比基础 R 工具更加灵活。 !! 的行为与上述类似:

myexpr <- rlang::expr(library(!!a))  # Same as above, myexpr is now library("rlang")

您可以使用rlang::expr!!来构建任何表达式以供未来评估。

x <- rlang::expr(mtcars)
y <- rlang::expr(mpg > 30)
z <- rlang::expr(disp)
rlang::expr(subset(!!x, !!y, !!z))   # Constructs subset(mtcars, mpg > 30, disp)

当你有很多参数时,你可以把它们放在一个列表中并使用!!!快捷方式。上面的表达式可以用以下方式复制:

l <- rlang::exprs(mtcars, mpg > 30, disp)   # Note the s on exprs
rlang::expr(subset(!!!l))                   # Also builds subset(mtcars, mpg > 30, disp)

{{运算符是最难解释的一种运算符,需要先介绍quo引用。

R语言中的表达式是一等对象,这意味着它们可以作为参数传递给函数、被函数返回等等。但是,使用rlang::expr创建的表达式总是在其即时上下文中进行评估。考虑以下代码:

a <- 10
x <- rlang::expr(a+5)

f <- function(y) {
  a <- 5
  eval(y)
}

f(x)     # What does this return?

尽管表达式x捕获了a+5,但在评估表达式之前,变量a的值已发生更改。Quosure会捕捉表达式和定义它们的环境,该环境始终用于评估该表达式。
a <- 10
x <- rlang::quo(a+5)    # Quosure = expression + environment where a == 10

f <- function(y) {
  a <- 5
  eval_tidy(y)          # Instead of simple eval()
}

f(x)                    # 15 = 10 + 5

通过使用exprquoen-版本,可以将表达式或引用捕获并移动到函数内部:

f <- function(y) {
  a <- 5
  eval(rlang::enexpr(y))
}

g <- function(y) {
  a <- 5
  eval_tidy(rlang::enquo(y))
}

允许用户直接将表达式传递给函数

a <- 10
f(a*4)    # 20 = 5*4,  because f captures expressions, and a is overwritten
g(a*4)    # 40 = 10*4, because g captures quosures

综上所述,{{x}}只是!!enquo(x)的简写。


库(a,character.only = TRUE) - Roland
一些事情已经澄清了,感谢你的耐心,Артем! - rg4s
Artem,我接受了Justin的答案,但这并不意味着你的回答没有解答问题。如果我可以接受两个答案,我也会接受你的。谢谢! - rg4s
1
没问题。很高兴它能有所帮助。我想提醒大家,NSE 可能会使代码难以阅读和维护。我在此处创建了一个简单的示例进行演示,但在实际代码中,我会使用 Roland 在他的评论中写的 library() 调用。 - Artem Sokolov

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