【问题标题】:Dispatch of `rbind` and `cbind` for a `data.frame`为“data.frame”分派“rbind”和“cbind”
【发布时间】:2018-06-06 15:04:14
【问题描述】:

背景

R 函数 rbind()cbind() 的调度机制是非标准的。当参数之一是data.frame 时,我探索了编写rbind.myclass()cbind.myclass() 函数的一些可能性,但到目前为止我还没有一个令人满意的方法。这篇文章主要关注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())

这种行为有点令人惊讶,但它实际上记录在 dispatchrbind() 的部分。那里给出的建议是:

如果你想将其他对象与数据框结合起来, 可能需要先将它们强制转换为数据帧。

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

方法

警告用户

第一种可能性是警告用户,当x 是数据框时,不应调用rbind(a, 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() 没有被从.GlobalEnv 调用rbind.data.frame 欺骗,而是照常调用base 版本。

另一种策略是通过本地函数覆盖rbind(),这在S3 dispatching of `rbind` and `cbind` 中提出了建议。

rbind <- function (...) {
  if (attr(list(...)[[1]], "class") == "myclass") return(rbind.myclass(...))
  else return(base::rbind(...))
}

这非常适合发送到rbind.myclass(),因此用户现在可以为任何类型的对象x 键入rbind(a, x)

rbind(a, data.frame())

缺点是在library(mypackage) 之后我们会收到消息The following objects are masked from ‘package:base’: rbind

虽然技术上一切都按预期工作,但应该有比 base 函数覆盖更好的方法。

结论

以上替代方案都不令人满意。我已经阅读了有关使用 S4 调度的替代方案,但到目前为止,我还没有找到该想法的任何实现。有什么帮助或建议吗?

【问题讨论】:

  • AFAIK,比我更聪明的人,例如 Matt Dowle 和 Hadley Wickham 到目前为止还没有能够以优雅的方式解决这个问题。您可以在 data.table 或 tidyverse(可能是 dplyr?)包源中研究他们的解决方法。
  • 谢谢。这很有帮助。我查看了data.table 中所做的事情。当用户发出library(data.table) 包重新定义base::rbind.data.frame 以调度到内部方法。它适用于data.table,但我担心在多个包中同时这样做的原则是自找麻烦。在 FAQ 2.23 中,马特说:“如果有更好的解决方案,我们很乐意改变它。”
  • 我担心我们碰壁了。 Hadley 将此称为“无法修复”github.com/tidyverse/dplyr/issues/606#issuecomment-56529411,并且似乎在经过大量尝试后放弃了。所以这里也没有解决方案......
  • 如果你有一个实际的用例,我建议在 R-devel 邮件列表中提出这个问题。在我看来,这是一个应该在 R 中修复的设计缺陷。但不保证会得到修复。特别是如果您不提供补丁。
  • 这是一个选项,但我确信 R 以它的工作方式工作是有原因的。我提出这个问题是希望找到一个替代方案。

标签: r dispatch rbind cbind


【解决方案1】:

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

其实甚至有两种不同的方式:
1) Rmpfr 使用新的方式为 rbind()/cbind() 中的 '...' 定义方法。 这在?dotsMethods(助记符:'...' = 点)中有详细记录,并在 Rmpfr/R/array.R 第 511 行 ff 中实现(例如 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”意味着您为两个(“2”)类似矩阵的对象定义 S4 方法,并且 rbind/cbind 将它们递归地用于其可能的许多参数中的 两个。 p>

【讨论】:

  • 谢谢。这些都是有用的建议。我将研究这些选项,并尝试开发示例代码。
  • 如果您采用 cbind2/rbind2 方法(但也可能用于 '...'),您必须使用 setOldClass("mids") [或具有更多参数的版本,例如S4class = .,另见`?Methods_for_S3`——这是相对较新的(2016)顺便说一句,好的“技术”可能还没有被广泛使用......所以我们(我,JMC,......)可能是有兴趣提供帮助。
【解决方案2】:

dotsMethod 方法由 Martin Maechler 提出并在 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

【讨论】:

  • 我认为这只有在 (1) myclass 是 S4 对象时才有效; (2) 不继承自data.frame; (3) 你 setClassUnion(myclass, data.frame) 到 somthing,比如 myclass_data.frame; (4) 你定义 rbind.myclass_data.frame。我在这里可能是错的,但如果不是,我仍然坚持下面的答案。
【解决方案3】:

我不认为你能想出完全令人满意的东西。你能做的最好的就是导出rbind.myclass,这样用户就可以直接调用它而不用做mypackage:::rbind.myclass。如果你愿意,你可以叫它别的名字(dplyr 称它的版本为bind_rows),但如果你选择这样做,我会使用一个能唤起rbind 的名字,比如rbind_myclass

即使您可以让 r-core 同意更改调度行为,以便 rbind 在其第一个参数上调度,仍然会有用户希望 rbind 多个对象与一个myclass 对象不是第一个。用户还能如何发送到rbind.myclass(df, df, myclass)

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

【讨论】:

  • 同意。最好是导出rbind.myclass() 并调用rbind()。关键是如果第一个参数是myclass,而第二个或更晚的参数是data.frame,你会得到错误的答案。重命名可以避免这个问题,但缺点是用户需要处理多个名称来实现概念上相同的操作。 R 中已经有太多这样的例子了。另外,它会破坏我现有的代码。调用rbind(df, df, myclass) 调度到base::rbind.data.frame(),这是正确的。 rbind() 按设计处理多个参数,因此无需更改。
猜你喜欢
  • 2017-02-17
  • 1970-01-01
  • 1970-01-01
  • 2023-03-31
  • 2020-07-09
  • 2015-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多