注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

云之南

风声,雨声,读书声,声声入耳;家事,国事,天下事,事事关心

 
 
 

日志

 
 
关于我

专业背景:计算机科学 研究方向与兴趣: JavaEE-Web软件开发, 生物信息学, 数据挖掘与机器学习, 智能信息系统 目前工作: 基因组, 转录组, NGS高通量数据分析, 生物数据挖掘, 植物系统发育和比较进化基因组学

网易考拉推荐

R中面向对象编程方法  

2009-12-15 22:31:51|  分类: R&Bioconductor |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

R中面向对象编程方法

http://cos.name/2009/07/studying-notes-on-oop-in-r/

By 胡荣兴 @ 2009/07/04
关键词R, S4, 面向对象 分类统计软件
作者信息
版权声明:本文版权归原作者所有,未经许可不得转载。原文可能随时需要修改纰漏,全文复制转载会带来不必要的误导,若您想推荐给朋友阅读,敬请以负责的态度提供原文链接;点此查看如何在学术刊物中引用本文
常规引用方式
胡荣兴. R中面向对象编程方法. 统计之都, 2009.07. URL: http://cos.name/2009/07/studying-notes-on-oop-in-r/.
BibTeX引用
@ARTICLE{,
AUTHOR = {胡荣兴},
TITLE = {R中面向对象编程方法},
JOURNAL = {统计之都},
YEAR = {2009},
month = {07},
URL = {http://cos.name/2009/07/studying-notes-on-oop-in-r/},
}

R主要面向统计计算,似乎很少会用到面向对象的编程方法。但在统计计算中,在下列情形中使用面向对象的编程方法可以编程更有效率。
1)当需要用一种新的方式来表示数据,该方式与已有的数据类型有区别的时候。
2)当需要一个新的函数,该函数可以根据不同的参数类型做出不同的反应的时候。
在R中,经常需要定义一个新的函数,并且定义一个新的函数也是一项繁重的工作。相反,较少去定义一个新的类。但有时候定义一个类是一个很关键的步骤。一个 类通常决定了如何对对象进行处理,决定了对象中应当包含什么样的信息。甚至有时候,类的定义决定你的项目的成败。(本文的PDF版本:R中的面象对象编程学习笔记)

1. 旧的实现方法

R的面向对象(OOP)是基于泛型函数(generic function)的,而不是基于类层次结构。什么是R中的泛型函数?R中的面向对象是怎么样的呢?我们首先看下面来自使用R编写统计程序,第3部分:可重用和面向对象中的一个实例。

下面我们首先来创建一个泛型函数whoAmI( )

# 创建泛型函数
> whoAmI <- function(x, ...) UseMethod("whoAmI")
> whoAmI.foo <- function(x) print("I am a foo")
> whoAmI.bar <- function(x) print("I am a bar")
> whoAmI.default <- function(x) print("I don't know who I am")

在R中,每个对象都属于0个或1个或多个类,我们可以用class( )函数查看对象所属的类。

> a = 1:10
> b = 2:20
> class(a)
[1] "integer"
> class(b)
[1] "integer"

有了泛型函数和类之后,我们就可以看到R中的面泛型函数是如何工作的

> whoAmI(a) # a的类为“integer”,没有为该类定义方法,
# 就执行whoAmI.default方法
[1] "I don't know who I am"
> attr(a,'class') <- 'foo' # 用attr( )函数将a的类指定为“foo”
> class(a)
[1] "foo"
> attr(b,'class')<-c('baz','bam','bar') #指定b的属于三个类“baz”“bam”“bar”
> class(b)
[1] "baz" "bam" "bar"
> whoAmI(a) # 现在a属于“foo”类,执行whoAmI.foo( )
[1] "I am a foo"
> whoAmI(b) # b虽然属于三个类,但只对“bar”类定义了
# 方法,所以就执行whoAmI.bar()
[1] "I am a bar"
> attr(a,'class') <- 'bar' # 改变a的类
> class(a)
[1] "bar"
> whoAmI(a)
[1] "I am a bar"

上面的例子中提到了一个对象同时属于多个类的情况,再看下面的一个补充实例。

> meth1 <- function(x) UseMethod("meth1")
> meth1.Mom <- function(x) print("Mom's meth1") # meth1对类“Mom”
> meth1.Dad <- function(x) print("Dad's meth1") # 和类“Dad”都定义了方法
> meth2 <- function(x) UseMethod("meth2") # meth2只对类
> meth2.Dad <- function(x) print("Dad's meth2") # “Dad”定义了方法
> attr(a,'class') <- c('Mom','Dad')
> meth1(a) # 执行了meth1.Mom( ),因为Mom在前。
[1] "Mom's meth1"
> meth2(a)
[1] "Dad's meth2"

包含祖先的一个例子。

