在R函数中指定可选参数的“正确”方法


165

我对在R中使用可选参数编​​写函数的“正确”方式感兴趣。随着时间的流逝,我偶然发现了几条采用不同方法的代码,而我找不到合适的(正式)位置关于这个话题。

到目前为止,我已经编写了如下可选参数:

fooBar <- function(x,y=NULL){
  if(!is.null(y)) x <- x+y
  return(x)
}
fooBar(3) # 3
fooBar(3,1.5) # 4.5

如果仅x提供该函数,则仅返回其参数。它对NULL第二个参数使用默认值,如果该参数恰好不是NULL,则函数将两个数字相加。

或者,可以这样编写函数(其中第二个参数需要通过名称指定,但也可以unlist(z)或定义z <- sum(...)):

fooBar <- function(x,...){
  z <- list(...)
  if(!is.null(z$y)) x <- x+z$y
  return(x)
}
fooBar(3) # 3
fooBar(3,y=1.5) # 4.5

我个人更喜欢第一个版本。但是,我可以同时看到好与坏。第一个版本不太容易出错,但是第二个版本可用于合并任意数量的可选对象。

有没有一种“正确的”方法来在R中指定可选参数?到目前为止,我已经确定了第一种方法,但是两者有时都可能会感到有些“ hacky”。


查看源代码xy.coords以了解常用方法。
卡尔·威索夫特

5
Carl Witthoft l xy.coords提到的源代码可以在xy.coords中找到
RubenLaguna

Answers:


129

您还可以missing()用来测试是否y提供了参数:

fooBar <- function(x,y){
    if(missing(y)) {
        x
    } else {
        x + y
    }
}

fooBar(3,1.5)
# [1] 4.5
fooBar(3)
# [1] 3

5
我喜欢更好的错过。特别是如果您有很多NULL默认值,则在包装文档中将不会有x = NULL,y = NULL,z = NULL
rawr 2015年

5
@rawr missing()在“说出它的意思”的意义上也更具表现力。另外,它允许用户在有意义的地方传递NULL值!
Josh O'Brien

30
对我来说,以这种方式使用失误有很大的弊端:浏览函数参数时,您将不再看到哪些参数是必需的,哪些是选项。
哈德利2015年

3
@param x numeric; something something; @param y numeric; **optional** something something; @param z logical; **optional** something something
rawr 2015年

4
missing()当您想将参数从一个函数传递给另一个函数时,这很糟糕。
约翰·史密斯

55

坦白地说,我喜欢OP的第一种实际方法,即先从一个NULL值开始,然后再对其进行检查is.null(主要是因为它非常简单易懂)。这可能取决于人们习惯于编码的方式,但Hadl​​ey似乎也支持这种is.null方式:

摘自Hadley的书“ Advanced-R”第6章,函数,第84页(在线版本请点击此处):

您可以确定missing()函数是否提供了参数。

i <- function(a, b) {
  c(missing(a), missing(b))
}
i()
#> [1] TRUE TRUE
i(a = 1)
#> [1] FALSE  TRUE
i(b = 2)
#> [1]  TRUE FALSE
i(1, 2)
#> [1] FALSE FALSE

有时您想添加一个非平凡的默认值,这可能需要几行代码来计算。除了在功能定义中插入该代码之外,您还可以根据需要使用missing()有条件地对其进行计算。但是,如果不仔细阅读文档,就很难知道哪些参数是必需的,哪些参数是可选的。相反,我通常将默认值设置为NULL,并使用is.null()来检查是否提供了参数。


2
有趣。这听起来很合理,但是您是否发现自己对函数的哪些参数是必需的以及哪些是可选的感到困惑?我不确定我是否真的有过这种经历……
Josh O'Brien 2015年

2
@ JoshO'Brien我想我无论哪种编码风格都没有这个问题,至少它从来都不是主要问题,可能是因为文档或阅读源代码。这就是为什么我主要说这确实与您习惯的编码风格有关。我已经使用这种NULL方式已有相当长的一段时间了,这也许就是为什么我在看到源代码时更加习惯这种方式的原因。对我来说似乎更自然。就是说,正如您说的那样,基数R采取了两种方法,这实际上归结为个人偏好。
LyzandeR 2015年

2
到现在为止,我真的希望我可以将两个答案标记为正确,因为我真正使用这两个答案,is.nullmissing取决于上下文和参数的用途。
SimonG 2015年

5
@SimonG没关系,谢谢:)。我同意这两个答案都非常好,而且有时确实取决于上下文。这是一个非常好的问题,我相信答案会提供非常好的信息和知识,这也是这里的主要目标。
LyzandeR 2015年

24

这些是我的经验法则:

如果可以从其他参数计算出默认值,请使用默认表达式,如下所示:

