【r<-高级|理论】R-面向对象编程(续)

R-面向对象编程

下面演示如何基于TimeSeries类实现一个WeightHistory类以记录个人的历史体重信息。

> setClass("TimeSeries",
+   representation(
+     data="numeric",
+     start="POSIXct",
+     end="POSIXct"
+ 
+     )
+ )
> setValidity("TimeSeries",
+   function(object) {
+     object@start <= object@end &&
+     length(object@start)==1 &&
+     length(object@end)==1
+   }
+ )
Class "TimeSeries" [in ".GlobalEnv"]

Slots:
                              
Name:     data   start     end
Class: numeric POSIXct POSIXct

创建子类:

> setClass(
+   "WeightHistory",
+   representation(
+     height = "numeric",
+     name = "character"
+  ),
+   contains = "TimeSeries"
+ )

添加实例对象:

> john.doe <- new("WeightHistory", 
+   data=c(170,169,171,168,170,169),
+   start=as.POSIXct("02/14/2019 0:00:00", tz="GMT",
+     format="%m/%d/%Y %H:%M:%S"),
+   end=as.POSIXct("03/28/2019 0:00:00", tz="GMT",
+     format="%m/%d/%Y %H:%M:%S"),
+   height=72,
+   name="John Doe")
> john.doe
An object of class "WeightHistory"
Slot "height":
[1] 72

Slot "name":
[1] "John Doe"

Slot "data":
[1] 170 169 171 168 170 169

Slot "start":
[1] "2019-02-14 GMT"

Slot "end":
[1] "2019-03-28 GMT"

我们还可以通过另外一种方式构建一个体重记录。假设我们已经创建好了一个包含人名和体重的Person类。

> setClass("Person",
+   representation(
+     height = "numeric",
+     name = "character")
+ )

我们可以创建一个基于TimeSeries类和Person类的体重记录类。

> setClass(
+   "AltWeightHistory",
+   contains = c("TimeSeries", "Person")
+ )

可以发现,如果我们已经有了先期的开发经验或者相关类的代码,对新任务进行重构是非常方便的。短短几行代码就搞定了,充分利用了代码的可重复性。这也是OOP在高级语言中如此普遍的一个原因吧。

S4类

我们接下来更深入地探讨构造类的函数。

类的定义

R中使用setClass函数来创建一个新类,格式如下:

setClass(Class, representation, prototype, contains=character(), validity, access, where, version, sealed, package, S3methods=FALSE)

描述

  • Class - 字符串,用来指定新类的名字(这是唯一必需的参数)
  • representation - 列表,列表的每一个元素代表不同的槽的类型,元素名为槽名(可以用"ANY"来指定类型为任意)
  • prototype - 包含各个槽的默认值的对象
  • contains - 字符向量,包含该类继承的父类名
  • validity - 验证该类的对象有效性的函数(默认没有检查),可以后续使用setValidity函数来设置
  • access - 无作用,为了和S-PLUS兼容
  • where - 存储该对象定义的环境
  • version - 无作用,为了和S-PLUS兼容
  • sealed - 逻辑值,表示该类是否还能被setClass按照原来的类名重新定义
  • package - 字符串,指定该类所在的R包名
  • S3methods - 逻辑值,表示是否使用了S3类写这个类

为了简化类的创建,methods包提供了representation以及protype函数。它们在将其他类继承为数据部分、拥有多个父类、或者组合继承类和槽的时候非常有用。

值得注意的是,有些名字是属性的保留字因而不能作为槽名使用,包括"class","comment","dim","dimnames","names","rownames"和"tsp"。

可以使用setIs函数来显式地定义继承关系。

setIs(class1, class2, test=NULL, coerce=NULL, replace=NULL,
     by=character(), where=topenv(parent.frame()), classDef=, extensionObject=NULL, doComplete=TRUE)

可以使用setValidity函数来显式地设置类的验证函数:

setValidity(Class, method, where=topenv(parent.frame()))

R可以定义一个虚类作为多个其他类的父类。如果一个虚类本身不包含任何数据,但是如果你想要创建一批函数用于一批类中,这种方式非常有用。可以通过setClassUnion函数实现:

setClassUnion(name, members, where)
  • name - 新的父类的名字
  • members - 字符向量,指定所有子类的名字
  • where - 新类所在的环境

对象的新建

我们可以通过调用类的new方法新建一个对象。专业术语中称为构造函数。

new(c, ...)

在调用new的时候,我们可以通过指定参数将数据填充到槽中。如果c中存在名为initialize的方法,那么当新的对象被创建后,会立刻调用initialize函数进行初始化。

槽的存取

我们可以使用slot函数或者简化符号@来访问存储对象某个槽中的值,当然也可以用它来赋值。

> john.doe@name
[1] "John Doe"
> slot(john.doe, "name")
[1] "John Doe"

对象的操作

使用is(o, c)函数测试对象o是否是类c的成员。使用函数extend(c1, c2)测试类c1是否继承于类c2

如果要得到对象o包含的所有槽的名称,使用slotNames(o),如果要得到槽的类型,使用getSlots(o)。这两个函数也可以对类使用。

> getSlots("WeightHistory")
     height        name        data       start         end 
  "numeric" "character"   "numeric"   "POSIXct"   "POSIXct" 

