前言
本文接上一篇文章 R語言基於S3的面向對象編程,本文繼續介紹R語言基於S4的面向對象編程。
S4對象系統具有明顯的結構化特征,更適合面向對象的程序設計。Bioconductor社區,以S4對象系統做為基礎架構,只接受符合S4定義的R包。
目錄
- S4對象介紹
- 創建S4對象
- 訪問對象的屬性
- S4的泛型函數
- 查看S4對象的函數
- S4對象的使用
1 S4對象介紹
S4對象系統是一種標准的R語言面向對象實現方式,S4對象有明確的類定義,參數定義,參數檢查,繼承關系,實例化等的面向對象系統的特征。
2 創建S4對象
本文的系統環境
- Linux: Ubuntu Server 12.04.2 LTS 64bit
- R: 3.0.1 x86_64-pc-linux-gnu
為了方便我們檢查對象的類型,引入pryr包作為輔助工具。關於pryr包的介紹,請參考文章:[撬動R內核的高級工具包pryr](http://blog.fens.me/r-pryr/)
# 加載pryr包
> library(pryr)
2.1 如何創建S4對象?
由於S4對象是標准的面向對象實現方式, 有專門的類定義函數 setClass() 和類的實例化函數new() ,我們看一下setClass()和new()是如何動作的。
2.1.1 setClass()
查看setClass的函數定義
setClass(Class, representation, prototype, contains=character(),
validity, access, where, version, sealed, package,
S3methods = FALSE, slots)
參數列表:
- Class: 定義類名
- slots: 定義屬性和屬性類型
- prototype: 定義屬性的默認值
- contains=character(): 定義父類,繼承關系
- validity: 定義屬性的類型檢查
- where: 定義存儲空間
- sealed: 如果設置TRUE,則同名類不能被再次定義
- package: 定義所屬的包
- S3methods: R3.0.0以后不建議使用
- representation R3.0.0以后不建議使用
- access R3.0.0以后不建議使用
- version R3.0.0以后不建議使用
2.2 創建一個S4對象實例
# 定義一個S4對象
> setClass("Person",slots=list(name="character",age="numeric"))
# 實例化一個Person對象
> father<-new("Person",name="F",age=44)
# 查看father對象,有兩個屬性name和age
> father
An object of class "Person"
Slot "name":
[1] "F"
Slot "age":
[1] 44
# 查看father對象類型,為Person
> class(father)
[1] "Person"
attr(,"package")
[1] ".GlobalEnv"
# 查看father對象為S4的對象
> otype(father)
[1] "S4"
2.3 創建一個有繼承關系的S4對象
# 創建一個S4對象Person
> setClass("Person",slots=list(name="character",age="numeric"))
# 創建Person的子類
> setClass("Son",slots=list(father="Person",mother="Person"),contains="Person")
# 實例化Person對象
> father<-new("Person",name="F",age=44)
> mother<-new("Person",name="M",age=39)
# 實例化一個Son對象
> son<-new("Son",name="S",age=16,father=father,mother=mother)
# 查看son對象的name屬性
> son@name
[1] "S"
# 查看son對象的age屬性
> son@age
[1] 16
# 查看son對象的father屬性
> son@father
An object of class "Person"
Slot "name":
[1] "F"
Slot "age":
[1] 44
# 查看son對象的mother屬性
> slot(son,"mother")
An object of class "Person"
Slot "name":
[1] "M"
Slot "age":
[1] 39
# 檢查son類型
> otype(son)
[1] "S4"
# 檢查son@name屬性類型
> otype(son@name)
[1] "primitive"
# 檢查son@mother屬性類型
> otype(son@mother)
[1] "S4"
# 用isS4(),檢查S4對象的類型
> isS4(son)
[1] TRUE
> isS4(son@name)
[1] FALSE
> isS4(son@mother)
[1] TRUE
2.4 S4對象的默認值
> setClass("Person",slots=list(name="character",age="numeric"))
# 屬性age為空
> a<-new("Person",name="a")
> a
An object of class "Person"
Slot "name":
[1] "a"
Slot "age":
numeric(0)
# 設置屬性age的默認值20
> setClass("Person",slots=list(name="character",age="numeric"),prototype = list(age = 20))
# 屬性age為空
> b<-new("Person",name="b")
# 屬性age的默認值是20
> b
An object of class "Person"
Slot "name":
[1] "b"
Slot "age":
[1] 20
2.5 S4對象的類型檢查
> setClass("Person",slots=list(name="character",age="numeric"))
# 傳入錯誤的age類型
> bad<-new("Person",name="bad",age="abc")
Error in validObject(.Object) :
invalid class “Person” object: invalid object for slot "age" in class "Person": got class "character", should be or extend class "numeric"
# 設置age的非負檢查
> setValidity("Person",function(object) {
+ if (object@age <= 0) stop("Age is negative.")
+ })
Class "Person" [in ".GlobalEnv"]
Slots:
Name: name age
Class: character numeric
# 修傳入小於0的年齡
> bad2<-new("Person",name="bad",age=-1)
Error in validityMethod(object) : Age is negative.
2.6 從一個已經實例化的對象中創建新對象
S4對象,還支持從一個已經實例化的對象中創建新對象,創建時可以覆蓋舊對象的值
> setClass("Person",slots=list(name="character",age="numeric"))
# 創建一個對象實例n1
> n1<-new("Person",name="n1",age=19);n1
An object of class "Person"
Slot "name":
[1] "n1"
Slot "age":
[1] 19
# 從實例n1中,創建實例n2,並修改name的屬性值
> n2<-initialize(n1,name="n2");n2
An object of class "Person"
Slot "name":
[1] "n2"
Slot "age":
[1] 19
3 訪問對象的屬性
在S3對象中,一般我使用$來訪問一個對象的屬性,但在S4對象中,我們只能使用@來訪問一個對象的屬性
> setClass("Person",slots=list(name="character",age="numeric"))
> a<-new("Person",name="a")
# 訪問S4對象的屬性
> a@name
[1] "a"
> slot(a, "name")
[1] "a"
# 錯誤的屬性訪問
> a$name
Error in a$name : $ operator not defined for this S4 class
> a[1]
Error in a[1] : object of type 'S4' is not subsettable
> a[[1]]
Error in a[[1]] : this S4 class is not subsettable
4 S4的泛型函數
S4的泛型函數實現有別於S3的實現,S4分離了方法的定義和實現,如在其他語言中我們常說的接口和實現分離。通過setGeneric()來定義接口,通過setMethod()來定義現實類。這樣可以讓S4對象系統,更符合面向對象的特征。
普通函數的定義和調用
> work<-function(x) cat(x, "is working")
> work('Conan')
Conan is working
讓我來看看如何用R分離接口和現實
# 定義Person對象
> setClass("Person",slots=list(name="character",age="numeric"))
# 定義泛型函數work,即接口
> setGeneric("work",function(object) standardGeneric("work"))
[1] "work"
# 定義work的現實,並指定參數類型為Person對象
> setMethod("work", signature(object = "Person"), function(object) cat(object@name , "is working") )
[1] "work"
# 創建一個Person對象a
> a<-new("Person",name="Conan",age=16)
# 把對象a傳入work函數
> work(a)
Conan is working
通過S4對象系統,把原來的函數定義和調用2步,為成了4步進行:
- 定義數據對象類型
- 定義接口函數
- 定義實現函數
- 把數據對象以參數傳入到接口函數,執行實現函數
通過S4對象系統,是一個結構化的,完整的面向對象實現。
5 查看S4對象的函數
當我們使用S4對象進行面向對象封裝后,我們還需要能查看到S4對象的定義和函數定義。
還以上節中Person和work的例子
# 檢查work的類型
> ftype(work)
[1] "s4" "generic"
# 直接查看work函數
> work
standardGeneric for "work" defined from package ".GlobalEnv"
function (object)
standardGeneric("work")
<environment: 0x2aa6b18>
Methods may be defined for arguments: object
Use showMethods("work") for currently available ones.
# 查看work函數的現實定義
> showMethods(work)
Function: work (package .GlobalEnv)
object="Person"
# 查看Person對象的work函數現實
> getMethod("work", "Person")
Method Definition:
function (object)
cat(object@name, "is working")
Signatures:
object
target "Person"
defined "Person"
> selectMethod("work", "Person")
Method Definition:
function (object)
cat(object@name, "is working")
Signatures:
object
target "Person"
defined "Person"
# 檢查Person對象有沒有work函數
> existsMethod("work", "Person")
[1] TRUE
> hasMethod("work", "Person")
[1] TRUE
6 S4對象的使用
我們接下用S4對象做一個例子,定義一組圖形函數的庫。
6.1 任務一:定義圖形庫的數據結構和計算函數
假設最Shape為圖形的基類,包括圓形(Circle)和橢圓形(Ellipse),並計算出它們的面積(area)和周長(circum)。
- 定義圖形庫的數據結構
- 定義圓形的數據結構,並計算面積和周長
- 定義橢圓形的數據結構,並計算面積和周長
如圖所示結構:
定義基類Shape 和 圓形類Circle
# 定義基類Shape
> setClass("Shape",slots=list(name="character"))
# 定義圓形類Circle,繼承Shape,屬性radius默認值為1
> setClass("Circle",contains="Shape",slots=list(radius="numeric"),prototype=list(radius = 1))
# 驗證radius屬性值要大等於0
> setValidity("Circle",function(object) {
+ if (object@radius <= 0) stop("Radius is negative.")
+ })
Class "Circle" [in ".GlobalEnv"]
Slots:
Name: radius name
Class: numeric character
Extends: "Shape"
# 創建兩個圓形實例
> c1<-new("Circle",name="c1")
> c2<-new("Circle",name="c2",radius=5)
定義計算面積的接口和現實
# 計算面積泛型函數接口
> setGeneric("area",function(obj,...) standardGeneric("area"))
[1] "area"
# 計算面積的函數現實
> setMethod("area","Circle",function(obj,...){
+ print("Area Circle Method")
+ pi*obj@radius^2
+ })
[1] "area"
# 分別計算c1和c2的兩個圓形的面積
> area(c1)
[1] "Area Circle Method"
[1] 3.141593
> area(c2)
[1] "Area Circle Method"
[1] 78.53982
定義計算周長的接口和現實
# 計算周長泛型函數接口
> setGeneric("circum",function(obj,...) standardGeneric("circum"))
[1] "circum"
# 計算周長的函數現實
> setMethod("circum","Circle",function(obj,...){
+ 2*pi*obj@radius
+ })
# 分別計算c1和c2的兩個圓形的面積
[1] "circum"
> circum(c1)
[1] 6.283185
> circum(c2)
[1] 31.41593
上面的代碼,我們實現了圓形的定義,下來我們實現橢圓形。
# 定義橢圓形的類,繼承Shape,radius參數默認值為c(1,1),分別表示橢圓形的長半徑和短半徑
> setClass("Ellipse",contains="Shape",slots=list(radius="numeric"),prototype=list(radius=c(1,1)))
# 驗證radius參數
> setValidity("Ellipse",function(object) {
+ if (length(object@radius) != 2 ) stop("It's not Ellipse.")
+ if (length(which(object@radius<=0))>0) stop("Radius is negative.")
+ })
Class "Ellipse" [in ".GlobalEnv"]
Slots:
Name: radius name
Class: numeric character
Extends: "Shape"
# 創建兩個橢圓形實例e1,e2
> e1<-new("Ellipse",name="e1")
> e2<-new("Ellipse",name="e2",radius=c(5,1))
# 計算橢圓形面積的函數現實
> setMethod("area", "Ellipse",function(obj,...){
+ print("Area Ellipse Method")
+ pi * prod(obj@radius)
+ })
[1] "area"
# 計算e1,e2兩個橢圓形的面積
> area(e1)
[1] "Area Ellipse Method"
[1] 3.141593
> area(e2)
[1] "Area Ellipse Method"
[1] 15.70796
# 計算橢圓形周長的函數現實
> setMethod("circum","Ellipse",function(obj,...){
+ cat("Ellipse Circum :\n")
+ 2*pi*sqrt((obj@radius[1]^2+obj@radius[2]^2)/2)
+ })
[1] "circum"
# 計算e1,e2兩個橢圓形的周長
> circum(e1)
Ellipse Circum :
[1] 6.283185
> circum(e2)
Ellipse Circum :
[1] 22.65435
6.2 任務二:重構圓形和橢圓形的設計
上一步,我們已經完成了 圓形和橢圓形 的數據結構定義,以及計算面積和周長的方法現實。不知大家有沒有發現,圓形是橢圓形的一個特例呢?
當橢圓形的長半徑和短半徑相等時,即radius的兩個值相等,形成的圖形為圓形。利用這個特點,我們就可以重新設計 圓形和橢圓形 的關系。橢圓形是圓形的父類,而圓形是橢圓形的子類。
如圖所示結構:
# 基類Shape
> setClass("Shape",slots=list(name="character",shape="character"))
# Ellipse繼承Shape
> setClass("Ellipse",contains="Shape",slots=list(radius="numeric"),prototype=list(radius=c(1,1),shape="Ellipse"))
# Circle繼承Ellipse
> setClass("Circle",contains="Ellipse",slots=list(radius="numeric"),prototype=list(radius = 1,shape="Circle"))
# 定義area接口
> setGeneric("area",function(obj,...) standardGeneric("area"))
[1] "area"
# 定義area的Ellipse實現
> setMethod("area","Ellipse",function(obj,...){
+ cat("Ellipse Area :\n")
+ pi * prod(obj@radius)
+ })
[1] "area"
# 定義area的Circle實現
> setMethod("area","Circle",function(obj,...){
+ cat("Circle Area :\n")
+ pi*obj@radius^2
+ })
[1] "area"
# 定義circum接口
> setGeneric("circum",function(obj,...) standardGeneric("circum"))
[1] "circum"
# 定義circum的Ellipse實現
> setMethod("circum","Ellipse",function(obj,...){
+ cat("Ellipse Circum :\n")
+ 2*pi*sqrt((obj@radius[1]^2+obj@radius[2]^2)/2)
+ })
[1] "circum"
# 定義circum的Circle實現
> setMethod("circum","Circle",function(obj,...){
+ cat("Circle Circum :\n")
+ 2*pi*obj@radius
+ })
[1] "circum"
# 創建實例
> e1<-new("Ellipse",name="e1",radius=c(2,5))
> c1<-new("Circle",name="c1",radius=2)
# 計算橢圓形的面積和周長
> area(e1)
Ellipse Area :
[1] 31.41593
> circum(e1)
Ellipse Circum :
[1] 23.92566
# 計算圓形的面積和周長
> area(c1)
Circle Area :
[1] 12.56637
> circum(c1)
Circle Circum :
[1] 12.56637
我們重構后的結構,是不是會更合理呢!!
6.3 任務三:增加矩形的圖形處理
我們的圖形庫,進一步擴充,需要加入矩形和正方形。
- 定義矩形的數據結構,並計算面積和周長
- 定義正方形的數據結構,並計算面積和周長
- 正方形是矩形的特例,定義矩形是正方形的父類,而正方形是矩形的子類。
如圖所示結構:
# 定義矩形Rectangle,繼承Shape
> setClass("Rectangle",contains="Shape",slots=list(edges="numeric"),prototype=list(edges=c(1,1),shape="Rectangle"))
# 定義正方形Square,繼承Rectangle
> setClass("Square",contains="Rectangle",slots=list(edges="numeric"),prototype=list(edges=1,shape="Square"))
# 定義area的Rectangle實現
> setMethod("area","Rectangle",function(obj,...){
+ cat("Rectangle Area :\n")
+ prod(obj@edges)
+ })
[1] "area"
# 定義area的Square實現
> setMethod("area","Square",function(obj,...){
+ cat("Square Area :\n")
+ obj@edges^2
+ })
[1] "area"
# 定義circum的Rectangle實現
> setMethod("circum","Rectangle",function(obj,...){
+ cat("Rectangle Circum :\n")
+ 2*sum(obj@edges)
+ })
[1] "circum"
# 定義circum的Square實現
> setMethod("circum","Square",function(obj,...){
+ cat("Square Circum :\n")
+ 4*obj@edges
+ })
[1] "circum"
# 創建實例
> r1<-new("Rectangle",name="r1",edges=c(2,5))
> s1<-new("Square",name="s1",edges=2)
# 計算矩形形的面積和周長
> area(r1)
Rectangle Area :
[1] 10
> area(s1)
Square Area :
[1] 4
# 計算正方形的面積和周長
> circum(r1)
Rectangle Circum :
[1] 14
> circum(s1)
Square Circum :
[1] 8
這樣,我們的圖形庫,已經支持了4種圖形了!用面向對象的結構來設計,是不是結構化思路很清晰呢!!
6.4 任務四:在基類Shape中,增加shape屬性和getShape方法
接下來,要對圖形庫的所有圖形,定義圖形類型的變量shape,然后再提供一個getShape函數可以檢查實例中的是shape變量。
這個需求,如果沒有面向對象的結構,那么你需要在所有圖形定義的代碼中,都增加一個參數和一個判斷,如果有100圖形,改起來還是挺復雜的。而面向對象的程序設計,就非常容易解決這個需求。我們只需要在基類上改動代碼就可以實現了。
如圖所示結構:
# 重新定義基類Shape,增加shape屬性
> setClass("Shape",slots=list(name="character",shape="character"))
# 定義getShape接口
> setGeneric("getShape",function(obj,...) standardGeneric("getShape"))
[1] "getShape"
# 定義getShape實現
> setMethod("getShape","Shape",function(obj,...){
+ cat(obj@shape,"\n")
+ })
[1] "getShape"
其實,這樣改動一個就可以了,我們只需要重實例化每個圖形的對象就行了。
# 實例化一個Square對象,並給shape屬性賦值
> s1<-new("Square",name="s1",edges=2, shape="Square")
# 調用基類的getShape()函數
> getShape(r1)
Rectangle
是不是很容易的呢!在代碼只在基類里修改了,所有的圖形就有了對應的屬性和方法。
如果我們再多做一步,可以修改每個對象的定義,增加shape屬性的默認值。
setClass("Ellipse",contains="Shape",slots=list(radius="numeric"),prototype=list(radius=c(1,1),shape="Ellipse"))
setClass("Circle",contains="Ellipse",slots=list(radius="numeric"),prototype=list(radius = 1,shape="Circle"))
setClass("Rectangle",contains="Shape",slots=list(edges="numeric"),prototype=list(edges=c(1,1),shape="Rectangle"))
setClass("Square",contains="Rectangle",slots=list(edges="numeric"),prototype=list(edges=1,shape="Square"))
再實例化對象時,屬性shape會被自動賦值
# 實例化一個Square對象
> s1<-new("Square",name="s1",edges=2)
# 調用基類的getShape()函數
> getShape(r1)
Rectangle
下面是完整的R語言的代碼實現:
setClass("Shape",slots=list(name="character",shape="character"))
setClass("Ellipse",contains="Shape",slots=list(radius="numeric"),prototype=list(radius=c(1,1),shape="Ellipse"))
setClass("Circle",contains="Ellipse",slots=list(radius="numeric"),prototype=list(radius = 1,shape="Circle"))
setClass("Rectangle",contains="Shape",slots=list(edges="numeric"),prototype=list(edges=c(1,1),shape="Rectangle"))
setClass("Square",contains="Rectangle",slots=list(edges="numeric"),prototype=list(edges=1,shape="Square"))
setGeneric("getShape",function(obj,...) standardGeneric("getShape"))
setMethod("getShape","Shape",function(obj,...){
cat(obj@shape,"\n")
})
setGeneric("area",function(obj,...) standardGeneric("area"))
setMethod("area","Ellipse",function(obj,...){
cat("Ellipse Area :\n")
pi * prod(obj@radius)
})
setMethod("area","Circle",function(obj,...){
cat("Circle Area :\n")
pi*obj@radius^2
})
setMethod("area","Rectangle",function(obj,...){
cat("Rectangle Area :\n")
prod(obj@edges)
})
setMethod("area","Square",function(obj,...){
cat("Square Area :\n")
obj@edges^2
})
setGeneric("circum",function(obj,...) standardGeneric("circum"))
setMethod("circum","Ellipse",function(obj,...){
cat("Ellipse Circum :\n")
2*pi*sqrt((obj@radius[1]^2+obj@radius[2]^2)/2)
})
setMethod("circum","Circle",function(obj,...){
cat("Circle Circum :\n")
2*pi*obj@radius
})
setMethod("circum","Rectangle",function(obj,...){
cat("Rectangle Circum :\n")
2*sum(obj@edges)
})
setMethod("circum","Square",function(obj,...){
cat("Square Circum :\n")
4*obj@edges
})
e1<-new("Ellipse",name="e1",radius=c(2,5))
c1<-new("Circle",name="c1",radius=2)
r1<-new("Rectangle",name="r1",edges=c(2,5))
s1<-new("Square",name="s1",edges=2)
area(e1)
area(c1)
circum(e1)
circum(c1)
area(r1)
area(s1)
circum(r1)
circum(s1)
通過這個例子,我們全面地了解了R語言中面向對象的使用,和S4對象系統的面向對象程序設計!
在程序員的世界里,世間萬物都可以抽象成對象。
http://blog.fens.me/r-class-s4/