exportClasses 实际上对 S4 类有什么作用?

What does exportClasses actually do with S4 classes?

提问人:gowerc 提问时间:7/19/2023 更新时间:7/19/2023 访问量:135

问:

我试图了解命名空间如何与 S4 类一起工作,因此使用不同的导出指令将一个小示例放在一起。结果对我来说毫无意义,所以希望有人能够帮助解释发生了什么?

我的包只包含 1 个文件,其中包含以下代码:

B1 <- setClass( "Base_1", slots = list(x = "numeric"))
B2 <- setClass( "Base_2", slots = list(x = "numeric"))
B3 <- setClass( "Base_3", slots = list(x = "numeric"))
B4 <- setClass( "Base_4", slots = list(x = "numeric"))

然后,我在 NAMESPACE 文件中定义以下内容:

export(B1)
exportClasses(Base_1)
export(B2)
exportClasses(Base_3)

然后,我加载并尝试使用该包,如下所示:

library(tstpkg)

B1(x = 1)   # Runs fine
B2(x = 1)   # Runs fine
B3(x = 1)   # Errors (as expected)
B4(x = 1)   # Errors (as expected)

new("Base_1", x = 1)   # Runs fine
new("Base_2", x = 1)   # Runs fine - Was expecting to error
new("Base_3", x = 1)   # Runs fine
new("Base_4", x = 1)   # Runs fine - Was expecting to error

setClass("Derv_1", contains = "Base_1")   # Runs fine
setClass("Derv_2", contains = "Base_2")   # Runs fine - Was expecting to error
setClass("Derv_3", contains = "Base_3")   # Runs fine
setClass("Derv_4", contains = "Base_4")   # Runs fine - Was expecting to error

我期待并失败了,因为这些课程都没有暴露。有人能解释这里发生了什么吗?new(<class>)setClass(..., contains=<class>)Base_2Base_4

(如果你想自己玩代码,我把所有代码都放到github repo中)

R R-S4

评论


答:

4赞 Mikael Jagan 7/19/2023 #1

导出的类放置在导出列表中,存储在包命名空间中。以下是 Matrix 版本 1.6-0 版导出的类列表:

ns <- asNamespace("Matrix")
ns.exports <- getNamespaceInfo(ns, "exports")
cl1 <- sort(grep("^[.]__C__", names(ns.exports), value = TRUE))
cl1[1:20]
 [1] ".__C__BunchKaufman"              ".__C__BunchKaufmanFactorization"
 [3] ".__C__CHMfactor"                 ".__C__CHMsimpl"                 
 [5] ".__C__CHMsuper"                  ".__C__Cholesky"                 
 [7] ".__C__CholeskyFactorization"     ".__C__CsparseMatrix"            
 [9] ".__C__LU"                        ".__C__Matrix"                   
[11] ".__C__MatrixFactorization"       ".__C__QR"                       
[13] ".__C__RsparseMatrix"             ".__C__Schur"                    
[15] ".__C__SchurFactorization"        ".__C__TsparseMatrix"            
[17] ".__C__abIndex"                   ".__C__atomicVector"             
[19] ".__C__compMatrix"                ".__C__corMatrix"

下面是在 Matrix 命名空间中定义但导出的类列表:

cl0 <- setdiff(sort(grep("^[.]__C__", names(ns), value = TRUE)), cl1)
cl0
 [1] ".__C__dCsparseMatrix" ".__C__determinant"    ".__C__geMatrix"      
 [4] ".__C__lCsparseMatrix" ".__C__mMatrix"        ".__C__nCsparseMatrix"
 [7] ".__C__numLike"        ".__C__replValueSp"    ".__C__seqMat"        
[10] ".__C__xMatrix"

包导出类,以定义其他包可以导入和不能导入的内容。如果另一个包尝试导入未从包中导出的类,则该包的安装将在调用 中失败,并显示以下格式的错误:importIntoEnv

类 %s 不是由“namespace:%s”导出的

从其他包导入类的包将导入类的定义缓存在其命名空间的父环境中。它们还可以缓存超类定义,但缓存在命名空间本身中,而不是在其父命名空间中。大多数使用 S4 的软件包维护者都不知道后一种缓存,并且容易过时;我在下面解释。

以下是由 Matrix 版本 1.6-0 导出并由 SeuratObject 版本 4.1.3 导入的类列表:

ns <- asNamespace("SeuratObject")
ns.imports <- getNamespaceInfo(ns, "imports")
ns.imports.Matrix <- c(ns.imports[names(ns.imports) == "Matrix"],
                       recursive = TRUE, use.names = FALSE)
