在编写自己的函数时如何使用R的省略号功能?

How to use R's ellipsis feature when writing your own function?

R语言有一个很好的功能,用于定义可以采用可变数量参数的函数。例如,函数data.frame接受任意数量的参数,并且每个参数都成为结果数据表中列的数据。用法示例:

1
2
3
4
5
> data.frame(letters=c("a","b","c"), numbers=c(1,2,3), notes=c("do","re","mi"))
  letters numbers notes
1       a       1    do
2       b       2    re
3       c       3    mi

函数的签名包括省略号,如下所示:

1
2
3
4
5
function (..., row.names = NULL, check.rows = FALSE, check.names = TRUE,
    stringsAsFactors = default.stringsAsFactors())
{
    [FUNCTION DEFINITION HERE]
}

我想编写一个类似的函数,获取多个值并将它们合并为一个返回值(以及进行一些其他处理)。为了做到这一点,我需要弄清楚如何从函数的函数参数中"解包"...。我不知道该怎么做。 data.frame的函数定义中的相关行是object <- as.list(substitute(list(...)))[-1L],这是我无法理解的。

那么如何将省略号从函数的签名转换为例如列表呢?

更具体地说,如何在下面的代码中编写get_list_from_ellipsis

1
2
3
4
5
6
7
my_ellipsis_function(...) {
    input_list <- get_list_from_ellipsis(...)
    output_list <- lapply(X=input_list, FUN=do_something_interesting)
    return(output_list)
}

my_ellipsis_function(a=1:10,b=11:20,c=21:30)

编辑

似乎有两种可能的方法来做到这一点。它们是as.list(substitute(list(...)))[-1L]list(...)。但是,这两者并没有完全相同。 (有关差异,请参阅答案中的示例。)任何人都可以告诉我它们之间的实际区别是什么,我应该使用哪一个?


我阅读了答案和评论,我发现很少有东西没有提到:

  • data.frame使用list(...)版本。代码片段:

    1
    2
    3
    object <- as.list(substitute(list(...)))[-1L]
    mrn <- is.null(row.names)
    x <- list(...)

    object用于对列名称进行一些魔术,但x用于创建最终data.frame
    要使用未评估的...参数,请查看write.csv代码,其中使用了match.call

  • 当您在评论结果中写入Dirk时,答案不是列表。是长度为4的列表,哪些元素是language类型。第一个对象是symbol - list,第二个是表达式1:10,依此类推。这解释了为什么需要[-1L]:它从...中提供的参数中删除了预期的symbol(因为它总是一个列表)。
    由于Dirk声明substitute返回"解析树的未评估表达式"。
    当你调用my_ellipsis_function(a=1:10,b=11:20,c=21:30)然后..."创建"一个参数列表:list(a=1:10,b=11:20,c=21:30)substitute使它成为四个元素的列表:

    1
    2
    3
    4
    5
    List of 4
    $  : symbol list
    $ a: language 1:10
    $ b: language 11:20
    $ c: language 21:30

    第一个元素没有名称,这在Dirk答案中是[[1]]。我使用以下方法获得此结果

    1
    2
    3
    4
    5
    6
    my_ellipsis_function <- function(...) {
      input_list <- as.list(substitute(list(...)))
      str(input_list)
      NULL
    }
    my_ellipsis_function(a=1:10,b=11:20,c=21:30)
  • 如上所述,我们可以使用str来检查函数中的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    my_ellipsis_function <- function(...) {
        input_list <- list(...)
        output_list <- lapply(X=input_list, function(x) {str(x);summary(x)})
        return(output_list)
    }
    my_ellipsis_function(a=1:10,b=11:20,c=21:30)
     int [1:10] 1 2 3 4 5 6 7 8 9 10
     int [1:10] 11 12 13 14 15 16 17 18 19 20
     int [1:10] 21 22 23 24 25 26 27 28 29 30
    $a
       Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
       1.00    3.25    5.50    5.50    7.75   10.00
    $b
       Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
       11.0    13.2    15.5    15.5    17.8    20.0
    $c
       Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
       21.0    23.2    25.5    25.5    27.8    30.0

    没关系。让我们看看substitute版本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
       my_ellipsis_function <- function(...) {
           input_list <- as.list(substitute(list(...)))
           output_list <- lapply(X=input_list, function(x) {str(x);summary(x)})
           return(output_list)
       }
       my_ellipsis_function(a=1:10,b=11:20,c=21:30)
        symbol list
        language 1:10
        language 11:20
        language 21:30
       [[1]]
       Length  Class   Mode
            1   name   name
       $a
       Length  Class   Mode
            3   call   call
       $b
       Length  Class   Mode
            3   call   call
       $c
       Length  Class   Mode
            3   call   call

    不是我们需要的。您将需要其他技巧来处理这些类型的对象(如write.csv)。

  • 如果你想使用...那么你应该像Shane一样用list(...)来使用它。


    您可以将省略号转换为带有list()的列表,然后对其执行操作:

    1
    2
    3
    4
    5
    6
    7
    > test.func <- function(...) { lapply(list(...), class) }
    > test.func(a="b", b=1)
    $a
    [1]"character"

    $b
    [1]"numeric"

    所以你的get_list_from_ellipsis功能只不过是list

    一个有效的用例是在你想要传入未知数量的对象进行操作的情况下(如c()data.frame()的例子)。但是,当您事先知道每个参数时,使用...并不是一个好主意,因为它会给参数字符串增加一些歧义和进一步的复杂性(并使函数签名对任何其他用户都不清楚)。参数列表是功能用户的重要文档。

    否则,它对于您希望将参数传递给子函数而不将它们全部暴露在您自己的函数参数中的情况也很有用。这可以在功能文档中注明。


    只是为了补充Shane和Dirk的回答:比较有趣

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    get_list_from_ellipsis1 <- function(...)
    {
      list(...)
    }
    get_list_from_ellipsis1(a = 1:10, b = 2:20) # returns a list of integer vectors

    $a
     [1]  1  2  3  4  5  6  7  8  9 10

    $b
     [1]  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    get_list_from_ellipsis2 <- function(...)
    {
      as.list(substitute(list(...)))[-1L]
    }
    get_list_from_ellipsis2(a = 1:10, b = 2:20) # returns a list of calls

    $a
    1:10

    $b
    2:20

    就目前而言,任何一个版本在my_ellipsis_function中都适合您的用途,但第一个版本显然更简单。


    你已经给出了一半答案。考虑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    R> my_ellipsis_function <- function(...) {
    +   input_list <- as.list(substitute(list(...)))
    + }
    R> print(my_ellipsis_function(a=1:10, b=2:20))
    [[1]]
    list

    $a
    1:10

    $b
    11:20

    R>

    所以这从调用中获取了两个参数ab并将其转换为列表。那不是你要的吗?


    这按预期工作。
    以下是交互式会话:

    1
    2
    3
    4
    5
    6
    > talk <- function(func, msg, ...){
    +     func(msg, ...);
    + }
    > talk(cat, c("this","is","a","message."), sep=":")
    this:is:a:message.
    >

    相同,除了默认参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    > talk <- function(func, msg=c("Hello","World!"), ...){
    +     func(msg, ...);
    + }
    > talk(cat,sep=":")
    Hello:World!
    > talk(cat,sep=",", fill=1)
    Hello,
    World!
    >

    如您所见,如果默认值不是您在特定情况下所需的值,则可以使用此参数将"额外"参数传递给函数中的函数。