fun <- function(x,levels=levels(x)){
    blah blah blah
}

否则使用失踪

fun <- function(x,levels){
    if(missing(levels)){
        [calculate levels here]
    }
    blah blah blah
}

极少数情况下,用户可能希望指定一个持续整个R会话的默认值,请使用getOption

fun <- function(x,y=getOption('fun.y','initialDefault')){# or getOption('pkg.fun.y',defaultValue)
    blah blah blah
}

如果某些参数根据第一个参数的类而适用,请使用S3泛型:

fun <- function(...)
    UseMethod(...)


fun.character <- function(x,y,z){# y and z only apply when x is character
   blah blah blah 
}

fun.numeric <- function(x,a,b){# a and b only apply when x is numeric
   blah blah blah 
}

fun.default <- function(x,m,n){# otherwise arguments m and n apply
   blah blah blah 
}

使用...仅当您在传递附加参数给另一个函数

cat0 <- function(...)
    cat(...,sep = '')

最后,如果你选择使用...,而不经过点到另一个功能,提醒你的函数被忽略任何未使用的参数,用户,因为它可以是非常纷繁复杂的:

fun <- (x,...){
    params <- list(...)
    optionalParamNames <- letters
    unusedParams <- setdiff(names(params),optionalParamNames)
    if(length(unusedParams))
        stop('unused parameters',paste(unusedParams,collapse = ', '))
   blah blah blah 
}

s3方法选项也是我想到的第一件事
原始2015年

2
回想起来,我很喜欢OP NULL在函数签名中进行赋值的方法,因为它使制作可以很好地链接的函数更加方便。
Jthorpe

10

有多种选择,尽管它们可以将不同的信息传达给计算机和其他读取您的代码的人,但它们都不是官方的正确方法,也没有真正不正确的选择。

对于给定的示例,我认为最清晰的选择是提供一个身份默认值,在这种情况下,请执行以下操作:

fooBar <- function(x, y=0) {
  x + y
}

这是到目前为止显示的最短的选项,而简短可以帮助提高可读性(有时甚至可以提高执行速度)。显然,返回的是x和y的总和,并且您可以看到y的值不会为0,当将其加到x时,只会得到x。显然,如果使用比加法复杂的东西,那么将需要不同的标识值(如果存在)。

我真正喜欢这种方法的一件事是,很清楚使用该args函数甚至查看帮助文件时的默认值是什么(您无需向下滚动到详细信息,它就在用法中)。

此方法的缺点是,当默认值很复杂(需要多行代码)时,尝试将所有内容都放入默认值可能会降低可读性,并且missingor NULL方法变得更加合理。

当参数传递给另一个函数时,或使用match.callsys.call函数时,方法之间的其他一些差异将出现。

因此,我认为“正确”的方法取决于您打算对特定参数进行的处理以及要传达给代码阅读器的信息。


7

我倾向于倾向于使用NULL,以明确要求的内容和可选的内容。警告:有关使用依赖于其他参数的默认值的警告,如Jthorpe所建议。在调用函数时不会设置该值,而是在首次引用该参数时才设置!例如:

foo <- function(x,y=length(x)){
    x <- x[1:10]
    print(y)
}
foo(1:20) 
#[1] 10

另一方面,如果在更改x之前引用y:

foo <- function(x,y=length(x)){
    print(y)
    x <- x[1:10]
}
foo(1:20) 
#[1] 20

这有点危险,因为它使得很难跟踪正在初始化的“ y”,就像在函数中未提前调用一样。


7

仅想指出,内置sink函数提供了在函数中设置参数的不同方法的良好示例:

> sink
function (file = NULL, append = FALSE, type = c("output", "message"),
    split = FALSE)
{
    type <- match.arg(type)
    if (type == "message") {
        if (is.null(file))
            file <- stderr()
        else if (!inherits(file, "connection") || !isOpen(file))
            stop("'file' must be NULL or an already open connection")
        if (split)
            stop("cannot split the message connection")
        .Internal(sink(file, FALSE, TRUE, FALSE))
    }
    else {
        closeOnExit <- FALSE
        if (is.null(file))
            file <- -1L
        else if (is.character(file)) {
            file <- file(file, ifelse(append, "a", "w"))
            closeOnExit <- TRUE
        }
        else if (!inherits(file, "connection"))
            stop("'file' must be NULL, a connection or a character string")
        .Internal(sink(file, closeOnExit, FALSE, split))
    }
}

1

这个怎么样?

fun <- function(x, ...){
  y=NULL
  parms=list(...)
  for (name in names(parms) ) {
    assign(name, parms[[name]])
  }
  print(is.null(y))
}

然后尝试:

> fun(1,y=4)
[1] FALSE
> fun(1)
[1] TRUE
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.