使用class()函数改变S4对象的“class”时会发生什么?

6
如果我有一个S4类,比如:
setClass("MyClass",
     representation(
       data="data.frame",
       name="character"))

并将其实例化(比如说成obj),

obj <- new('MyClass', data=data.frame(1:3), name='An S4 class')

我将有以下表示:

An object of class "MyClass"
Slot "data":
  X1.3
1    1
2    2
3    3

Slot "name":
[1] "An S4 class"

到目前为止还不错。但是,如果我尝试使用以下方式更改“class”:
class(obj) <- "animal"

我现在理解
An object of class "animal"
<S4 Type Object>
attr(,"data")
  X1.3
1    1
2    2
3    3
attr(,"name")
[1] "An S4 class"

如果我尝试检查它是否仍然是S4类,则会返回true:

>isS4(obj)
[1] TRUE

到底发生了什么?为什么“slots”变成了属性?这还是一个S4类吗?

更新:

感谢详细的回答。只是想澄清一下,我并不希望这个代码能够正常工作或在实际场景中使用。我只是想更好地理解这种行为背后的机制。 此外,很难选择一个“最佳”答案(它们都非常出色),但是在SO的精神中,我必须选择一个。


1
我认为你刚刚添加了一个类属性,并创建了一个混合的S3-S4对象。我不会信任任何打印方法,因为它们可能没有设计来处理这种奇怪的对象。 - hadley
3个回答

3
S4将插槽作为属性实现。这通常对用户隐藏,但很容易被看到。
> attributes(setClass("MyClass", representation(x="integer"))())
$x
integer(0)

$class
[1] "MyClass"
attr(,"package")
[1] ".GlobalEnv"

稍微详细地说,我们有以下内容:
> .Internal(inspect(setClass("MyClass", representation(x="integer"))()))
@1fe4dfd8 25 S4SXP g0c0 [OBJ,NAM(2),S4,gp=0x10,ATT] 
ATTRIB:
  @1fe4dfa0 02 LISTSXP g0c0 [] 
    TAG: @23c8978 01 SYMSXP g0c0 [MARK,NAM(2)] "x"
    @1fe4df68 13 INTSXP g0c0 [] (len=0, tl=0)
    TAG: @2363208 01 SYMSXP g0c0 [MARK,NAM(2),LCK,gp=0x4000] "class" (has value)
    @1fd9f1b8 16 STRSXP g0c1 [NAM(2),ATT] (len=1, tl=0)
      @2e09e138 09 CHARSXP g0c1 [gp=0x61] [ASCII] [cached] "MyClass"
    ATTRIB:
      @1fd9fb20 02 LISTSXP g0c0 [] 
    TAG: @236cc00 01 SYMSXP g0c0 [MARK,NAM(2)] "package"
    @1fd9f278 16 STRSXP g0c1 [NAM(2)] (len=1, tl=0)
      @23cc938 09 CHARSXP g0c2 [MARK,gp=0x61] [ASCII] [cached] ".GlobalEnv"

这表明用于表示所有R对象的底层S表达式是一个带有属性列表的S4SXP。

通过使用S3-ism class<-,正如@hadley所指出的那样,你创建了一个混合怪物。class<-仅更新类属性,而不改变底层的S4SXP。当你打印对象时,它会使用类“animal”的对象的打印方法,可能是print.default。另一方面,isS4测试S表达式是否为S4SXP,它是这样的。所以你得到了一些东西...

通过实现相关的setAs函数,可以进行强制转换,使用`as(obj, "animal")`。


2

询问什么是S4对象,有点棘手。如果我们采用R内部的定义,是的,它仍然是一个S4对象,因为S4位仍然被设置。

obj <- new('MyClass', data=data.frame(1:3), name='An S4 class')
attr(obj, 'class')
## [1] "MyClass"
## attr(,"package")
## [1] ".GlobalEnv"

obj2 <- obj
class(obj2) <- 'animal'
attr(obj, 'class')
## [1] "MyClass"

请注意,就内存表示而言,objobj2之间唯一的区别实际上在于与class属性相关联的package属性的缺失。我们可以通过调用以下方式来“修复”这个问题:
attr(class(obj2), "package") <- ".GlobalEnv"

但在这种情况下,我们也会得到同样“奇怪”的结果:
print(obj2)
## An object of class "animal"
## <S4 Type Object>
## attr(,"data")
##   X1.3
## 1    1
## 2    2
## 3    3
## attr(,"name")
## [1] "An S4 class"

所以让我们寻找负责打印objobj2的方法。在两种情况下,都是通过签名为ANYshow完成的。使用getMethod("show", "ANY")打印将我们派遣到showDefault函数。
showDefault要做的第一件事情是:
...
clDef <- getClass(cl <- class(object), .Force = TRUE)
...

你看,getClass无法在GlobalEnv中找到animal的正式类定义。这就是为什么它调用show(unclass(object)),我们看到所有东西都作为属性显示(参见print(unclass(obj)))(编辑:为什么是属性:@MartinMorgan的回答中解释了)。

2

obj添加类属性'animal'后仍将作为S4对象运行,但请注意,在这种混合对象中更改插槽的值将失败,除非animal是具有相同名称插槽的定义S4类。此外,更改插槽值的操作还将删除任何不在animal中的插槽。

obj@data <- data.frame() # FAILS, animal not defined
setClass("animal", representation(data="data.frame"))
obj@data <- data.frame() # works, but drops name

正如@MartinMorgan所指出的,将一个S4类转换成另一个类的正确方法是使用setAs注册转换函数,然后对具有新类名称的对象调用as

# define animal with the same slots
setClass("animal", representation(data="data.frame", name="character"))
# register conversion function
setAs("MyClass", "animal", function(from, to )new(to, data=from@data, name=from@name))
# new obj
obj <- new('MyClass', data=data.frame(1:3), name='An S4 class')
as(obj, 'animal')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接