当您认为 `class(.) == *` 时,再想想!



历史遗留:R matrix 不是 array

在最近对 R-devel 邮件列表的讨论中,在 7 月 8 日开始的一个主题中,head.matrix 可以返回 1000 列 - 限制为 n 或添加新参数?迈克尔·奇里科和加布·贝克尔提出将 head()tail() 实用程序函数泛化,加布指出当前(R-4.x.y 之前的)head() 不会特殊处理 array我回复道,指出R 目前通常需要 matrixarray 方法

但请注意以下历史怪癖

sapply(setNames(,1:5),
       function(K) inherits(array(7, dim=1:K), "array"))

((我希望这会改变,我明确放入了当前 R 3.x.y 结果,而不是评估上述 R 块:))

     1     2     3     4     5
  TRUE FALSE  TRUE  TRUE  TRUE

请注意,matrix 对象在(继承)意义上不是 array,尽管 - 许多用户可能不知道 -

identical(
    matrix(47, 2,3), # NB  " n, n+1 " is slightly special
    array (47, 2:3))
## [1] TRUE

所有矩阵都可以通过 array(.) 等效地构造,尽管在 matrix(*, byrow=TRUE) 的情况下略显笨拙。

请注意,正因为如此,基本 R 本身有三个函数,其中 matrixarray 方法是相同的,正如我在帖子中所写:因此,目前,“通常”foo.matrix 只是 foo.array 的副本,如果后者存在,其中 foo 的base 示例为 {unique, duplicated, anyDuplicated}。

for(e in expression(unique, duplicated, anyDuplicated)) { # `e` is a `symbol`
    f.m <- get(paste(e, "matrix", sep="."))
    f.a <- get(paste(e, "array",  sep="."))
    stopifnot(is.function(f.m),
              identical(f.m, f.a))
}

在 R 4.0.0 中,matrix() 会是 "array" 吗?

在同一篇文章中,我还问过

这是我们应该考虑在 R 4.0.0 中更改的内容 - 让它对 2d 数组也为 TRUE,即矩阵对象?

与此同时,我试探性地回答了我自己的问题“是”,并开始调查一些后果。从我发现的内容来看,在过于热心的(单元)测试中,甚至是我自己编写的,我被提醒我想教更多的人了解一个潜在的相关问题,在这个问题中,我们看到许多不安全的使用者不安全地使用 R

如果您认为 class(.) == *,请三思:           相反,inherits(., *) …. 或 is(., *)

大多数非初学者 R 用户都了解类之间的继承,更普遍地说,R 对象在概念上不止一种“类型”。例如,pi 既是 "numeric" 又是 "double",或者 1:2 既是 integer 又是 numeric。他们可能知道时间日期对象有两种形式:?DateTimeClasses (或 ?POSIXt) 帮助页面描述了 POSIXctPOSIXlt,并说

"POSIXct" 更方便地包含在数据框中,而 "POSIXlt" 更接近于人类可读形式。存在一个虚拟类 "POSIXt",两个类都继承自该类...

例如

class(tm <- Sys.time())
## [1] "POSIXct" "POSIXt"

显示 class(.) 在此处的长度为 2,这会中断 if(class(x) == "....") .. 调用。

正式类:S4

R 的正式类系统称为S4(主要在标准 R 软件包 methods 中实现),提供功能和工具来实现丰富的类继承结构,在 软件包 MatrixBioconductor 项目 中得到大量使用,该项目有 1800 多个 R “软件”软件包。Bioconductor 甚至建立在提供大量使用的 S4 类的核心软件包之上,例如 BiostringsS4VectorsXVectorIRangesGenomicRanges。另请参见 常见的 Bioconductor 方法和类

在正式的 S4 类系统中,扩展和继承非常重要且经常被广泛使用,因此这样的表达式

if (class(obj) == "matrix")  { ..... }   # *bad* - do not copy !

特别无用,因为 obj 很可能是扩展矩阵的类,而使用 S4 的程序员很早就学会了使用

if (is(obj, "matrix"))  { ..... }        # *good* !!!

请注意,Bioconductor 软件包开发人员指南警告不要滥用 class(.) == *,请参见 R 代码和最佳实践 一节

非正式“经典”类:S3

R 被创建为 S 的方言或实现,请参见 维基百科的 R 历史,对于 S,“白皮书”(Chambers & Hastie,1992)引入了一种方便且相对简单的面向对象 (OO),后来被称为S3,因为白皮书引入了S 版本 3(蓝皮书描述了S 版本 2,绿皮书描述了S 版本 4,即 S4)。

白皮书还介绍了公式、数据框等,在某些情况下还介绍了某些 S 对象可以是给定类的特定情况,并且从这个意义上扩展该类。例如,在 R 中,多元时间序列 ("mts") 扩展了(简单)时间序列 ("ts"),或者多元或广义线性模型 ("mlm""glm") 扩展了正态线性模型 "lm"

“解决方法”:class(.)[1]

因此,一些经验更丰富、更谨慎的程序员已在这样的比较中用 class(x)[1](或 class(x)[1L])替换 class(x),例如,在 2018 年广受赞誉的 useR!演讲中。
在某些情况下,这已经足够好,这也是 R 的 data.class(.) 函数(除其他函数外)或(用户隐藏的)methods:::.class1(.) 所做的。

但是,程序员应意识到,这只是一个解决方法,并且在使用典型的 S3 继承的情况下会导致其工作不正确:在某些情况下,对结果为 "fitme" 类的函数 fitme() 进行轻微修改或扩展非常自然,通常通过编写 fitmeMore()(例如)来实现,其值将为 c("fMore", "fitme") 类,以便几乎所有“fitme”方法都将继续工作,但是 fitmeMore() 的作者将另外提供一个 print() 方法,即提供方法函数 print.fMore()

但是,如果其他用户使用 class(.)[1] 并为 class(.)[1] == "fitme" 的情况提供了代码,则该代码将错误地不适用于新的 "fMore" 对象。
唯一正确的解决方案是使用 inherits(., "fitme"),因为它适用于所有它应该适用的对象。

在很大程度上依赖 CRAN 包中,以下行(略有混淆)应有效确定特定类的列表条目

isC <- vapply(args, class, "") == "__my_class__"

发现(并通知包维护者)需要更正为

isC <- vapply(args, inherits, TRUE, what = "__my_class__")

摘要

您应该使用 inherits(x, "foo") 而不是 class(x) == "foo"
    或者可能也可以使用 is(x, "foo")

推论

switch(class(x)[1],
       "class_1" = { ..... },
       "class_2" = { ..... },
       .......,
       .......,
       "class_10" = { ..... },
       stop(" ... invalid class:", class(x)))

可能看起来很简洁,但几乎总是不够好,因为它是(通常)错误的,例如,当 class(x)c("class_7", "class_2") 时。

参考

  • R 核心团队 (2019)。R 帮助页面

  • Becker, R. A., Chambers, J. M. 和 Wilks, A. R. (1988) The New S Language蓝皮书,介绍 S 版本 2 (S2));Wadsworth & Brooks/Cole。

  • Chambers, J. M. 和 Hastie, T. J. 编辑 (1992) Statistical Models in S白皮书,介绍 S 版本 3 (S3);Chapman & Hall,伦敦)。

  • 钱伯斯,约翰·M.(1998)使用数据编程绿皮书S4 原版);施普林格。

  • 钱伯斯,约翰·M.(2008)数据分析软件:使用 R 编程S4 等 R);施普林格。