[关闭]
@evilking 2018-03-03T16:10:56.000000Z 字数 6573 阅读 1255

R基础篇

面向对象编程

R语言同样是一门面向对象的语言,你接触到的R中的所有东西都是对象

我们知道面向对象的特性就是封装、多态和继承,本篇我们会介绍R中的S3类和S4类的创建和使用

封装,即把独立但相关的数据项目打包为一个类的实例;封装可帮助你跟踪相关的变量,提高清晰度

多态,这意味着相同的函数使用不同类的对象时可以调用不同的操作。例如使用print()调用特定类的对象会调用适合此类的打印功能。多态可以促进代码可重用性

继承,即允许把一个给定的类的性质自动赋予为其下属的更多特殊化的类

S3类

一个S3类包含一个列表,再附加上一个类名属性和调度(dispatch)的功能。而后者使泛型函数(generic function)的使用变成可能。S4类是后来开发出来的,目的是增加安全性,以避免意外地访问不存在的类组件

创建

S3类的结构如同用一个列表将各个成员变量堆砌起来,列表中的每个组件就是这些成员变量。"类"属性通过attr()或者class()函数手动设置,然后再定义各种泛型函数的实现方法

这里编写一个简单的S3类来说明,假设我们要自定义一个员工类,这个类有三个成员变量,分别是姓名name,工资salary,是否属于员工联盟union

> emp <- list(name = "Joe", salary = 55000, union = T)  #先用list包装一个列表对象

> class(emp) <- "employee"  #为这个列表对象设置类属性

> attributes(emp)  #可查看该对象的属性
$names
[1] "name"   "salary" "union" 

$class
[1] "employee"

> emp    # 在终端中也可以直接输入对象名字
$name
[1] "Joe"

$salary
[1] 55000

$union
[1] TRUE

attr(,"class")
[1] "employee"

> str(emp)   #查看类的内部结构
List of 3
 $ name  : chr "Joe"
     $ salary: num 55000
 $ union : logi TRUE
 - attr(*, "class")= chr "employee"

> emp$name    #使用"$"符合提取成员变量的值
[1] "Joe"
>

按照上面S3类的描述,先创建一个列表对象emp,该列表中的每个组件即为要创建类的成员变量,如name, salary, union等,然后给这个列表对象emp加上类属性,例如class(emp)<-"employee",这样一个简单的类就创建完成了,只是现在还没为这个"employee"类创建泛型函数


创建成员函数

本质上对象emp打印时是被当做一个列表打印出来的,现在我们为这个类创建自己的打印方法,体现出多态特性:

> print.employee <- function(wrkr){
+     cat(wrkr$name, "\n")
    +     cat("salary", wrkr$salary, "\n")
+     cat("union member", wrkr$union, "\n")
+ }

> print(emp)  #定向到类自己的打印方法
Joe 
salary 55000 
union member TRUE 

> emp    #打印方法已重写
Joe 
salary 55000 
union member TRUE 

上面创建函数的名字为 方法名+"."+类名,如print.employee,函数的参数wrkr为该类的实例对象,这样就可以为该类创建成员函数了,若该函数存在,则重写,若不存在,则新创建

如果想看泛型函数有哪些实现方法,可以使用methods()函数,例如
> print
function (x, ...)
UseMethod("print")
<bytecode: 0x102176190>
<environment: namespace:base>

> methods(print)
[1] print.acf*
[2] print.anova*
[3] print.aov*
[4] print.aovlist*
[5] print.ar*
[6] print.Arima*
......
[80] print.ecdf*
[81] print.employee
[82] print.factanal*
[83] print.factor
.......

可以看到我们刚刚实现的print.employee函数也出现在print泛型函数的实现方法列表中


继承

继承的思想是在已有类的基础上创建新的类。

例如我们在前面employee类的基础上,创建一个针对小时工的新类"hrlyemployee":

> hemp <- list(name = "Kate", salary = 6000, union = F, hrsthismonth = 2)

> class(hemp) <- c("hrlyemployee","employee")  #继承

> hemp
Kate 
salary 6000 
union member FALSE 
>

这个例子第一句是创建了一个列表对象hemp,其中多了一个组件为hrsthismonth

第二句是设置类属性,但是这里是把一个二元向量设置进去了,这种写法就是继承,表示创建类属性"hrlyemployee",并由类"hrlyemployee"继承类"employee";如果这个向量后面还有多个类名,则表示依次继承下去