#包含祖先
char0 = character(0)
makeMRO <- function(classes = char0, parents = char0) {
# Create a method resolution order from an optional
# explicit list and an optional list of parents
mro <- c(classes)
for (name in parents) {
mro <- c(mro, name)
ancestors <- attr(get(name), "class")
mro <- c(mro, ancestors[ancestors != name])
}
return(mro)
}
NewInstance <- function(value = 0, classes = char0,
parents = char0) {
# Create a new object based on initial value,
# explicit classes and parents (all optional)
obj <- value
attr(obj, "class") <- makeMRO(classes, parents)
return(obj)
}
MaternalGrandma <- NewInstance()
PaternalGrandma <- NewInstance()
Mom <- NewInstance(classes = "Mom", parents = "MaternalGrandma")
Dad <- NewInstance(0, classes = c("Dad", "Uncle"),
"PaternalGrandma")
Me <- NewInstance(value = "Hello World", "Me", c("Mom",
"Dad"))
> print(Me)
[1] "Hello World"
attr(,"class")
[1] "Me" "Mom" "MaternalGrandma" "Dad"
[5] "Uncle" "PaternalGrandma"

从上面的例子,我们可以看到,R中的“面向对象”是以类和泛型函数为基础,类可以多重继承类,泛型函数可以分派到任意的参数(签 名,signature)集合,泛型函数将不同的方法聚集到一起,由R根据函数的对象的类型来决定选择执行哪个方法。在某些情况下,类和方法是不同的概 念,我样要区别对待。

2.类

用setClass来定义一个类。

setClass(Class, representation, prototype, contains=character(),

validity, access, where=1, version=FALSE)

其中:

Class 字符串,类的名称
representation 新的类的接口,或者该类扩展的其它类。通常是对representation函数的一个调用。
prototype 为接口提供的默认的数据对象。如果在构造一个新的类的时候,不明确地改写,那么新的实例将采用这些数据。
contains 该类所扩展的类,所有扩展的类的新的实例都会继承其所有父类的接口
validity, access, version 与S-Plus兼容的控制参数
where 存储或移除定义的元数据的环境

定义了一个类后,就可以用函数new()来生成一个类的实例(instances)。类定义了一个对象的结构。类的实例代表了对象本身。一个子类是 对其父类的扩展,一个新的类将包含其父类的所有接口。在创建一个新的类时,接口的名字不能重复(包括其直接或间接的父类)。下面的代码将创建两个类foo 和bar,再创建它们的一个子类baz。

> library(methods)
> setClass("foo",representation(a="character",b="numeric"))
[1] "foo"
> setClass("bar", representation(d = "numeric", c = "numeric"))
[1] "bar"
> setClass("baz", contains = c("foo", "bar"))
[1] "baz"
> getClass("baz")
Class "baz" [in ".GlobalEnv"]
Slots:
Name: a b d c
Class: character numeric numeric numeric
Extends: "foo", "bar"

现在我们可以生成类baz的一个实例。

> x <- new("baz", a = "xxx", b = 5, c = 10)
> x
An object of class "baz"
Slot "a":
[1] "xxx"
Slot "b":
[1] 5
Slot "d":
numeric(0)
Slot "c":
[1] 10

可以通过操作符@访问接口中的数据。但最好的办法是不直接访问接口的数据,而是定义一些特殊的方法来访问这些数据。

> x@a
[1] "xxx"

3. 虚类

虚类就是不会用来生成实例的类。它们是用来将拥有不同的representations的类(它们不能互相继承)链接起来, 可以通过虚类为这些不同的representations提供相似的功能。一个标准的做法就是建立一个虚类,然后用其它类来扩展它。

在实际中,可以通过以下几种不同的方式来使用该虚类。

1. 虚类的方法将用到其所有的扩展类中。

2. 新的类的接口将与虚类的接口的数据类型一致,这样可以使接口的多样化。

3. 虚类的接口将会出现在它的所有扩展类中。

下面我们用类来表示,一个树状图。一个树状图的结点有三个值:高度、左结点、右结点。终端结点与之不同,有一个高度和一个值(也有可能是另一个对象的实例。)我们可以用一个虚类dendNode来表示该结构。我们可以再定义两个该类的扩展类:终端和非终端结点。

> setClass("dendNode")
[1] "dendNode"
> setClass("dnode", representation(left = "dendNode", right = "dendNode",
+ height = "numeric"), contains = "dendNode")
[1] "dnode"
> setClass("tnode", representation(height = "numeric", value = "numeric",
+ label = "character"), contains = "dendNode")
[1] "tnode"

现在我们就可以创建树了,它的结点或者是终端结点或者是非终端节点。在类dnode中,它的左节点和右节点都是dendNode类的实例,而 dnode类本身又是dendNode的扩展。在此例中虚类被用来允许两个不同的类:左节点和右节点。这种设计使得可以重复使用树变得简单。

