将一个函数传递给一个向量或未定义数量的参数。

5

我希望能够通过 ... 传递一个未定义数量的参数给函数,但同时也可以传递一个 vector。以下是一个愚蠢的例子:

library(tidyverse)
df <- data.frame(gear = as.character(unique(mtcars$gear)),
                 id = 1:3)
myfun <- function(...) {
  ids_lst <- lst(...)
  df2 <- bind_rows(map(ids_lst, function(x) 
    mtcars %>% 
      filter(gear == x) %>% 
      select(mpg)), .id = "gear") %>% 
    left_join(df)
  df2
}
#these all work:
myfun(3)
myfun(3, 4)
myfun(3, 4, 5)

不过将其传递给向量是行不通的:

myvector <- unique(mtcars$gear)
myfun(myvector)

问题出在该函数收集参数和返回结果的方式上:
myfun_lst <- function(...) {
  ids_lst <- lst(...)
  ids_lst
}
myfun_lst(3, 4, 5)
# $`3`
# [1] 3

# $`4`
# [1] 4

# $`5`
# [1] 5

myfun_lst(myvector)
# $myvector
# [1] 4 3 5

我认为修复方法就是测试输入是否为向量,可以像这样:

myfun_final <- function(...) {
  if(is.vector(...) & !is.list(...)) {
    ids_lst <- as.list(...)
    names(ids_lst) <- (...)
  } else { 
    ids_lst <- lst(...)
  }
  df2 <- bind_rows(map(ids_lst, function(x) 
    mtcars %>% 
      filter(gear == x) %>% 
      select(mpg)), .id = "gear") %>% 
    left_join(df)
  df2
}

现在,向函数传递一个向量是可行的,但收集参数却不行:
myfun_final(3, 4, 5)
myfun_final(myvector)

有什么好的方法来解决这个问题吗? 谢谢


你尝试过使用do.call(myfun_final, myvector)exec(myfun_final, myvector)吗? - ekoam
myfun_final(myvector) 已经可以工作。myfun_final(3, 4, 5) 不能工作? - user63230
1
那么情况是这样的,你会将一些标量或单个向量传递给函数,对吗? - ekoam
这就是了,我原以为if语句能够确定提供的是哪一个,但事实并非如此! - user63230
2个回答

4

当然,你可以修改你的函数,使得它能够同时适用于常规参数myfun(3, 4, 5)和向量myfun(myvector),如上面的答案所示。

另一种选择是使用感叹号三次!!!运算符进行参数展开。这个运算符只支持某些{rlang}和{tidyverse}函数。在你的例子中,你在purrr::map中评估了点...,该函数支持参数展开。因此,可能无需重新编写你的函数:

library(tidyverse)

# your original function:
myfun <- function(...) {
        ids_lst <- lst(...)
        df2 <- bind_rows(map(ids_lst, function(x) 
                mtcars %>% 
                        filter(gear == x) %>% 
                        select(mpg)), .id = "gear") %>% 
                left_join(df)
        df2
}

myvector <- unique(mtcars$gear)

myfun(!!! myvector) # works

#> Joining, by = "gear"
#>    gear  mpg id
#> 1     4 21.0  1
#> 2     4 21.0  1
#> 3     4 22.8  1
#> 4     4 24.4  1
#> 5     4 22.8  1
#> 6     4 19.2  1
#> 7     4 17.8  1
#> 8     4 32.4  1
#> 9     4 30.4  1
#> 10    4 33.9  1
#> ...


myfun(3, 4, 5) # works

#> Joining, by = "gear"
#>    gear  mpg id
#> 1     3 21.4  2
#> 2     3 18.7  2
#> 3     3 18.1  2
#> 4     3 14.3  2
#> 5     3 16.4  2
#> 6     3 17.3  2
#> 7     3 15.2  2
#> 8     3 10.4  2
#> 9     3 10.4  2
#> 10    3 14.7  2
#> ...

本文由reprex包(v0.3.0)于2021-12-30创建

你可以在这里了解有关bang bang bang操作符的抽取引用更多信息。

最后,你应该考虑你函数的用户。如果你是唯一的用户,那么选择适合自己的内容即可。如果有其他用户,则应该考虑他们希望功能如何工作。也许用户不希望函数同时使用几个参数,或者通过提供这些参数的向量来实现。在tidyverse中,使用!!!进行参数拼接是一个良好的被接受的概念。在基本R中,我们通常会使用do.call("myfun", as.list(myvector))来实现类似的效果。


添加另一种选择:

purrr包有一系列的lift函数,可用于修改函数所接受的参数类型。其中最显著的是lift_dl,它将以点为参数的函数转换为以列表或向量为参数的函数。这也可以用来解决问题,而无需重写函数:

