防止部分参数匹配

19

我有一个R函数:

myFunc <- function(x, base='') {
}

我现在正在扩展这个函数,允许使用任意一组额外的参数:

myFunc <- function(x, base='', ...) {
}

如何禁用对base参数的部分匹配?我不能在base=''之前放置...,因为我想保持函数的向后兼容性(通常会使用myFunction('somevalue', 'someothervalue')这样的形式来调用函数,而不会明确命名base参数)。

我尝试这样调用函数,结果出现了问题:

myFunc(x, b='foo')

我希望这意味着 base='',b='foo',但是R使用部分匹配并假定base='foo'

是否有一些代码可以插入到myFunc中,以确定传递了哪些参数名称,并仅将确切的"base"与base参数匹配,否则就将其作为...的一部分分组?


你可以尝试使用 sys.call(),并从中检查函数是如何被调用的。 - Jouni Helske
5
新的API命名为myFunc2,并保留旧的API(myFunc)不变以实现向后兼容性,但将其实现更改为myFunc2的简单包装。 - NPE
6个回答

7

这里有一个想法:

myFunc <- function(x, .BASE = '', ..., base = .BASE) {
    base
}

## Takes fully matching named arguments       
myFunc(x = "somevalue", base = "someothervalue")
# [1] "someothervalue"

## Positional matching works
myFunc("somevalue", "someothervalue")
# [1] "someothervalue"

## Partial matching _doesn't_ work, as desired
myFunc("somevalue", b="someothervalue")
# [1] ""

我在问题中已经提到,为了向后兼容,我不想将 ... 移动到 base 之前。倾向于 @Hemmo 的建议,尽管我怀疑最终可能会使用 @NPE 的建议。 - mathematical.coffee
@mathematical.coffee 我看到了,这就解决了你提出的一个向后兼容性问题(即用户将依赖位置匹配来将提供的值传递给“base”参数)。 - Josh O'Brien
哦,抱歉,没看到 .BASE 这一点!那么我会为每个可选参数做这个操作,对吧? - mathematical.coffee
@mathematical.coffee -- 是的,对于每个想要位置匹配可能性但不想进行名称部分匹配的参数,您都需要这样做。 - Josh O'Brien
这确实回答了我的问题,而且非常不错。不过我可能最终会使用 @NPE 的建议 - 它非常有道理。 - mathematical.coffee

3

最简单的方法是设置一些 R 选项。特别的,

  • warnPartialMatchArgs: 逻辑值。如果为真,在参数匹配中使用部分匹配时会发出警告。
  • warnPartialMatchAttr: 逻辑值。如果为真,从 attr 提取属性时使用部分匹配会发出警告。
  • warnPartialMatchDollar: 逻辑值。如果为真,使用 $ 进行提取时使用部分匹配会发出警告。

设置这些变量将会引发警告,您可以选择将其转换为错误。


2

刚刚想到了另一种解决方法,得到了@Hemmo的提示。

使用sys.call()可以知道如何调用myFunc(如果不需要部分参数匹配,请使用match.call):

myFunc <- function(x, base='', ...) {
    x <- sys.call() # x[[1]] is myFunc, x[[2]] is x, ...
    argnames <- names(x)
    if (is.null(x$base)) {
        # if x[[3]] has no argname then it is the 'base' argument (positional)
        base <- ifelse(argnames[3] == '', x[[3]], '')
    }
    # (the rest of the `...` is also in x, perhaps I can filter it out by
    #  comparing argnames against names(formals(myFunc)) .

}

0

可以使用sys.call()来访问调用者给出的函数参数。需要注意的是,sys.call()不会评估参数,而是给出调用的表达式。当函数被调用时,如果使用...作为参数,则会变得特别困难: sys.call()仅包含...,而不包含它们的值。但是,可以将sys.call()作为另一个函数(例如list())的参数列表进行评估。这将评估所有承诺并丢弃一些信息,但我无法看到如何在尝试规避R的内部匹配时摆脱这种情况。

一个想法是模拟严格匹配。如果作为函数中的第一个命令调用,则附加了一个帮助程序函数,可以实现这一点:

fun = function(x, base='', ...) {
  strictify()  # make matching strict

  list(x, base, ...)
}

这将过滤掉不匹配的参数:

> fun(10, b = 20)                                                                                                                                                                                   
[[1]]                                                                                                                                                                                               
[1] 10

[[2]]
[1] ""

$b
[1] 20

并且应该在大多数其他情况下也能正常工作(带或不带...,参数在...右侧,带有参数默认值)。唯一无法使用的是非标准评估,例如尝试使用substitute(arg)获取参数表达式时。