在实际中,我们经常会出现这样的问题,希望一个接口或者是一个列表或者是一个空对象(NULL( ))。因为一个NULL( )本身不是一个列表,所以它们不能共享同一个接口。我们可以创建一个虚拟类来扩展列表类和NULL( )类。

有两种方法可以建立虚类。第一种方法,与上面一样,用setClass( )建立一个没有representation的类。第二种方法,在参数representation中包括类“VIRTUAL”

> setClass("myVclass", representation(a = "character", "VIRTUAL"))
[1] "myVclass"
> getClass("myVclass")
Virtual Class "myVclass" [in ".GlobalEnv"]
Slots:
Name: a
Class: character

4. 初始化和原型

有两种办法可以用来控制类的实例生成的对象的初始值。

第一种方法:可以用参数prototype来控制生成的实例的值。使用该参数,可以将任何接口赋初值。

> setClass("xx", representation(a = "numeric", b = "character"),
+ prototype(a = 3, b = "hi there"))
[1] "xx"
> new("xx")
An object of class "xx"
Slot "a":
[1] 3
Slot "b":
[1] "hi there"

第二种方法是为该类指定一个initialize方法,用该方法为类的实例赋值。

> setMethod("initialize", "xx", function(.Object, b)
+ {
+ .Object@b <- b
+ .Object@a <- nchar(b)
+ .Object
+ })
[1] "initialize"
> new("xx", b = "yowser")
An object of class "xx"
Slot "a":
[1] 6
Slot "b":
[1] "yowser"

注意在第二种方法中,应当在initialize方法中返回该对象。

5. 泛型函数和方法

面向对象编程的一个重要方面就是泛型函数。泛型函数实际上就是一个分派机制。方法就是一种特殊的函数,它能根据特定的输入对象执行需要执行的任务。 泛型函数的工作就是针对不同的输入参数决定什么样的方法被执行。一旦对泛型函数进行了签名,那么泛型函数就不能添加新的参数了。在设计泛型函数的参数时, 要将可能用到的参数都包括进来。

6. 访问子函数

我们可以通过@运算符来访问接口,但在实际编程中并不建议这么做。我样可以通过访问子函数来访问接口。

> setClass("foo", representation(a = "ANY")) #新建一个类foo
[1] "foo"
#申明一个标准的泛型函数a
> if (!isGeneric("a")) {
if (is.function("a"))
fun <- a
else fun <- function(object) standardGeneric("a")
setGeneric("a", fun)
}
> setMethod("a", "foo", function(object) object@a)
[1] "a"
> b <- new("foo", a = 10)
> a(b)
[1] 10

R主要面向统计计算,似乎很少会用到面向对象的编程方法。但在统计计算中,在下列情形中使用面向对象的编程方法可以编程更有效率。

1)当需要用一种新的方式来表示数据,该方式与已有的数据类型有区别的时候。

2)当需要一个新的函数,该函数可以根据不同的参数类型做出不同的反应的时候。

在R中,经常需要定义一个新的函数,并且定义一个新的函数也是一项繁重的工作。相反,较少去定义一个新的类。但有时候定义一个类是一个很关键的步骤。一个类通常决定了如何对对象进行处理,决定了对象中应当包含什么样的信息。甚至有时候,类的定义决定你的项目的成败。

我们来看下面的一个例子。在该例中,我们将定义一个名为whatIs( )的函数,该函数返回对象的类型。

> whatIs <- function(object) data.class(object)
> whatIs(1:10)
[1] "numeric"
> whatIs(matrix())
[1] "matrix"
> whatIs(whatIs)
[1] "function"

上面的程序好像太简单,我们还可以打印出对象的长度。

> whatIs <- function(object) cat("类:",
+ data.class(object),"\n长度:",length(object),"\n")
> whatIs(1:10)
类: numeric
长度: 10
> whatIs(matrix())
类: matrix
长度: 1
> whatIs(whatIs)
类: function
长度: 1
> A<-matrix(1:6,c(2,3))
> A
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6
> whatIs(A)
类: matrix
长度: 6

现在看来,又改进了一步,但是还是有点不尽如人意的地方,这里给出了函数的长度,意义不大。对于矩阵,我们更想知道它的行数和列数。所以我们需要为function类和matrix类单独定义函数,才能得到我们期望的信息:

> whatIs.function <- function(object) cat("类:",
+ data.class(object),"\n")
> whatIs.function(whatIs)
类: function
> whatIs.matrix <- function(object) cat("类:",
+ data.class(object),"\n",nrow(object),"行",ncol(object),"列\n")
> whatIs.matrix(A)
类: matrix
2 行 3 列

7. 定义方法