lift_dl(myfun)(myvector)

#> Joining, by = "gear"
#>    gear  mpg id
#> 1     4 21.0  1
#> 2     4 21.0  1
#> 3     4 22.8  1
#> 4     4 24.4  1
#> 5     4 22.8  1
#> 6     4 19.2  1
#> 7     4 17.8  1
#> 8     4 32.4  1
#> 9     4 30.4  1
#> 10    4 33.9  1
#> ...

本文档创建于2022-01-01,使用了 reprex 包 (v0.3.0)


3

试试测试...长度是否为1,且传递的唯一参数是向量?如果不是,则将...视为标量列表,并使用lst(...)进行捕获。

myfun_final <- function(...) {
  if (...length() == 1L && is.vector(..1))
    ids_lst <- `names<-`(..1, ..1)
  else
    ids_lst <- lst(...)
  
  df2 <- bind_rows(map(ids_lst, function(x) 
    mtcars %>% 
      filter(gear == x) %>% 
      select(mpg)), .id = "gear") %>% 
    left_join(df)
  df2
}

测试

> myfun_final(3)
Joining, by = "gear"
   gear  mpg id
1     3 21.4  2
2     3 18.7  2
3     3 18.1  2
4     3 14.3  2
5     3 16.4  2
6     3 17.3  2
7     3 15.2  2
8     3 10.4  2
9     3 10.4  2
10    3 14.7  2
11    3 21.5  2
12    3 15.5  2
13    3 15.2  2
14    3 13.3  2
15    3 19.2  2
> myfun_final(3,4,5)
Joining, by = "gear"
   gear  mpg id
1     3 21.4  2
2     3 18.7  2
3     3 18.1  2
4     3 14.3  2
5     3 16.4  2
6     3 17.3  2
7     3 15.2  2
8     3 10.4  2
9     3 10.4  2
10    3 14.7  2
11    3 21.5  2
12    3 15.5  2
13    3 15.2  2
14    3 13.3  2
15    3 19.2  2
16    4 21.0  1
17    4 21.0  1
18    4 22.8  1
19    4 24.4  1
20    4 22.8  1
21    4 19.2  1
22    4 17.8  1
23    4 32.4  1
24    4 30.4  1
25    4 33.9  1
26    4 27.3  1
27    4 21.4  1
28    5 26.0  3
29    5 30.4  3
30    5 15.8  3
31    5 19.7  3
32    5 15.0  3
> myfun_final(c(3,4,5))
Joining, by = "gear"
   gear  mpg id
1     3 21.4  2
2     3 18.7  2
3     3 18.1  2
4     3 14.3  2
5     3 16.4  2
6     3 17.3  2
7     3 15.2  2
8     3 10.4  2
9     3 10.4  2
10    3 14.7  2
11    3 21.5  2
12    3 15.5  2
13    3 15.2  2
14    3 13.3  2
15    3 19.2  2
16    4 21.0  1
17    4 21.0  1
18    4 22.8  1
19    4 24.4  1
20    4 22.8  1
21    4 19.2  1
22    4 17.8  1
23    4 32.4  1
24    4 30.4  1
25    4 33.9  1
26    4 27.3  1
27    4 21.4  1
28    5 26.0  3
29    5 30.4  3
30    5 15.8  3
31    5 19.7  3
32    5 15.0  3

谢谢,看起来可以工作。我能问一下 ...length() == 1L && is.vector(..1) 吗?什么时候在 length() 前面加上 ...,我通常看到它写成 length(...)?同样,为什么是 ..1 而不是 ...?最后,我们需要 & !is.list(...) 吗?因为对于列表的 is.vector 将返回 TRUE,请参见这里 - user63230
1
@user63230 首先回答你的最后一个问题:这取决于情况。就我个人而言,在这种情况下,我认为传递 c(1,2,3)list(1,2,3) 没有区别,因此您不必避免后者。但是,如果您确实想确保通过的参数不是列表,请添加 && !is.list(..1)。对于您的前两个问题,..1..length()(两个点,不是三个)是用于访问 ... 中的内容的特殊对象。请参见此链接 - ekoam
1
@user63230 一个小问题:您可以考虑测试一些不同于is.vector(..1)的东西。在这种情况下,gears是数字型的,但您将允许类型为字符原始列表表达式的向量,并且禁止具有除names之外的属性的数字向量,例如 structure(1:3, a = NA)。根据您实际使用的情况,is.numeric(..1)is.atomic(..1) && !is.object(..1)可能更适合进行测试。 - Mikael Jagan

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