第八章 S3
前情提要:上一章我們的代碼得到了老虎機(jī)的輸出結(jié)果盯另,但與我們理想情況還差一些:
1蘸际、沒(méi)有考慮鉆石可以當(dāng)作萬(wàn)能牌的問(wèn)題
2替废、輸出格式不太對(duì)
我們這一章首先解決輸出格式的問(wèn)題衩辟,這一點(diǎn)其實(shí)就要用到我們第三章3.2 屬性學(xué)習(xí)的知識(shí)。下面開(kāi)始吧赁豆。
# 目標(biāo)格式
play()
## 0 0 DD
## $0
# 現(xiàn)在的格式
play()
## "0" "0" "DD"
## 0
如果我們把play()
函數(shù)存儲(chǔ)為一個(gè)對(duì)象仅醇,這個(gè)新對(duì)象的顯示結(jié)果就又有了問(wèn)題如下:
play <- function(){
symbols <- get_symbols()
print(symbols)
score(symbols)
}
one_play <- play()
## "B" "0" "B"
one_play
## 0
我們的程序中展示符號(hào)部分的代碼較為直接和隨意:從play函數(shù)內(nèi)調(diào)用print
函數(shù)。這樣只要play()
一出現(xiàn)魔种,控制臺(tái)就會(huì)輸出符號(hào)部分的內(nèi)容析二。而我們?cè)诳刂婆_(tái)上輸入我們命名的新對(duì)象one_play時(shí)play()
函數(shù)只會(huì)返回該函數(shù)最后一行代碼所得到的值。修改下上述代碼就可以理解這句話:
play <- function(){
symbols <- get_symbols()
score(symbols)
print(symbols)
}
one_play <- play()
## "BBB" "0" "0"
one_play #因?yàn)樽詈笠恍写a是print(symbols)所以輸出結(jié)果如下
## "BBB" "0" "0"
8.1 S3系統(tǒng)
S3指的是R自帶的類系統(tǒng)节预,該系統(tǒng)掌管著R如何處理具有不同類的對(duì)象叶摄。一些函數(shù)會(huì)首先查詢對(duì)象的S3類,再根據(jù)其類屬性作出相應(yīng)的相應(yīng)安拟。
print就是這樣的函數(shù)
num <- 1000000000
print(num)
## 1000000000
若我們賦予該數(shù)字后面跟著POSIXt的S3類POSIXct蛤吓,print將會(huì)顯示一個(gè)時(shí)間,原因詳見(jiàn)第三章3.5 類的講解
num <- 1000000000
class(num) <- c('POSIXt','POSIXct')
print(num)
## "2001-09-09 09:46:40 CST"
R的S3系統(tǒng)有三個(gè)組成部分:屬性(attribute)(尤其是class屬性)糠赦、泛型函數(shù)(generic function)和方法(method)柱衔。
8.2 屬性
在第三章3.2節(jié)中,我們了解到很多R對(duì)象都具有屬性愉棱,這些屬性包含了關(guān)于這個(gè)對(duì)象的某些額外信息并且被賦予了屬性名稱唆铐,附加在該對(duì)象上。屬性不會(huì)影響對(duì)象的實(shí)際取值奔滑,但是作為該對(duì)象的某種類型的元數(shù)據(jù)艾岂,可以被R用于控制和管理這個(gè)對(duì)象。如:數(shù)據(jù)框?qū)⑵湫忻土忻鎯?chǔ)為一個(gè)屬性朋其,還將其類data.frame存儲(chǔ)為一個(gè)屬性王浴。attributes
函數(shù)可以查看一個(gè)對(duì)象的屬性。
# 查看撲克牌項(xiàng)目中的DECK數(shù)據(jù)框
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中提供了很多輔助函數(shù)梅猿,可以幫助我們?cè)O(shè)置和獲取一些常見(jiàn)的屬性氓辣。如:names,dim,class
,其實(shí)還有很多如: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
#賦予某個(gè)對(duì)象一個(gè)新的屬性
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允許我們?yōu)槟硞€(gè)對(duì)象添加任何你覺(jué)得必要的屬性(然而大多數(shù)屬性也會(huì)被R忽略掉)钞啸。只有在某個(gè)函數(shù)需要找到某個(gè)屬性卻又找不到時(shí),R才會(huì)抱怨喇潘。
attr
函數(shù)可以給某個(gè)對(duì)象添加任何屬性体斩,也可以查詢某個(gè)對(duì)象所包含的屬性
one_play <- play()
one_play
## 0
attributes(one_play)
## NULL #這代表one_play沒(méi)有任何屬性
現(xiàn)在使用attr
函數(shù),該函數(shù)接受兩個(gè)參數(shù):一個(gè)R對(duì)象和某個(gè)屬性的名稱(以字符串的形式)颖低。具體看幫助文檔
# 現(xiàn)在賦予one_play對(duì)象一個(gè)名為symbols的屬性絮吵,該屬性包含一個(gè)字符串向量
attr(one_play, 'symbols') <- c('B','0','BB')
#查看one_play的屬性會(huì)發(fā)現(xiàn)多了一個(gè)叫‘symbols’的屬性且值為'B','0','BB'
attributes(one_play)
## $symbols
## [1] "B" "0" "BB"
同理查找某個(gè)對(duì)象的某個(gè)屬性用attr函數(shù),參數(shù)給定對(duì)象名及屬性名即可
attr(one_play, 'symbols')
## [1] "B" "0" "BB"
如果將某個(gè)屬性賦給一個(gè)原子型向量(如現(xiàn)在我們給one_play賦予了屬性)忱屑,那么R通常會(huì)將該屬性顯示在這個(gè)向量的值的下方蹬敲。但是如果該屬性改變了這個(gè)向量的類暇昂,R可能會(huì)用一種新的方式顯示這個(gè)向量所包含的所有信息(如POSIXct的例子)
one_play
## [1] 0
## attr(,"symbols")
## [1] "B" "0" "BB"
練習(xí):用上面學(xué)到的attr
函數(shù)修改play()使返回金額的同時(shí)返回符號(hào)信息。
play <- function(){
symbols <- get_symbols()#1.生成符號(hào)組合
prize <- score(symbols) #2.生成金額賦值給對(duì)象prize
attr(prize,'symbols') <- symbols #3.給prize添加屬性
prize #4.輸出prize
}
play()
## [1] 0
## attr(,"symbols")
## [1] "0" "0" "0"
現(xiàn)在play()可以同時(shí)顯示中獎(jiǎng)金額和符號(hào)了伴嗡,但依然不好看急波,我們等下再美觀這個(gè)輸出結(jié)果。先簡(jiǎn)化下代碼闹究。
我們可以利用structure
函數(shù)把上面代碼的2和3即(生成金額和設(shè)置屬性值合并為一步來(lái)完成)。structure
函數(shù)創(chuàng)建的是帶有一組屬性的R對(duì)象食店。第一個(gè)參數(shù)是一個(gè)R對(duì)象或?qū)ο蟮娜≈?/strong>渣淤,剩下的參數(shù)是你想添加給這個(gè)對(duì)象的屬性。屬性名稱可以任意設(shè)置吉嫩。structure會(huì)將你提供的參數(shù)名稱作為屬性名稱賦給該對(duì)象价认。
play <- function(){
symbols <- get_symbols()
structure(score(symbols), mysymbols = symbols)
}
two_play <- play()
two_play
## [1] 0
## attr(,"mysymbols")
## [1] "0" "B" "BBB"
這樣play()函數(shù)輸出的結(jié)果是一個(gè)帶有mysymbols
屬性的對(duì)象,接下來(lái)我們自定義函數(shù)來(lái)查找和使用這個(gè)屬性了自娩。我們把函數(shù)命名為position
用踩。
position <- function(prize){
symbols <- attr(prize, which = 'mysymbols') #1.因?yàn)槲覀兿M敵龅慕Y(jié)果既有符號(hào)又有金額,所以這里先屬性‘mysymbols’的值并存儲(chǔ)在名為symbols的對(duì)象中
symbols <- paste(symbols, collapse = " ") #2.使用paste函數(shù)把向量上一步存儲(chǔ)的符號(hào)(三個(gè))壓縮為一個(gè)字符串忙迁,不熟悉paste函數(shù)的可以查看下幫助文檔
string <- paste(symbols, prize, sep = "\n$") #3.把壓縮好的符號(hào)字符串與金額合并在一起脐彩,并使用"\n$"分隔,\n為正則表達(dá)式表示另起一個(gè)新行(相當(dāng)于回車)
cat(string) #4.在控制臺(tái)上顯示正則表達(dá)式的結(jié)果姊扔,但去掉其中的引號(hào)
}
position(two_play)
## 0 B BBB
## $0
#單獨(dú)運(yùn)行1
symbols <- attr(two_play, which = 'mysymbols')
symbols
## [1] "0" "B" "BBB"
#然后運(yùn)行2
symbols <- paste(symbols, collapse = " ")
symbols
## "0 B BBB"
#然后運(yùn)行3
string <- paste(symbols, two_play, sep = "\n$")
string
## "0 B BBB\n$0"
#然后運(yùn)行4
cat(string)
## 0 B BBB
## $0
詳細(xì)的cat()
函數(shù)用法及參數(shù)大家可以查看幫助文檔惠奸,至此美化輸出結(jié)果完成了。但問(wèn)題還存在就是玩一次美化一次很麻煩恰梢。
position(play())
## BBB 0 B
## $0
這種清理函數(shù)輸出的方法要求人工介入某個(gè)R會(huì)話(這里指人工調(diào)用了position()函數(shù))佛南。有一種函數(shù)可以使這個(gè)過(guò)程自動(dòng)化,即每次play函數(shù)運(yùn)行結(jié)束后都自動(dòng)對(duì)輸出結(jié)果進(jìn)行美化嵌言。這個(gè)函數(shù)就是print
他是一個(gè)泛型函數(shù)嗅回。
8.3 泛型函數(shù)
每次在控制臺(tái)窗口顯示某個(gè)輸出結(jié)果時(shí)R都會(huì)調(diào)用print函數(shù),知識(shí)在后天運(yùn)行摧茴,我們沒(méi)察覺(jué)
play()
## [1] 0
## attr(,"mysymbols")
## [1] "0" "0" "B"
print(play())
## [1] 2
## attr(,"mysymbols")
## [1] "0" "C" "B"
我們可以通過(guò)改寫(xiě)print函數(shù)使R輸出結(jié)果的方式绵载,達(dá)到應(yīng)用position函數(shù)的效果。其實(shí)我們之前已經(jīng)見(jiàn)過(guò)泛型函數(shù)print的應(yīng)用了
#在顯示無(wú)類屬性的num使苛白,print的輸出結(jié)果
num <- 1000000000
print(num)
## 1000000000
#如果賦予num一個(gè)類尘分,print的輸出結(jié)果便發(fā)生了改變
num <- 1000000000
class(num) <- c('POSIXt','POSIXct')
print(num)
## "2001-09-09 09:46:40 CST"
print不是一個(gè)普通的函數(shù),是泛型函數(shù)丸氛,這意味著print可以在不同場(chǎng)合下培愁,完成不同的任務(wù)。
下面看下print的源代碼缓窜,我們或許回想print是如何實(shí)現(xiàn)不同場(chǎng)合完成不同任務(wù)的定续,會(huì)不會(huì)是先查找某個(gè)對(duì)象的類屬性谍咆,然后根據(jù)類屬性的不同,使用if樹(shù)分配合理的輸出顯示方法那私股?其實(shí)print的工作原理和這個(gè)想法很接近了摹察。但print用的方法更簡(jiǎn)單即:調(diào)用的一個(gè)特殊的函數(shù)UseMethod
print
## function (x, ...)
## UseMethod("print")
## <bytecode: 0x000001b4a4567060>
## <environment: namespace:base>
8.4 方法
當(dāng)調(diào)用print的時(shí)候,它其實(shí)調(diào)用了一個(gè)特別的函數(shù)UseMethod
(中文可以理解為:使用方法)
UseMethod檢查你提供給print函數(shù)的第一個(gè)參數(shù)的類屬性倡鲸,然后再將你提供的待輸出對(duì)象交給一個(gè)新函數(shù)來(lái)處理供嚎,該函數(shù)專門(mén)處理具有某種類屬性的輸入對(duì)象。比如你向print提供一個(gè)類屬性為POSIXct的對(duì)象時(shí)峭状,UseMethod會(huì)將print函數(shù)的所有參數(shù)交給print.POSIXct
函數(shù)來(lái)處理克滴。R隨后會(huì)運(yùn)行print.POSIXct
函數(shù)并返回針對(duì)POSIXct類屬性的輸出結(jié)果。
print.POSIXct被稱為print函數(shù)的方法(method)优床。這個(gè)函數(shù)本身時(shí)普通的R函數(shù)劝赔,但特殊在,UseMethod會(huì)調(diào)用他們?nèi)ヌ幚?strong>具有對(duì)應(yīng)類屬性的對(duì)象胆敞。正是因?yàn)檫@樣着帽,print函數(shù)才可以針對(duì)不同類屬性的對(duì)象進(jìn)行不同的操作。print調(diào)用UseMethod——>UseMethod檢測(cè)print第一個(gè)參數(shù)的類屬性——>根據(jù)類屬性調(diào)用特定方法處理移层。
# methods函數(shù)查看print有多少種方法
length(methods(print))
## 185
#查看前6個(gè)
head(methods(print))
## [1] "print.acf" "print.anova" "print.aov" "print.aovlist"
## [5] "print.ar" "print.Arima"
泛型函數(shù)——>方法——>基于類的分派仍翰;這樣一個(gè)方式所構(gòu)成的系統(tǒng)就是R的S3系統(tǒng)。
S3系統(tǒng)使得R函數(shù)可以在不同場(chǎng)合有不同的表型观话。我們可以利用S3函數(shù)進(jìn)一步美化老虎機(jī)的輸出格式歉备。實(shí)現(xiàn)這一點(diǎn)首先將類屬性賦給輸出結(jié)果,然后針對(duì)該類屬性編寫(xiě)一個(gè)print的方法匪燕。下面我們大致了解下UseMethod選擇類方法函數(shù)的方法蕾羊。
8.4.1 方法分派
每個(gè)S3方法的名稱都包含兩個(gè)部分:前一部分指明該方法對(duì)應(yīng)的函數(shù);后一部分指明類屬性帽驯;中間用英文符號(hào).
隔開(kāi)龟再。如處理類屬性為POSIXct的print方法名為:print.POSIXct
現(xiàn)在我們?yōu)槲覀兊睦匣C(jī)編寫(xiě)一種新的print方法,類屬性命名為positions
尼变。注意兩點(diǎn):1利凑、這個(gè)函數(shù)必須命名為print.positions
;否則UseMethod就不知道如何找到它嫌术;2哀澈、這個(gè)類方法函數(shù)所接受的輸入?yún)?shù)應(yīng)與print函數(shù)一致,否則在傳遞參數(shù)時(shí)R會(huì)報(bào)錯(cuò)度气。
args(print)
## function (x, ...)
## NULL
print.positions <- function(x, ...){
position(x)# 我們?cè)谇懊鎸?xiě)了這個(gè)函數(shù)割按,這里直接調(diào)用即可
}
現(xiàn)在R在顯示類屬性為positions
的對(duì)象時(shí),會(huì)自動(dòng)找到使用print.positions
函數(shù)磷籍,下面只需要確保play函數(shù)輸出的對(duì)象的類屬性為positions即可
play <- function(){
symbols <- get_symbols()
structure(score(symbols), mysymbols = symbols, class = "positions")
}
play()
## B 0 0
## $0
play()
## B 0 BB
## $0
補(bǔ)充:
1适荣、有些R對(duì)象具有多個(gè)類屬性现柠。此時(shí)UseMethod首先尋找并匹配該對(duì)象類屬性向量中的第一個(gè)屬性,找不到的話弛矛,會(huì)嘗試匹配第二個(gè)類屬性够吩,以此類推。
2丈氓、在print函數(shù)運(yùn)行時(shí)周循,如果對(duì)象的類沒(méi)有匹配的print方法,那么UseMethod將調(diào)用一個(gè)名為print.default
的特殊方法万俗,該方法專門(mén)處理一般情況湾笛。
8.5 類
你可以使用R的S3系統(tǒng)為對(duì)象新建一個(gè)穩(wěn)健的類(class)。R會(huì)以一致且合理的方式對(duì)待同屬一類的對(duì)象该编。步驟:
(1)給類起一個(gè)名字(老虎機(jī)中起的名字是:positions)
(2)給屬于該類的每個(gè)對(duì)象賦類屬性(class = "positions")
(3)為屬于該類的對(duì)象編寫(xiě)常用泛型函數(shù)的類方法迄本。(print.positions())
注意:
1硕淑、R在將多個(gè)對(duì)象組合成一個(gè)向量時(shí)會(huì)丟棄對(duì)象的屬性(如類屬性)
2课竣、R在對(duì)某個(gè)對(duì)象取子集時(shí)也會(huì)丟棄其屬性
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與調(diào)試
從上述看,當(dāng)我們嘗試去理解R函數(shù)時(shí)置媳,S3系統(tǒng)可能會(huì)增加很多煩惱于樟。因?yàn)橐粋€(gè)函數(shù)在其代碼中調(diào)用UseMethod后,就很難知道這個(gè)函數(shù)真正是做什么的拇囊。但學(xué)完S3系統(tǒng)后我們可以知道該怎么處理:
1迂曲、UseMethod本身會(huì)調(diào)用某個(gè)類屬性所對(duì)應(yīng)的類方法函數(shù),因此可以直接找到這個(gè)類方法函數(shù)并檢查它的源代碼
2寥袭、這個(gè)類方法函數(shù)的名稱符合<function.class>或<function.default>的結(jié)構(gòu)路捧。
3、還可以使用methods
函數(shù)查看與某個(gè)函數(shù)或類相關(guān)的方法传黄。
#查看某個(gè)函數(shù)相關(guān)的類方法
head(methods(print))
##[1] "print.acf" "print.anova" "print.aov" "print.aovlist"
##[5] "print.ar" "print.Arima"
#查看某個(gè)類相關(guān)的方法
head(methods(class = 'factor'))
##[1] "[.factor" "[[.factor" "[[<-.factor"
##[4] "[<-.factor" "all.equal.factor" "as.character.factor"
8.7 小結(jié)
1杰扫、在R中存儲(chǔ)信息并非只能通過(guò)賦值的方式;創(chuàng)建某種特殊的行為也不一定只能通過(guò)編寫(xiě)函數(shù)來(lái)實(shí)現(xiàn)膘掰。這兩種任務(wù)都可以通過(guò)R的S3系統(tǒng)來(lái)完成章姓。
2、泛型函數(shù)會(huì)檢查其輸入對(duì)象的類屬性识埋,并有針對(duì)性地生成基于類屬性的輸出結(jié)果
3凡伊、泛型函數(shù)——>方法——>基于類的分派;這樣一個(gè)方式所構(gòu)成的系統(tǒng)就是R的S3系統(tǒng)窒舟。
4系忙、很多常見(jiàn)的R函數(shù)都是S3泛型函數(shù)。