第三句是调用对象hemp的打印方法,由继承的目的可以知道得到上述打印的结果是显而易见的,但是内部实现的原理过程是怎么样的呢?

直接键入hemp即可调用print(hemp),就会调用UseMethod(),去查找"hrlyemployee"类的打印方法,这是因为"hrlyemployee"是hemp的两个类名称的第一个,结果没有找到对应的方法,所以UseMethod()尝试查找另一个类"employee"对应的打印方法,找到print.employee(),然后执行该函数

由于S3类的设计本质上是一个列表,可以随意增加新的组件,所以一些程序员认为S3类不具有面向对象编程固有的安全性,举个例子:
> str(emp)
List of 3
$ name : chr "Joe" $ salary: num 55000
$ union : logi TRUE
- attr(*, "class")= chr "employee"

> emp$name
[1] "Joe"

> emp$name_wrong
NULL

类employee有成员变量name,但是没有成员变量name_wrong,当提取name成员变量时可以成功,提取name_wrong时应该要报错的,但是这里可以有返回值NULL,并没有报错

这就是S3类存在的问题,于是就有了S4类的出现


S4类

这里总结一下R中S3类与S4类的区别:

操作 S3类 S4类
定义类 在构造函数的代码中隐式定义 setClass()
创建对象 创建列表,设置类属性 new()
引用成员变量 $ @
实现泛型函数f() 定义f.classname() setMethod()
声明泛型函数 UseMethod() setGeneric()

创建S4类

可以调用setClass()来定义一个S4类,继续使用前面创建S3类时的例子:

> setClass("employee",
+          representation(
+            name = "character",
+            salary = "numeric",
+            union = "logical"
+          )
+ )    #定义了一个类employee

> joe <- new("employee",name = "Joe", salary = 55000, union = T)   #创建一个employee类的对象

> joe    #S4类的print()方法打印对象
An object of class "employee"
Slot "name":
[1] "Joe"

Slot "salary":
[1] 55000

Slot "union":
[1] TRUE

> joe@name   #使用"@"符合提取成员变量
[1] "Joe"

> slot(joe,"salary") #也可以使用slot*()函数提取指定成员变量
[1] 55000

> joe@salary <- 60000   #可以直接给成员变量赋值

> joe
An object of class "employee"
Slot "name":
[1] "Joe"

Slot "salary":
[1] 60000

Slot "union":
[1] TRUE