cl1 <- sort(grep("^[.]__C__", ns.imports.Matrix, value = TRUE))
[1] ".__C__dgCMatrix"

只需一个,方便。下面是缓存的类定义:

cl1.def <- mget(cl1, parent.env(ns))
str(cl1.def, max.level = 3L)
List of 1
 $ .__C__dgCMatrix:Formal class 'classRepresentation' [package "methods"] with 11 slots
  .. ..@ slots     :List of 6
  .. ..@ contains  :List of 11
  .. ..@ virtual   : logi FALSE
  .. ..@ prototype :Formal class 'S4' [package ""] with 0 slots
 list()
  .. ..@ validity  :function (object)  
  .. ..@ access    : list()
  .. ..@ className : chr "dgCMatrix"
  .. .. ..- attr(*, "package")= chr "Matrix"
  .. ..@ package   : chr "Matrix"
  .. ..@ subclasses: list()
  .. ..@ versionKey:<externalptr> 
  .. ..@ sealed    : logi FALSE

下面是缓存的超类定义:

scl1 <- paste0(".__C__", sort(names(cl1.def[[1L]]@contains)))
scl1.def <- mget(scl1, ns, ifnotfound = list(NULL)) # NULL <=> unexported
str(scl1.def, max.level = 2L)
List of 11
 $ .__C__CsparseMatrix :Formal class 'classRepresentation' [package "methods"] with 11 slots
 $ .__C__Matrix        :Formal class 'classRepresentation' [package "methods"] with 11 slots
 $ .__C__compMatrix    :Formal class 'classRepresentation' [package "methods"] with 11 slots
 $ .__C__dCsparseMatrix: NULL
 $ .__C__dMatrix       :Formal class 'classRepresentation' [package "methods"] with 11 slots
 $ .__C__dsparseMatrix :Formal class 'classRepresentation' [package "methods"] with 11 slots
 $ .__C__generalMatrix :Formal class 'classRepresentation' [package "methods"] with 11 slots
 $ .__C__mMatrix       : NULL
 $ .__C__replValueSp   : NULL
 $ .__C__sparseMatrix  :Formal class 'classRepresentation' [package "methods"] with 11 slots
 $ .__C__xMatrix       : NULL

这里对超类定义的缓存指向了 SeuratObject 中的一个错误:它导入但没有导出的超类,即它有dgCMatrixNAMESPACE

importClassesFrom(Matrix, dgCMatrix)

但真的应该有

importClassesFrom(Matrix, dgCMatrix,
                  ## and the exported superclasses:
                  CsparseMatrix, Matrix, compMatrix, dMatrix, 
                  dsparseMatrix, generalMatrix, sparseMatrix)

结果是,缓存在命名空间中的超类定义最终可能会过时。的定义是在加载时检索的(当填充命名空间的父环境时),而超类的定义是在安装时检索的(当命名空间本身被填充[和序列化]时)。如果加载时可用的 Matrix 版本与安装时可用的 Matrix 版本不同,并且这些版本包含冲突的超类定义,那么 SeuratObject 的用户可能会遇到问题 - 如果您还不熟悉缓存机制,这些问题很难调试。dgCMatrix

它可能应该记录在编写 R 扩展手册(此处)中,如果使用某个类,则还会隐式使用其超类,并且还必须导入这些超类。

这些陷阱只是维护者在更改他们导出的类的定义时应该尝试保持向后兼容性的众多原因之一。

最后,为什么要“看到”未导出的类?就 而言,我想部分原因是性能。实例化必须快速,并且对“导出性”的测试具有不小的成本。就 而言,我不太确定。这绝对是一个错误,但我想更努力地思考它,甚至可能查阅 R-devel 邮件列表(也是关于修改 WRE)......我现在已经做到了newsetClassnewsetClass

评论

0赞 gowerc 9/8/2023
您是否收到任何关于/能够访问未导出类是否是一个错误的跟进?newsetClass
1赞 Mikael Jagan 9/9/2023
我最终没有就这个具体问题咨询 R-devel,因为我说服自己当前的行为并不是真正的错误。用户总是能够通过 and(如果试图欺骗 CRAN)访问未导出的对象,而软件包维护者总是负责不搬起石头砸自己的脚。话虽如此,也许更合理的建议是测试包是否使用了未导入的类(并且尚未在包方法中定义),因为未导出的类永远无法导入。:::get(., asNamespace(.))R CMD check