第八章 S3
前情提要:上一章我们的代码得到了老虎机的输出结果,但与我们理想情况还差一些:
1、没有考虑钻石可以当作万能牌的问题
2、输出格式不太对
我们这一章首先解决输出格式的问题,这一点其实就要用到我们第三章3.2 属性学习的知识。下面开始吧。
# 目标格式
play()
## 0 0 DD
## $0
# 现在的格式
play()
## "0" "0" "DD"
## 0
如果我们把play()
函数存储为一个对象,这个新对象的显示结果就又有了问题如下:
play <- function(){
symbols <- get_symbols()
print(symbols)
score(symbols)
}
one_play <- play()
## "B" "0" "B"
one_play
## 0
我们的程序中展示符号部分的代码较为直接和随意:从play函数内调用print
函数。这样只要play()
一出现,控制台就会输出符号部分的内容。而我们在控制台上输入我们命名的新对象one_play时play()
函数只会返回该函数最后一行代码所得到的值。修改下上述代码就可以理解这句话:
play <- function(){
symbols <- get_symbols()
score(symbols)
print(symbols)
}
one_play <- play()
## "BBB" "0" "0"
one_play #因为最后一行代码是print(symbols)所以输出结果如下
## "BBB" "0" "0"
8.1 S3系统
S3指的是R自带的类系统,该系统掌管着R如何处理具有不同类的对象。一些函数会首先查询对象的S3类,再根据其类属性作出相应的相应。
print就是这样的函数
num <- 1000000000
print(num)
## 1000000000
若我们赋予该数字后面跟着POSIXt的S3类POSIXct,print将会显示一个时间,原因详见第三章3.5 类的讲解
num <- 1000000000
class(num) <- c('POSIXt','POSIXct')
print(num)
## "2001-09-09 09:46:40 CST"
R的S3系统有三个组成部分:属性(attribute)(尤其是class属性)、泛型函数(generic function)和方法(method)。
8.2 属性
在第三章3.2节中,我们了解到很多R对象都具有属性,这些属性包含了关于这个对象的某些额外信息并且被赋予了属性名称,附加在该对象上。属性不会影响对象的实际取值,但是作为该对象的某种类型的元数据,可以被R用于控制和管理这个对象。如:数据框将其行名和列名存储为一个属性,还将其类data.frame存储为一个属性。attributes
函数可以查看一个对象的属性。
# 查看扑克牌项目中的DECK数据框
attributes(DECK)
## $names
## [1] "face" "suit" "value"
## $class
## [1] "data.frame"
## $row.names
## [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
## [25] 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
## [49] 49 50 51 52
R中提供了很多辅助函数,可以帮助我们设置和获取一些常见的属性。如:names,dim,class
,其实还有很多如:row.names,levels
等。
row.names(DECK)
## [1] "1" "2" "3" "4" "5" "6" "7" "8" "9" "10" "11" "12" "13" "14"
## [15] "15" "16" "17" "18" "19" "20" "21" "22" "23" "24" "25" "26" "27" "28"
## [29] "29" "30" "31" "32" "33" "34" "35" "36" "37" "38" "39" "40" "41" "42"
## [43] "43" "44" "45" "46" "47" "48" "49" "50" "51" "52"
#改变属性取值
row.names(DECK) <- 101:152
#赋予某个对象一个新的属性
levels(DECK) <- c('level1','level2','level3','level4')
attributes(DECK)
## $names
## [1] "face" "suit" "value"
## $class
## [1] "data.frame"
## $row.names
## [1] 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
## [19] 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
## [37] 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
## $levels
## [1] "level1" "level2" "level3" "level4"
R允许我们为某个对象添加任何你觉得必要的属性(然而大多数属性也会被R忽略掉)。只有在某个函数需要找到某个属性却又找不到时,R才会抱怨。
attr
函数可以给某个对象添加任何属性,也可以查询某个对象所包含的属性
one_play <- play()
one_play
## 0
attributes(one_play)
## NULL #这代表one_play没有任何属性
现在使用attr
函数,该函数接受两个参数:一个R对象和某个属性的名称(以字符串的形式)。具体看帮助文档
# 现在赋予one_play对象一个名为symbols的属性,该属性包含一个字符串向量
attr(one_play, 'symbols') <- c('B','0','BB')
#查看one_play的属性会发现多了一个叫‘symbols’的属性且值为'B','0','BB'
attributes(one_play)
## $symbols
## [1] "B" "0" "BB"
同理查找某个对象的某个属性用attr函数,参数给定对象名及属性名即可
attr(one_play, 'symbols')
## [1] "B" "0" "BB"
如果将某个属性赋给一个原子型向量(如现在我们给one_play赋予了属性),那么R通常会将该属性显示在这个向量的值的下方。但是如果该属性改变了这个向量的类,R可能会用一种新的方式显示这个向量所包含的所有信息(如POSIXct的例子)
one_play
## [1] 0
## attr(,"symbols")
## [1] "B" "0" "BB"
练习:用上面学到的attr
函数修改play()使返回金额的同时返回符号信息。
play <- function(){
symbols <- get_symbols()#1.生成符号组合
prize <- score(symbols) #2.生成金额赋值给对象prize
attr(prize,'symbols') <- symbols #3.给prize添加属性
prize #4.输出prize
}
play()
## [1] 0
## attr(,"symbols")
## [1] "0" "0" "0"
现在play()可以同时显示中奖金额和符号了,但依然不好看,我们等下再美观这个输出结果。先简化下代码。
我们可以利用structure
函数把上面代码的2和3即(生成金额和设置属性值合并为一步来完成)。structure
函数创建的是带有一组属性的R对象。第一个参数是一个R对象或对象的取值,剩下的参数是你想添加给这个对象的属性。属性名称可以任意设置。structure会将你提供的参数名称作为属性名称赋给该对象。
play <- function(){
symbols <- get_symbols()
structure(score(symbols), mysymbols = symbols)
}
two_play <- play()
two_play
## [1] 0
## attr(,"mysymbols")
## [1] "0" "B" "BBB"
这样play()函数输出的结果是一个带有mysymbols
属性的对象,接下来我们自定义函数来查找和使用这个属性了。我们把函数命名为position
。
position <- function(prize){
symbols <- attr(prize, which = 'mysymbols') #1.因为我们希望输出的结果既有符号又有金额,所以这里先属性‘mysymbols’的值并存储在名为symbols的对象中
symbols <- paste(symbols, collapse = " ") #2.使用paste函数把向量上一步存储的符号(三个)压缩为一个字符串,不熟悉paste函数的可以查看下帮助文档
string <- paste(symbols, prize, sep = "\n$") #3.把压缩好的符号字符串与金额合并在一起,并使用"\n$"分隔,\n为正则表达式表示另起一个新行(相当于回车)
cat(string) #4.在控制台上显示正则表达式的结果,但去掉其中的引号
}
position(two_play)
## 0 B BBB
## $0
#单独运行1
symbols <- attr(two_play, which = 'mysymbols')
symbols
## [1] "0" "B" "BBB"
#然后运行2
symbols <- paste(symbols, collapse = " ")
symbols
## "0 B BBB"
#然后运行3
string <- paste(symbols, two_play, sep = "\n$")
string
## "0 B BBB\n$0"
#然后运行4
cat(string)
## 0 B BBB
## $0
详细的cat()
函数用法及参数大家可以查看帮助文档,至此美化输出结果完成了。但问题还存在就是玩一次美化一次很麻烦。
position(play())
## BBB 0 B
## $0
这种清理函数输出的方法要求人工介入某个R会话(这里指人工调用了position()函数)。有一种函数可以使这个过程自动化,即每次play函数运行结束后都自动对输出结果进行美化。这个函数就是print
他是一个泛型函数。
8.3 泛型函数
每次在控制台窗口显示某个输出结果时R都会调用print函数,知识在后天运行,我们没察觉
play()
## [1] 0
## attr(,"mysymbols")
## [1] "0" "0" "B"
print(play())
## [1] 2
## attr(,"mysymbols")
## [1] "0" "C" "B"
我们可以通过改写print函数使R输出结果的方式,达到应用position函数的效果。其实我们之前已经见过泛型函数print的应用了
#在显示无类属性的num使,print的输出结果
num <- 1000000000
print(num)
## 1000000000
#如果赋予num一个类,print的输出结果便发生了改变
num <- 1000000000
class(num) <- c('POSIXt','POSIXct')
print(num)
## "2001-09-09 09:46:40 CST"
print不是一个普通的函数,是泛型函数,这意味着print可以在不同场合下,完成不同的任务。
下面看下print的源代码,我们或许回想print是如何实现不同场合完成不同任务的,会不会是先查找某个对象的类属性,然后根据类属性的不同,使用if树分配合理的输出显示方法那?其实print的工作原理和这个想法很接近了。但print用的方法更简单即:调用的一个特殊的函数UseMethod
print
## function (x, ...)
## UseMethod("print")
## <bytecode: 0x000001b4a4567060>
## <environment: namespace:base>
8.4 方法
当调用print的时候,它其实调用了一个特别的函数UseMethod
(中文可以理解为:使用方法)
UseMethod检查你提供给print函数的第一个参数的类属性,然后再将你提供的待输出对象交给一个新函数来处理,该函数专门处理具有某种类属性的输入对象。比如你向print提供一个类属性为POSIXct的对象时,UseMethod会将print函数的所有参数交给print.POSIXct
函数来处理。R随后会运行print.POSIXct
函数并返回针对POSIXct类属性的输出结果。
print.POSIXct被称为print函数的方法(method)。这个函数本身时普通的R函数,但特殊在,UseMethod会调用他们去处理具有对应类属性的对象。正是因为这样,print函数才可以针对不同类属性的对象进行不同的操作。print调用UseMethod——>UseMethod检测print第一个参数的类属性——>根据类属性调用特定方法处理。
# methods函数查看print有多少种方法
length(methods(print))
## 185
#查看前6个
head(methods(print))
## [1] "print.acf" "print.anova" "print.aov" "print.aovlist"
## [5] "print.ar" "print.Arima"
泛型函数——>方法——>基于类的分派;这样一个方式所构成的系统就是R的S3系统。
S3系统使得R函数可以在不同场合有不同的表型。我们可以利用S3函数进一步美化老虎机的输出格式。实现这一点首先将类属性赋给输出结果,然后针对该类属性编写一个print的方法。下面我们大致了解下UseMethod选择类方法函数的方法。
8.4.1 方法分派
每个S3方法的名称都包含两个部分:前一部分指明该方法对应的函数;后一部分指明类属性;中间用英文符号.
隔开。如处理类属性为POSIXct的print方法名为:print.POSIXct
现在我们为我们的老虎机编写一种新的print方法,类属性命名为positions
。注意两点:1、这个函数必须命名为print.positions
;否则UseMethod就不知道如何找到它;2、这个类方法函数所接受的输入参数应与print函数一致,否则在传递参数时R会报错。
args(print)
## function (x, ...)
## NULL
print.positions <- function(x, ...){
position(x)# 我们在前面写了这个函数,这里直接调用即可
}
现在R在显示类属性为positions
的对象时,会自动找到使用print.positions
函数,下面只需要确保play函数输出的对象的类属性为positions即可
play <- function(){
symbols <- get_symbols()
structure(score(symbols), mysymbols = symbols, class = "positions")
}
play()
## B 0 0
## $0
play()
## B 0 BB
## $0
补充:
1、有些R对象具有多个类属性。此时UseMethod首先寻找并匹配该对象类属性向量中的第一个属性,找不到的话,会尝试匹配第二个类属性,以此类推。
2、在print函数运行时,如果对象的类没有匹配的print方法,那么UseMethod将调用一个名为print.default
的特殊方法,该方法专门处理一般情况。
8.5 类
你可以使用R的S3系统为对象新建一个稳健的类(class)。R会以一致且合理的方式对待同属一类的对象。步骤:
(1)给类起一个名字(老虎机中起的名字是:positions)
(2)给属于该类的每个对象赋类属性(class = "positions")
(3)为属于该类的对象编写常用泛型函数的类方法。(print.positions())
注意:
1、R在将多个对象组合成一个向量时会丢弃对象的属性(如类属性)
2、R在对某个对象取子集时也会丢弃其属性
play1 <- play()
play1
## DD 0 0
## $0
play2 <- play()
play2
## 0 BB 0
## $0
c(play1, play2)
## 0 0
play1[1]
## 0
8.6 S3与调试
从上述看,当我们尝试去理解R函数时,S3系统可能会增加很多烦恼。因为一个函数在其代码中调用UseMethod后,就很难知道这个函数真正是做什么的。但学完S3系统后我们可以知道该怎么处理:
1、UseMethod本身会调用某个类属性所对应的类方法函数,因此可以直接找到这个类方法函数并检查它的源代码
2、这个类方法函数的名称符合<function.class>或<function.default>的结构。
3、还可以使用methods
函数查看与某个函数或类相关的方法。
#查看某个函数相关的类方法
head(methods(print))
##[1] "print.acf" "print.anova" "print.aov" "print.aovlist"
##[5] "print.ar" "print.Arima"
#查看某个类相关的方法
head(methods(class = 'factor'))
##[1] "[.factor" "[[.factor" "[[<-.factor"
##[4] "[<-.factor" "all.equal.factor" "as.character.factor"
8.7 小结
1、在R中存储信息并非只能通过赋值的方式;创建某种特殊的行为也不一定只能通过编写函数来实现。这两种任务都可以通过R的S3系统来完成。
2、泛型函数会检查其输入对象的类属性,并有针对性地生成基于类属性的输出结果
3、泛型函数——>方法——>基于类的分派;这样一个方式所构成的系统就是R的S3系统。
4、很多常见的R函数都是S3泛型函数。