在上面的例子中,为了得到我们想要的信息,我们定义了三个功能类似的函数,这样程序比较散乱,使用起来也比较繁琐。我们希望通过一个统一的函数名 whatIs( )来代表上述三个函数,然后根据输入的参数的类型(function, matrix或其它)来决定使用哪一个函数(whatIs.function( ), whatIs.function( )或whatIs( ) )。要完成该工作,我们可以使用函数setMethod( )。

> setMethod("whatIs","function", whatIs.function)
用函数"whatIs"来建立新的同屬函数
[1] "whatIs"
> setMethod("whatIs","matrix", whatIs.matrix)
[1] "whatIs"
> whatIs(1:10)
类: numeric
长度: 10
> whatIs(whatIs)
类: standardGeneric
> whatIs(A)
类: matrix
2 行 3 列

现在我们看到,我们只用一个函数whatIs( )就可以根据参数的类型执行不同的程序代码。这里的函数whatIs( )我们称之为泛型函数(上面的输出显示为“standardGeneric”),分别针对不同类型的对象的三个不同的程序功能称为方法(method)。

setMethod( )函数的三个参数分别告诉setMethod对什么泛型函数指定方法,执行该方法对应的参数的类型,执行的方法。其中第二项称为签名(signature)。

showMethods( ) 查看为函数函数定义了哪些方法。

dumpMethod( ) 提取为泛型函数的某个方法。

附录: R中的基本类

### The following are all basic vector classes.
### They can appear as class names in method signatures,
### in calls to as(), is(), and new().
"character"
"complex"
"double"
"expression"
"integer"
"list"
"logical"
"numeric"
"single"
"raw"
### the class
"vector"
### is a virtual class, extended by all the above
### The following are additional basic classes
"NULL" # NULL objects
"function" # function objects, including primitives
"externalptr" # raw external pointers for use in C code
"ANY" # virtual classes used by the methods package itself
"VIRTUAL"
"missing"
## The following class names can appear in method signatures,
## as the class in as() and is() expressions, and, except for
## the classes commented as VIRTUAL, in calls to new()
"matrix"
"array"
"ts"
"structure" ## VIRTUAL
The specific classes all extend class "structure", directly, and class "vector", by class "structure".

(由于不习惯在网站上编辑,更多内容请查看:R中的面象对象编程学习笔记)

相关文章

6 Responses to “ R中面向对象编程方法 ”

  1. 胡荣兴 on 2009/07/04 at 11:27

    这段时间都没有来COS了,上面更新了好多巨著都没有时间看,以后估计时间会更少,太懒了,得学会好好利用时间。
    用网页发布文章始终不爽。
    有没有学微观计量经济学的朋友啊?可以联系我,有问题向各路高手请教。

  2. 邱怡轩 on 2009/07/04 at 14:17

    之前确实没有注意过这些,学习了!

  3. 栾生 on 2009/07/07 at 09:15

    太棒了,这正是我需要的。还有上一篇的制作地图,也很爽。都是好文章啊!

  4. zhangwx on 2009/09/09 at 17:53

    请问R的底层是用什么语言写的?
    C++吗

    • 谢益辉 on 2009/09/10 at 04:52

      你用R分析一下R的源代码就知道了,R主要是用R写的(这句话看起来很崩溃?),加上一部分C语言,外加少量的Fortran以及其它语言:

      # 分析R源文件的扩展名(本方法可能不精确!)
      > x = list.files('R-2.9.2/',recursive = TRUE)
      # 正则表达式:去掉不含.的文件名,将*.替换为空,剩下的是扩展名
      > sort(table(gsub('.*\\.','',x[grep('\\.',x)])))

      3 ac aux bat bib
      1 1 1 1 1
      bot charset common dat dif
      1 1 1 1 1
      doc ftn fw guess ico
      1 1 1 1 1
      install jpeg LIB m modreg
      1 1 1 1 1
      notes packages portsrc rej Rnw
      1 1 1 1 1
      rpath Rterm rtest rtf stp
      1 1 1 1 1
      sub tiff top ts unix
      1 1 1 1 1
      windows zi zip 0 1
      1 1 1 2 2
      bmp csv db header hide
      2 2 2 2 2
      Rin S sed sin site
      2 2 2 2 2
      tex y manifest rda sty
      2 2 3 3 3
      css isl Rd~ def fd
      4 4 4 5 5
      jpg R~ sh eps tab
      5 5 5 6 6
      java mk txt class pl
      7 7 7 8 8
      pm rc texi tcl gz
      10 10 10 11 12
      tgz m4 Snw html enc
      12 13 13 14 18
      pdf pot gmo save f
      19 20 23 42 45
      win afm in mo h
      45 87 97 149 167
      po c R Rd
      170 511 718 1276

      其中Rd是R的帮助文档源文件,如果我没记错的话,po、mo和pot是语言文件。

  评论这张
 
阅读(884)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2016