辅助函数

strictify <- function() {
  # remove argument values from the function
  # since matching already happened
  parenv <- parent.frame()  # environment of the calling function
  rm(list=ls(parenv), envir=parenv)  # clear that environment

  # get the arguments
  scall <- sys.call(-1)  # 'call' of the calling function
  callingfun <- scall[[1]]
  scall[[1]] <- quote(`list`)
  args <- eval.parent(scall, 2)  # 'args' is now a list with all arguments

  # if none of the argument are named, we need to set the
  # names() of args explicitly
  if (is.null(names(args))) {
    names(args) <- rep("", length(args))
  }

  # get the function header ('formals') of the calling function
  callfun.object <- eval.parent(callingfun, 2)
  callfun.header <- formals(callfun.object)
  # create a dummy function that just gives us a link to its environment.
  # We will use this environment to access the parameter values. We
  # are not using the parameter values directly, since the default
  # parameter evaluation of R is pretty complicated.
  # (Consider fun <- function(x=y, y=x) { x } -- fun(x=3) and
  # fun(y=3) both return 3)
  dummyfun <- call("function", callfun.header, quote(environment()))
  dummyfun <- eval(dummyfun, envir=environment(callfun.object))
  parnames <- names(callfun.header)

  # Sort out the parameters that didn't match anything
  argsplit <- split(args, names(args) %in% c("", parnames))
  matching.args <- c(list(), argsplit$`TRUE`)
  nonmatching.arg.names <- names(argsplit$`FALSE`)

  # collect all arguments that match something (or are just
  # positional) into 'parenv'. If this includes '...', it will
  # be overwritten later.
  source.env <- do.call(dummyfun, matching.args)
  for (varname in ls(source.env, all.names=TRUE)) {
    parenv[[varname]] <- source.env[[varname]]
  }

  if (!"..." %in% parnames) {
    # Check if some parameters did not match. It is possible to get
    # here if an argument only partially matches.
    if (length(nonmatching.arg.names)) {
      stop(sprintf("Nonmatching arguments: %s",
          paste(nonmatching.arg.names, collapse=", ")))
    }
  } else {
    # we manually collect all arguments that fall into '...'. This is
    # not trivial. First we look how many arguments before the '...'
    # were not matched by a named argument:
    open.args <- setdiff(parnames, names(args))
    taken.unnamed.args <- min(which(open.args == "...")) - 1
    # We throw all parameters that are unmatched into the '...', but we
    # remove the first `taken.unnamed.args` from this, since they go on
    # filling the unmatched parameters before the '...'.
    unmatched <- args[!names(args) %in% parnames]
    unmatched[which(names(unmatched) == "")[seq_len(taken.unnamed.args)]] <- NULL
    # we can just copy the '...' from a dummy environment that we create
    # here.
    dotsenv <- do.call(function(...) environment(), unmatched)
    parenv[["..."]] <- dotsenv[["..."]]
  }
}

还可以编写一个函数,将通常匹配的函数转换为严格匹配的函数,例如:

strict.fun = strictificate(fun)

但那将使用相同类型的技巧。


0

可能有点晚了,但是为了日后参考,我分享一下我的想法。

可以使用带引号的名称来避免部分匹配。在函数中,可以使用sys.call()函数来获取参数。

    > myFunc <- function(x, base="base", ...) {
+     ## get the arguments
+     ss=sys.call()
+     
+     ## positional arguments can be retrieved using numbers
+     print(paste("ss[[2]]=",ss[[2]]))
+     
+     ## named arguments, no partial matching
+     print(ss[['base']]) ## NULL
+     
+     ## named arguments, no partial matching
+     print(ss[['b']]) ## "a"
+     
+     ## regular call, partially matched
+     print(base) ## "a"
+     
+     ## because 'b' is matched to 'base', 
+     ## 'b' does not exist, cause an error
+     print(b)
+ }
> 
> myFunc(x=1,b='a')
[1] "ss[[2]]= 1"
NULL
[1] "a"
[1] "a"
Error in print(b) : object 'b' not found
> myFunc(1,base="b")
[1] "ss[[2]]= 1"
[1] "b"
NULL
[1] "b"
Error in print(b) : object 'b' not found
> myFunc(2,"c")
[1] "ss[[2]]= 2"
NULL
NULL
[1] "c"
Error in print(b) : object 'b' not found
> 

-2

这是一个非常恶心的hack,但它可能能完成工作:

myFunc <- function(x, base='', b=NULL, ba=NULL, bas=NULL, ...) {
  dots <- list(b=b, ba=ba, bas=bas, ...)
  #..
}

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