match.call, R语言的一个连环坑¶
有时候会需要写一些相互依赖的函数以避免代码的重复。比如说函数A调用函数B,函数B调用函数C。但不同函数的arguments 会不一样。在函数A中指定每一个函数B中所用到的参数会不够灵活。那更加灵活的方式是用...
。 当函数的参数列表的最后有...
时R 会自动把任何没有匹配的函数放进去。那获取这些参数的方式,可以用 match.call()
或者 sys.call()
。这两者的区别是,前者返回一个named list,而后者是 unnamed list。 但和python 里的 *args
和 **kwargs
的区别在于 match.call()
和 sys.call()
会匹配所有的参数而不仅仅是...
(which is not a problem)。那获取了的参数就可以任意处理,转换,最后用do.call()
传递到调用的函数中。
举个例子,下面定义了两个函数。my_sum
把3个数字相加, 而sqr_sum
则会先把没一个数值乘方再调用my_sum
相加。
my_sum = function(x, y, z){ return(x + y + z) } sqr_sum = function(...){ args = as.list(match.call())[-1] args = lapply(args, function(x) x^2) do.call(my_sum, args) } sqr_sum(x = 2, y = 3, z = 4) # [1] 29
完美。但是,如果如果sqr_sum
被另一个函数调用的时候,坑就出现了。add_sqr_sum
函数先给每一个数加上一个值,再调用sqr_sum
add_sqr_sum = function(x, y, z){ x = x + 5 y = y + 6 z = z + 7 sqr_sum(x = x, y = y, z = z) } add_sqr_sum(2,3,4) # Error in x^2 : non-numeric argument to binary operator
用traceback
会发现,错误发生在sqr_sum
里面。sqr_sum
似乎是接收到了x
,但x
并不是numeric。改写一下sqr_sum
,把我们用match.call()
匹配到的args
都打出来瞧瞧。
sqr_sum = function(...){ args = as.list(match.call())[-1] print(args) # args = lapply(args, function(x) x^2) # do.call(my_sum, args) } add_sqr_sum(2,3,4) # $x # x # # $y # y # # $z # z
貌似args
里的值,都是未被eval
过的symbol
而非numeric
。那咱试试直接在global environment 里是用 sqr_sum
。
sqr_sum(2,3,4) # $x # [1] 2 # # $y # [1] 3 # # $z # [1] 4
那很显然这一次args
里的element
都是数值了。但是如果在global environment 里,当你把一个变量传入时,同样的问题也会发生
x = 2 sqr_sum(x = x, y = 3, z = 4) # $x # x # # $y # [1] 3 # # $z # [1] 4
所以问题就在于,match.call()
这个函数,只会愚蠢地把传入的参数给match 下来。这意味着,如果传入的是numeric
,那就是numeric
,传入的是character
就是character
,而如果传入的是一个变量的话,它只会把symbol
保留,而不会在**calling environment**中去 eval
。因此解决方案其实很简单,把任何的symbol
人为地eval
。
sqr_sum = function(...){ args = as.list(match.call())[-1] call.envir = parent.frame(1) args = lapply(args, function(arg){ if(is.symbol(arg)){ eval(arg, envir = call.envir) } else { arg } }) args = lapply(args, function(x) x^2) do.call(my_sum, args) } sqr_sum(x = x, y = 3, z = 4) # 29 add_sqr_sum(2,3,4) # 251
世界又变得美好了。
在这过程当中还发现match.call
另外的一个坑。以下是文档中的usage
。
# Usage match.call(definition = sys.function(sys.parent()), call = sys.call(sys.parent()), expand.dots = TRUE, envir = parent.frame(2L))
文档声称,definition
和 call
都是使用的是使用了match.call
的这个函数的 parent enviroment。match.call
没法直接查看源代码(貌似是一个internal function)但是尝试一番之后就会发现,实际上definition
和 call
都是使用的是该函数所在的 environment 而非 parent environment。因此正确的default parameter 其实如下:
match.call(definition = sys.function(0), call = sys.call(0), expand.dots = TRUE, envir = parent.frame(2L))
以上便是match.call
的两个坑。