@evilking
2018-03-03T16:10:56.000000Z
字数 6573
阅读 1255
R基础篇
R语言同样是一门面向对象的语言,你接触到的R中的所有东西都是对象
我们知道面向对象的特性就是封装、多态和继承,本篇我们会介绍R中的S3类和S4类的创建和使用
封装,即把独立但相关的数据项目打包为一个类的实例;封装可帮助你跟踪相关的变量,提高清晰度
多态,这意味着相同的函数使用不同类的对象时可以调用不同的操作。例如使用print()调用特定类的对象会调用适合此类的打印功能。多态可以促进代码可重用性
继承,即允许把一个给定的类的性质自动赋予为其下属的更多特殊化的类
一个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()函数,例如
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类的出现
这里总结一下R中S3类与S4类的区别:
操作 | S3类 | S4类 |
---|---|---|
定义类 | 在构造函数的代码中隐式定义 | setClass() |
创建对象 | 创建列表,设置类属性 | new() |
引用成员变量 | $ | @ |
实现泛型函数f() | 定义f.classname() | setMethod() |
声明泛型函数 | UseMethod() | setGeneric() |
可以调用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类上定义泛型函数需要使用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类的继承是在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()等函数,这里就不再重复介绍,读者可以自己多试试这几个函数
上面我们创建的类都比较简单,下面我们创建一个使用的类,我们自己来实现矩阵的操作:
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()重新定义泛型接口了,因为父类已经定义了
通过学习这个例子的编写,读者就能自己知道怎么来编写自己需要的类了,同时也知道怎么来实现满足自己业务需求的泛型函数了