为`data.frame`发送`rbind`和`cbind`

时间:2017-12-25 09:12:36

标签: r dispatch rbind cbind

背景

R函数rbind()cbind()的调度机制是非标准的。当其中一个参数是rbind.myclass()时,我探讨了编写cbind.myclass()data.frame函数的一些可能性,但到目前为止我还没有一个令人满意的方法。此帖子专注于rbind,但同样适用于cbind

问题

让我们创建一个rbind.myclass()函数,只需在调用它时回显。

rbind.myclass <- function(...) "hello from rbind.myclass"

我们创建了一个类myclass的对象,并且以下调用了所有rbind 正确发送到rbind.myclass()

a <- "abc"
class(a) <- "myclass"
rbind(a, a)
rbind(a, "d")
rbind(a, 1)
rbind(a, list())
rbind(a, matrix())

但是,当其中一个参数(不一定是第一个参数)时,rbind()会调用base::rbind.data.frame()代替:

rbind(a, data.frame())

这种行为有点令人惊讶,但它实际上是在文档中记录的 dispatch的{​​{1}}部分。给出的建议是:

  

如果要将其他对象与数据框合并,   可能有必要先将它们强制转换为数据框。

在实践中,这个建议可能难以实施。转换为数据框可能会删除基本类信息。此外,在发出命令rbind()后,可能不了解建议的用户可能会遇到错误或意外结果。

途径

警告用户

第一种可能性是警告用户当rbind(a, x)是数据帧时不应该调用rbind(a, x)。相反,包x的用户应该显式调用隐藏函数:

mypackage

这可以完成,但用户必须记住在需要时进行显式调用。调用隐藏函数是最后的手段,不应该是常规策略。

拦截mypackage:::rbind.myclass(a, x)

或者,我试图通过拦截调度来保护用户。我的第一次尝试是提供rbind的本地定义:

base::rbind.data.frame()

此操作失败,因为rbind.data.frame <- function(...) "hello from my rbind.data.frame" rbind(a, data.frame()) rm(rbind.data.frame) rbind()调用rbind.data.frame时未被愚弄,并照常调用.GlobalEnv版本。

另一种策略是通过S3 dispatching of `rbind` and `cbind`中建议的本地函数覆盖base

rbind()

这适用于发送到rbind <- function (...) { if (attr(list(...)[[1]], "class") == "myclass") return(rbind.myclass(...)) else return(base::rbind(...)) } ,因此用户现在可以为任何类型的对象rbind.myclass()键入rbind(a, x)

x

缺点是在rbind(a, data.frame()) 之后我们收到消息library(mypackage)

虽然技术上一切都按预期工作,但应该有比The following objects are masked from ‘package:base’: rbind函数覆盖更好的方法。

结论

上述替代方案均不令人满意。我已经阅读了有关使用S4调度的替代方案,但到目前为止我还没有找到任何实现这个想法。任何帮助或指示?

3 个答案:

答案 0 :(得分:5)

正如你自己提到的那样,使用S4将是一个很好的解决方案。我最近没有调查数据帧,因为我对其他广义矩阵更感兴趣,我的长期CRAN包'Matrix'(=“推荐”,即每个R分布的一部分)和'Rmpfr'。

实际上甚至有两种不同的方式:
1)Rmpfr使用新方法在rbind()/ cbind()中定义'...'的方法。    这在?dotsMethods(助记符:'...'=点)中有详细记载,并在Rmpfr / R / array.R第511行中实现(例如https://r-forge.r-project.org/scm/viewvc.php/pkg/R/array.R?view=annotate&root=rmpfr

2)Matrix通过为rbind2()和cbind2()定义(S4)方法来使用旧方法:如果你阅读?rbind,它确实提到了这个,并且当使用rbind2 / cbind2时。其中的想法:“2”意味着您定义了S4方法,其中包含两个(“2”)矩阵类对象的签名,rbind / cbind将它们用于递归的可能多个参数的两个

答案 1 :(得分:2)

Martin Maechler提出了dotsMethod方法,并在Rmpfr包中实施。我们需要使用S4定义一个新的泛型,类和方法。

setGeneric("rbind", signature = "...")
mychar <- setClass("myclass", slots = c(x = "character"))
b <- mychar(x = "b")
rbind.myclass <- function(...) "hello from rbind.myclass"
setMethod("rbind", "myclass",
      function(..., deparse.level = 1) {
        args <- list(...)
        if(all(vapply(args, is.atomic, NA)))
          return( base::cbind(..., deparse.level = deparse.level) )
        else
          return( rbind.myclass(..., deparse.level = deparse.level))
      })

# these work as expected
rbind(b, "d")
rbind(b, b)
rbind(b, matrix())

# this fails in R 3.4.3
rbind(b, data.frame())

Error in rbind2(..1, r) :
    no method for coercing this S4 class to a vector

我无法解决错误。看到 R: Shouldn't generic methods work internally within a package without it being attached? 对于相关问题。

由于此方法会覆盖rbind(),因此我们会收到警告The following objects are masked from 'package:base': rbind

答案 2 :(得分:1)

我不认为你能够想出一些完全令人满意的东西。您可以做的最好的事情是导出rbind.myclass,以便用户可以直接调用它而无需执行mypackage:::rbind.myclass。如果您愿意,可以将其称为其他内容(dplyr调用其版本bind_rows),但如果您选择这样做,我会使用唤起rbind的名称,例如rbind_myclass

即使您可以让r-core同意更改调度行为,以便rbind调度其第一个参数,仍然会出现用户希望rbind多个的情况对象与myclass对象一起在除第一个之外的某个地方。用户如何分发到rbind.myclass(df, df, myclass)

data.table解决方案似乎很危险;如果CRAN维护者在某个时候进行检查并且不允许这样做,我不会感到惊讶。