> joe@salary_wrong <- 70000   #S4类的安全性体现
Error in (function (cl, name, valueClass)  : 
  ‘salary_wrong’不是在“employee”类别里的槽
>

setClass()函数用来定义类,其中第一个参数表示类名,后面的representation()内的表示成员变量,name = "character"中name表示成员变量的名字,"character"表示该成员变量的类型

new()语句表示创建一个指定类的实例对象,比如这里创建了一个"employee"类的实例对象为joe,后面几个参数是具体的成员变量初始值

S4类中,成员变量称为slot,通常可用@符号引用,也可以使用slot()函数来查询某个实例对象的某成员变量的值

最后一句是为一个不存在的成员变量赋值,按照S4类安全性的设计目的,这里会报错


在S4类上实现泛型函数

在S4类上定义泛型函数需要使用setMethod()函数,这里我们还是用"employee"类举例,实现show()函数,在S4类中的功能与S3类的泛型函数print()类似

> joe    #为定义泛型函数时打印的结果
An object of class "employee"
Slot "name":
[1] "Joe"

Slot "salary":
[1] 60000

Slot "union":
[1] TRUE

> show(joe)    #show()函数的功能与print()类似
An object of class "employee"
Slot "name":
[1] "Joe"

Slot "salary":
[1] 60000

Slot "union":
[1] TRUE

> setMethod("show", "employee",
+           function(object){
+             inorout <- ifelse(object@union,"is","is not")
+             cat(object@name,"has a salary of ", object@salary,"and",inorout,"in the union","\n")
+           }
+ )    #定义泛型函数show()
[1] "show"

> joe #打印函数已重定向
Joe has a salary of  60000 and is in the union 
>

这里为类"employee"定义了一个泛型函数"show",直接键入joe可以看出打印函数确实已重定向


S4类的继承

S4类的继承是在setClass()函数中设置contains属性,再以针对小时工的例子来说:

> setClass("hrlyemployee",
+          representation(
+            name = "character",
+            salary = "numeric",
+            union = "logical",
+            hrsthismonth = "numeric"
+          ),
+          contains = "employee"
+ )    #创建类"hrlyemployee"并继承类"employee"

> Tom <- new("hrlyemployee", name = "Tom",salary = 3000, union=TRUE,hrsthismonth = 4)    #创建个对象Tom

> Tom    #可以看到打印函数调用了父类的show()函数
Tom has a salary of  3000 and is in the union 
> 

目前R中使用到的大部分类都是S3类,关于S4类的其他方面的详细介绍,可以参考豆瓣上网友的总结https://www.douban.com/note/427299243/


对象的管理

将了面向对象,我们知道R中所有的实例都是对象,这样一个典型的R会话中,很可能产生大量的对象,那么久需要一些工具来管理这些对象

> ls()    #ls()函数可以查看当前会话中有哪些对象
[1] "joe" "Tom"

> ls(pattern = "jo")   #查看对象名字满足指定模式的对象
[1] "joe"

> rm(joe)    #删除对象

> ls()    #对象已删除
[1] "Tom"

> rm(list = ls())    #可删除所有对象

> ls()
character(0)
>

> x <- stats::runif(20)    #生成20个0~1的随机数构成向量对象的x
> y <- list(a = 1, b = TRUE, c = "oops")

> save(x, y, file = "xy.RData")  #保存对象x和y到xy.RData文件中

> ls()
[1] "x" "y"

> rm(x,y)    #删除两对象后再加载
> ls()
character(0)

> load("xy.RData")    #从文件中将保存的两对象加载进会话
> ls()
[1] "x" "y"

> exists("x")    #查看是否存在对象x
[1] TRUE
>  

上述演示了R会话中,对象的查看、删除,持久化保存与重新加载,以及查看回归中是否存在某对象

在前面几篇文章中已经介绍过如何查看对象的内部结构,可以使用class(),mode(),names(),attributes(),unclass(),str(),edit()等函数,这里就不再重复介绍,读者可以自己多试试这几个函数

实用的S3例子

上面我们创建的类都比较简单,下面我们创建一个使用的类,我们自己来实现矩阵的操作:

setClass("my_matrix",slots = list(vector = "vector",nrow = "numeric",ncol = "numeric"))    #定义了自己的my_matrix类

setGeneric("%mutmut%",function(obj1,obj2) standardGeneric("%mutmut%"))  #若要实现自定义的类函数,需要先定义泛型接口

setMethod("%mutmut%",signature(obj1 = "my_matrix",obj2 = "my_matrix"),function(obj1,obj2){
  m_obj1 <- matrix(obj1@vector,obj1@nrow,obj1@ncol)
  m_obj2 <- matrix(obj2@vector,obj2@nrow,obj2@ncol)
  m_obj1 %*% m_obj2
})   #具体实现泛型函数

"%mut%" <- function(obj1,obj2){
  m_obj1 <- matrix(obj1@vector,obj1@nrow,obj1@ncol)
  m_obj2 <- matrix(obj2@vector,obj2@nrow,obj2@ncol)
  m_obj1 %*% m_obj2
}    #普通函数的方式定义了自己的二元运算方法

n_obj1 <- new("my_matrix",vector = c(1,2,3,4),nrow = 2,ncol = 2)   #分别创建了类的两个实例对象

n_obj2 <- new("my_matrix",vector = c(5,6,7,8),nrow = 2,ncol = 2)

n_obj1 %mut% n_obj2    #使用自定义的二元运算

     [,1] [,2]
[1,]   23   31
[2,]   34   46

> n_obj1 %mutmut% n_obj2   #以成员函数的形式做运算
     [,1] [,2]
[1,]   23   31
[2,]   34   46
> 

这个例子是实现自己的矩阵类,并自定义矩阵乘法的二元运算符,这里笔者用了两种方式来实现这个自定义的二元运算符,第一种是封装在类中的创建泛型函数的方式,第二种是按照以前介绍的创建普通函数的二元运算符

一般S4类使用第一种方式创建自定义成员函数,S3类使用第二种方式

其中在第一种方式中,若要创建自己的函数,需要先使用setGeneric()函数生成该函数的泛型函数接口,之后才能使用setMethod()函数来具体实现该泛型函数接口,否则会报错;如果仅仅是要重写父类的函数,就不需要用setGeneric()重新定义泛型接口了,因为父类已经定义了

通过学习这个例子的编写,读者就能自己知道怎么来编写自己需要的类了,同时也知道怎么来实现满足自己业务需求的泛型函数了

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注