> slotNames("WeightHistory")
[1] "height" "name"   "data"   "start"  "end"   
> slotNames("john.doe")
character(0)
> slotNames(john.doe)
[1] "height" "name"   "data"   "start"  "end"  

注意一些差别,有引号和没引号结果是不同的。

方法

泛型函数允许使用同一个函数名来代表很多不同的函数,针对不同的类,调用不同的参数。

设定方法的第一步是创建一个合适的泛型函数,如果该函数还不存在,可以使用setGeneric函数来创建这个泛型方法:

setGeneric(name, def=, group=list(), valueClass=character(),
          where=, package=, signature=, useAsDefault=,
          genericFUnction=, simpleInheritanceOnly=)

要把一个方法关联到某个类(具体而言就是指定泛型函数的signature参数),可以使用setMethod函数:

setMethod(f, signature=character(), definition,
         where = topenv(parent.frame()),
         valueClass=NULL, sealed=FALSE)

方法的管理

methods包包含了很多管理泛型方法的函数。

函数 描述
isGeneric 检查指定的泛型函数是否存在
isGroup 检查指定的分组泛型函数是否存在
removeGeneric 删除某个泛型函数关联的所有方法以及该泛型函数本身
dumpMethod 转存储某个方法到文件
findFunction 根据函数名查找函数对象,返回搜寻列表中的位置或当前顶层环境
dumpMethods 转存储一个泛型函数关联的所有方法
signature 返回在某个指定路径下定义了方法的泛型函数的名称
removeMethods 删除某个泛型函数关联的所有方法
setGeneric 根据指定的函数名创建新的泛型函数

methods包同样包含了很多管理方法的函数。

函数 描述
getMethod, selectMethod 返回某个特定泛型函数和类型标记的方法
existsMethod, hasMethod 检查某个方法(指定了泛型函数名和类型标记)是否存在
findMethod 返回包含了某个方法的包
showMethods 显示关联了某个S4泛型的所有方法

更多的帮助通过library(help="methods")命令获取。

守旧派OOP: S3

如果我们想要用R实现复杂的工程,应该使用S4的类和对象。不幸的是,我们在R中是很难避免S3对象的。比如统计包中的大部分建模工具都是用S3对象实现的。为了能够对这些软件包进行更好地理解、修改和扩展。我们必须了解S3类是如何实现的。

S3的类

S3对象只是原始的R对象加上一些额外的属性(包括一个类名)而已。它没有正式的定义,我们可以手工修改属性甚至类。

之前我们使用了时间序列作为S4的例子,其实在R中已经存在了表示它的S3类,称为ts对象。我们这里创建简单的时间序列对象,查看它的属性以及一些底层对象。

> my.ts <- ts(data=c(1,2,3,4,5), start=c(2009,2), frequency=12)
> my.ts
     Feb Mar Apr May Jun
2009   1   2   3   4   5
> attributes(my.ts)
$tsp
[1] 2009.083 2009.417   12.000

$class
[1] "ts"

> typeof(my.ts)
[1] "double"
> unclass(my.ts)
[1] 1 2 3 4 5
attr(,"tsp")
[1] 2009.083 2009.417   12.000
> attributes(my.ts)
$tsp
[1] 2009.083 2009.417   12.000

$class
[1] "ts"

可以发现ts对象只不过是一个数值向量加上classtsp这两个属性。class属性起始只是ts对象的类名。我们无法像S4对象中操作槽来提取S3对象的属性。

> my.ts@tsp
错误: 非S4类别的对象(类别为"ts")没有"tsp"这样的槽

S3方法

S3的泛型函数是通过命名约定来实现的。以下是步骤:

  1. 为泛型函数挑选一个名字,这里我们命名为gname
  2. 新建一个名为gname的函数,在gname的函数体中,调用UseMethod("gname")
  3. 为每一个想要使用gname的类创建一个名为gname.classname的函数,该函数的第一个参数必须是该对象的类名classname

一个现成的例子是plot函数:

> plot
function (x, y, ...) 
UseMethod("plot")
<bytecode: 0x1851c30>
<environment: namespace:graphics>

在调用plot的时候,plot将会调用UseMethod("plot")UseMethod会查看x对象的类,然后查找名为plot.class的函数,然后调用该函数。

比如给我们之前定义的TimeSeries类添加一个plot方法。

> plot.TimeSeries <- function(object, ...) {
+   plot(object@data, ...)
+ }

在S4的类中使用S3的类

我们不能直接指定S3的类到S4的槽。如果想要做到,我们需要基于S3的类创建一个S4的类。一个简单的方式是使用setOldClass函数:

setOldClass(Classes, prototype, where, test=FALSE, S4Class)

查找隐藏的S3方法

有时候我们会发现一些包的作者会选择隐藏单个方法,而把方法的实现封装在包中。这样可以鼓励用户去使用泛型函数。

> library(lattice)
> methods(histogram)
[1] histogram.factor*  histogram.formula* histogram.numeric*
see '?methods' for accessing help and source code

有时候我们可能需要找回这些隐藏的方法(想要查看源代码),这时候可以使用getS3method函数。例如,想要取到histgram.formula中的代码,可以使用以下命令:

> getS3method(f="histogram", class="formula")

或者使用getAnywhere函数:

> getAnywhere(histogram.formula)

学习整理自《R核心技术手册》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容