R语言有两种不同的OOP机制,分别是从其前身S语言继承而来的S3 Object和S4 Object,其中S4 Object更加的正式、也是现在用于开发的主力军,所以本文就从S4 Object谈起,并在最后讨论一下古老的S3 Object。
那我们就开始吧!首先我们来设计一个时间序列类,在它的内部,需要包含主数据、起始时间与截止时间、取样间隔这些数据。在R中我们可以定义如下:
setClass("TimeSeries", representation( data = "numeric", start = "POSIXct", end = "POSIXct" ) )
在这段代码中,data/start/end用于存放数据,称作“槽(slot)”。
现在我们已经定义了一个类,我们就来创建一个TimeSeries对象吧!
My_TimeSeries <- new("TimeSeries", data = c(1,2,3,4,5,6), start = as.POSIXct("01/12/2015 0:00:00", tz = "GMT", format = "%m/%d/%Y %H:%M:%S"), end = as.POSIXct("12/04/2015 0:00:00", tz = "GMT", format = "%m/%d/%Y %H:%M:%S") )
与其他OOP语言类似,R中新建对象的通用函数也叫”new”,但只能用于新建S4对象。现在我们来看看我们刚刚新建的My_TimeSeries:
> My_TimeSeries An object of class "TimeSeries" Slot "data": [1] 1 2 3 4 5 6 Slot "start": [1] "2015-01-12 GMT" Slot "end": [1] "2015-12-04 GMT" >My_TimeSeries@data #可以使用"@"符号来引用类中的内容 [1] 1 2 3 4 5 6 > My_TimeSeries@start [1] "2015-01-12 GMT"
但是,一个bug出现了:如果用户把start和end颠倒、或者把end误输为一个比start还靠前的时间,这样会造成时间序列变得没有意义。R语言提供了一个新建对象时的检验机制,只需要在setValidity函数中设置一下:
setValidity("TimeSeries", function(object) { object@start < object@end && length(object@start) == 1 && length(object@end) == 1 } )
现在我们来试一下定义一个end在start前的TimeSeries对象:
> bad_TimeSeries <- new("TimeSeries", + data=c(7, 8, 9, 10, 11, 12), + start=as.POSIXct("07/01/2009 0:06:00", tz="GMT", + format="%m/%d/%Y %H:%M:%S"), + end=as.POSIXct("07/01/1999 0:11:00", tz="GMT", + format="%m/%d/%Y %H:%M:%S") + ) Error in validObject(.Object) : 类别为“TimeSeries”的对象不对: FALSE
同时,也可以使用validObject()函数来检验一个对象是否有效。
> validObject(My_TimeSeries) [1] TRUE
其实,在定义类的时候也可以通过validity参数定义该类的合法性判断,如:
setClass("anotherTimeSeries", representation( data = "numeric", start = "POSIXct", end = "POSIXct" ), validity = function(object){ #定义时加上合法性判断 object@start < object@end && length(object@start) == 1 && length(object@end) == 1 } )
这样定义与前面的先定义类、后定义合法性检测的做法是等价的,只是把两步都集成到了setClass()函数中。
下面我们来看一下R语言中函数的多态性。我们先从重载一个通用函数summary()开始:
> summary(My_TimeSeries) Length Class Mode 1 TimeSeries S4 > setMethod("summary", #重载summary + signature = "TimeSeries", + definition = function(object){ + print( paste(object@start, " to ", object@end, + sep = "", collapse = "")) + print( paste(object@data, sep = ";", collapse = "")) + } + ) 从the global environment里的程辑包‘base’为‘summary’建立新的泛型函数 [1] "summary" > summary(My_TimeSeries) [1] "2015-01-12 to 2015-12-04" [1] "123456"
我们可以看到,当我们没对TimeSeries类重载summary()函数的时候,summary(My_TimeSeries)只提供了一些简要的信息。而在我们重载后,它就可以按照我们的要求输出信息了。
同时,我们知道运算符在R中也是相当于函数调用,也就是a+b与‘+’(a,b)是等价的。通过这个特性我们就可以重载R语言的运算符。
#这样,就可以使用My_TimeSeries[i]了 setMethod("[", signature("TimeSeries"), definition = function(x, i, j, ..., drop){ return(x@data[i]) } ) > My_TimeSeries[3] [1] 3
而要新建一个泛型函数,则可以使用setGeneric()函数来定义、再用setMethod()函数来实现它对各种类的功能。
setGeneric("increment", #建立一个函数名为increment的泛型函数 def = function(object, step, ...) standardGeneric("increment") ) #对TimeSeries类重载increment函数,使之返回object[step+1]-object[1]的数值 setMethod("increment", signature = "TimeSeries", def = function(object, step, ...){ return(object[step+1] - object[1]) } ) #对numeric类重载increment函数,使之返回object[step-1]-object[1]的数值 setMethod("increment", signature = "numeric", def = function(object, step, ...){ return(object[step-1] - object[1]) } )
那么我们的定义的这个increment()泛型函数是否有效呢?我们来检验一下:
> increment(My_TimeSeries,3)#根据我们的定义,应当返回My_TimeSeries[4]-My_TimeSeries[1] [1] 3 > vec <- vector("numeric", length = 6) > vec <- c(1:6) #现在我们来看一下对numeric型向量,运行increment()函数的结果 > increment(vec, 3) [1] 1
在泛型函数的最后,让我来写一个错误的示范:
> setMethod("anotherIncrement", + signature = "TimeSeries", + def = function(object, step, ...){ + return(object[step+1] - object[1]) + } + ) Error in setMethod("anotherIncrement", signature = "TimeSeries", def = function(obj ect, : 函数‘anotherIncrement’没有定义
在这个例子中,由于我没有定义anotherIncrement()为泛型函数,直接调用setMethod()就会报错——因为你根本没有定义它!
而如果我们对某个S4泛型函数不太了解,不知道它可以用于哪些类时,就可以使用showMethods()函数来看得到它可以作用的对象。
> showMethods(increment) Function: increment (package .GlobalEnv) object="integer" (inherited from: object="numeric") object="numeric" object="TimeSeries"
下面我们来看看类的派生:现在我们想要一个类来记录个人体重的变化情况。我们希望记录下个人的姓名和身高,其他的信息直接使用TimeSeries类记录就可以了,我们可以定义如下:
setClass("WeightHistory", #派生 representation( height = "numeric", name = "character" ), contains = "TimeSeries" )
现在我们来创建一个WeightHistory类的对象,来储存AlexDannel的体重数据
AlexDannel <- new("WeightHistory", data = c(120,118,119,123,121,119), start = as.POSIXct("07/01/2015 0:00:00", tz = "GMT", format = "%m/%d/%Y %H:%M:%S"), end = as.POSIXct("12/01/2015 0:00:00", tz = "GMT", format = "%m/%d/%Y %H:%M:%S"), height = 166, name = "Alex Dannel" )
有没有和新建TimeSeries序列对象的时候很像呢?
我们还可以用另一种方法定义WeightHistory类,那就是先定义一个Person类,里面包含name和height的slot(槽),然后直接从Person类和TimeSeries类继承出来。
setClass("Person", representation( height = "numeric", name = "character" ) ) setClass("anotherWeightHistory", contains = c("TimeSeries", "Person") )
虚类:差点忘了还有虚类这个东东~ 其实在R中定义虚类也特别简单
setClass("Cat", #定义一个cat类,让NamedThing作为它和Person的虚基类 representation( breed = "character", name = "character" ) ) setClassUnion("NamedThing", #定义虚基类 c("Person", "Cat") )
下面我们来简单讨论一下S3 Object类吧!其实S3类要比S4类更加“随意”,而S3类与JavaScript这种基于原型的(prototype-based)类非常相似。
#在S3类中,早已有对TimeSeries的定义,ts类对现在的R也是可用,现在我们来创建一个ts对象 my.ts <- ts(data=c(1, 2, 3, 4, 5), start=c(2009, 2), frequency=12)
需要注意的是,S3类中不能使用@来取slot中的值。
而要创建一个S3类的对象,则可以使用attr()函数或者structure()函数:
> x<-1 #通过attr创建 > attr(x,'class')<-'foo' > x [1] 1 >attr(,"class") [1] "foo" >class(x) "foo" >otype(x) #检查x的类型 "S3"
#通过structure()创建 > y <- structure(2, class = "foo") > y [1] 2 attr(,"class") [1] "foo" > class(y) [1] "foo" > otype(y) [1] "S3"
而要定义一个S3泛型函数,也是比较灵活的——只需如下三步:
1. Pick a name for the generic function. We’ll call this gname.
2. Create a function named gname. In the body for gname, call UseMethod(“gname“).
3. For each class that you want to use with gname, create a function called
gname.classname whose first argument is an object of class classname.——《R in a nutshell》, 2nd Edition
以plot为例,我们想要重载plot函数,使之可以对TimeSeries类绘图,就可以这样定义:
plot.TimeSeries <- function(object, ...){ plot(object@data, ...) }
现在,你就可以直接通过plot(My_TimeSeries)来画出图像了!
而如果想要查看S3泛型函数可以用于哪些类时,就可以使用methods()函数来看得到它可以作用的对象(因为用S4的showMethods()函数会报错→_→)。
> methods(plot) [1] plot.acf* plot.data.frame* plot.decomposed.ts* plot.default plot.dendrogram* plot.density* [7] plot.ecdf plot.factor* plot.formula* plot.function plot.hclust* plot.histogram* [13] plot.HoltWinters* plot.isoreg* plot.lm* plot.medpolish* plot.mlm* plot.ppr* [19] plot.prcomp* plot.princomp* plot.profile.nls* plot.raster* plot.shingle* plot.spec* [25] plot.stepfun plot.stl* plot.table* plot.trellis* plot.ts plot.tskernel* [31] plot.TukeyHSD* see '?methods' for accessing help and source code
我们甚至可以通过gets3method()函数来查看S3泛型函数的源代码:
library(lattice) getS3method("histogram", class = "formula")
到这里,笔者所知的R语言面向对象编程就介绍完毕了。由于作者水平有限,许多系统函数的参数没能系统的描述。读者不妨仔细阅读setClass、setGeneric、setMethod、new、method等函数的帮助页面,以加深对R语言OOP的理解。