第 2 章 R语言面向对象编程指南
面向对象是一种对世界理解和抽象的方法,当代码复杂度增加难以维护的时候,面向对象就会显得很重要,我经历过Java和Python两种语言从面向过程到面向对象的改造,对R的面向对象的编程也早有些研究但开始的时候并不是那么的透彻。随着大数据时代AI时代的来临,R将走向大规模的企业级应用,因此面向对象的编程方式也会将成为R语言的一种非常重要的趋势,并且多位R语言大神,像Hadley Wickham等在R包开发中早就引入了面向对象的编程方式,R语言的发展也势必会面向对象。
2.1 什么是面向对象
学过计算机编程基础的人都知道,计算机是通过接受一些逻辑指令,然后翻译成机器码,进而控制CPU的电路,从而实现我们能看到的所有操作。无论是何种计算机编程语言,都可以认为是计算机和人类沟通的翻译,将一般人能懂的计算机语言翻译成计算机能懂的机器语言。
简单的理解,有的语言比较接近机器的习惯,机器执行起来会更有效率;有的语言比较接近人的习惯,人类设计起来会更容易。面向对象的思想就属于第二种情况。
人的思维方式和计算机是不同的,计算机习惯按照顺序执行不同的指令,依据严格的逻辑进行不同的行为,而人类处理问题的方式通常是先对问题进行分析,然后调动不同的资源做不同的事情。
喜欢历史故事的朋友都知道诸葛亮打仗和刘邦打仗的区别。诸葛亮会命令某人埋伏,某人放火,某人举旗,某人不战而退,某人斜刺里杀出,每个人听到命令都不知道最后会发生什么。直到最后敌军被一条龙的歼灭后,得胜归来的将军们对诸葛亮佩服的五体投地。而刘邦打仗的故事远没有那么精彩,事情来了该让韩信做的交给韩信,该让萧何做的交给萧何。
如何像刘邦一样的写程序,这就是面向对象的程序设计。韩信,萧何这些人都可以认为是对象,我们只需要知道他们有什么特点,根据问题的不同派不同的人去做就可以了,而诸葛亮关注的是具体流程,至于是关羽去放火还是张飞去放火反而不重要了,这就是过程式的编程思想。
面向对象的程序设计在运行效率上可能没有优势,但是节约了开发者和设计者的时间,在R语言和S语言上这一点完全一致,S语言很重要的设计理念是“人的时间远比机器的时间宝贵”。对各种模型和算法的封装及重用与面向对象的编程目的是相同的。
在面向对象的程序设计中,对象(object)是最基本的元素,不过对象指的是具体的实例,在对象之上还有一个类(class)的概念。这里的类和R中的类的概念没有任何不同,都是指某一种抽象对象的类型(和R中的type不同,type指的是在内存存储方面的类型)。
比如说“马”就是一个类,随便牵来一匹白马或红马都属于马这个范畴,但都和马这个东西不一样,如果牵来的白马是刘备的的卢马,红马是关羽的赤兔马,那么这两匹马就是对象。所以类是抽象的概念,对象是类的具体实例。我们直接操作的是对象,但是需要定义的是类。
用面向对象的专业术语来说,马就是一个“类”,白马和红马是马的“子类”,的卢马是白马实例化的对象,也是马实例化的对象。
一般来说类包含属性和方法,属性指的是类具有的某些信息,在计算机程序中通常是变量,方法指的是类进行的操作,在计算机程序中相当于函数。
并不是具有了类和对象的概念后就成了面向对象的程序设计,一般来说还得具备三个特性:封装、继承和多态。
封装指的是隐藏对象的实现细节,仅对外公开接口,每个对象都可以独立的完成一定的功能,不需要和其他对象有过多的交互,所有的数据交换都通过接口来处理,专业术语是降低系统的“耦合度”。
比如说马这种交通工具就是一个封装的很好的类,具有颜色、体重等属性,具有载人、奔跑等方法。用的时候把马牵出来,通过缰绳和马鞭这几个接口来控制动作。
继承是一个类可以继承另一个类的各种属性及方法,重写或增加某些属性和方法,被继承的类称为“父类”,继承了父类的类的称为“子类”。通过继承可以重写父类方法或增加额外功能,注意R语言像Python一样支持多重继承,Java不支持多继承,但是有其他办法实现。当然除了继承也可以组合类。
多态可以说是面向对象的程序设计中最关键的特性,如果某种语言支持以上所有特性但是不支持多态,我们称其为“基于对象”而不是面向对象,所谓多态简单来说就是希望能用相同的命令作用于不同的类,根据类的不同产生不同的结果。
#作用在数值数据
summary(rnorm(10))
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -2.69364 -0.65721 -0.02727 -0.13618 0.58467 1.68234
#作用在Model上
summary(lm(rnorm(10)~rnorm(10)))
##
## Call:
## lm(formula = rnorm(10) ~ rnorm(10))
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.6121 -0.8625 0.1777 0.8150 1.7043
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -0.08898 0.33865 -0.263 0.799
##
## Residual standard error: 1.071 on 9 degrees of freedom
2.2 R为什么要进行面向对象的编程
R主要面向统计计算,而且代码量一般不会很大,几十行,几百行,使用面向过程的编程方式就可以很好的完成编程任务。在R中流传着一个深入人心的说法“万物皆对象”,另一方面,R是一种函数式的语言,与面向对象的程序设计存在着天然的差异,实际上这两个对象的描述的含义是不同的。“万物皆对象”是指R的基本数据结构的地位都是相同的,任何东西包括函数都可以认为是对象,都能作为参数传入到函数中。而“面向对象”指的是一种编程泛型,已经成为一个专有的名词。
但是随着R语言在工业界的火热,伴随着越来越多的工程背景的人的加入,R语言开始向更多领域发展,会有越来越难以维护的海量代码项目,所以必须使用面向对象的编程思想,此外如果你开发一些R包需要对特殊的对象重定义S3(下一章会讲)的方法,需要对大量程序代码最大化的重用和封装(S4(后面会讲到)),那么这时候你同样需要用到R语言的面向对象的编程。
一句话,随着R语言的发展,R面向对象编程一定是一个大的趋势。
2.3 R的面向对象编程
R的面向对象是基于泛型函数(generic function)的,而不是基于类层次结构,接下来我们从面向对象的3个特征入手,分别用R语言进行实现。
- 封装
# 定义teacher对象和行为
teacher <- function(x,...) UseMethod("teacher")
teacher.lecture <- function(x,...) print("上课")
teacher.assignment <- function(x,...) print("布置作业")
teacher.correcting <- function(x,...) print("批改作业")
teacher.default <- function(x,...) print("你不是teacher")
# 定义同学对象和行为
student <- function(x,...) UseMethod("student")
student.attend <- function(x,...) print("听课")
student.homework <- function(x,...) print("写作业")
student.exam <- function(x,...) print("考试")
student.default <- function(x,...) print("你不是student")
#定义两个变量,a老师和b同学
a <- 'teacher'
b <- 'student'
# 给老师变量设置行为
attr(a,"class") <- 'lecture'
# 执行老师的行为
teacher(a)
## [1] "上课"
attr(b,'class') <- 'attend'
student(b)
## [1] "听课"
attr(a,'class') <- 'assignment'
teacher(a)
## [1] "布置作业"
- 继承
# 给同学对象增加新的行为
student.correcting <- function(x) print("帮助老师批改作业")
# 辅助变量用于设置初始值
char0 = character(0)
#实现继承关系
create <- function(classes=char0,parents=char0){
mro <- c(classes)
for(name in parents){
mro <- c(mro,name)
ancestors <- attr(get(name),'type')
mro <- c(mro,ancestors(ancestors != name))
}
return(mro)
}
# 定义构造函数,创建对象
NewInstance <- function(value=0,classes=char0,parents=char0) {
obj <- value
attr(obj,'type') <- create(classes,parents)
attr(obj,"class") <- c('homework','correcting','exam')
return(obj)
}
# 创建对象实例
StudentObj <- NewInstance()
# 创建子对象实例
s1 <- NewInstance("普通同学",classes = 'normal',parents = "StudentObj")
s2 <- NewInstance('课代表',classes='leader',parents='StudentObj')
# 给课代表,增加批改作业行为
attr(s2,'class') <- c(attr(s2,'class'),'correcting')
s1
s2
- 多态
# 创建优等生和次等生,两个实例
e1 <- NewInstance("优等生",classes='excellent',parents='StudentObj')
e2 <- NewInstance("次等生",classes='poor',parents='StudentObj')
student.exam <- function(x,score){
p <- '考试'
if(score>85) print(paste(p,"优秀"))
if(score<70) print(paste(p,"及格"))
}
# 执行优等生的考试行为,并输入分数为90
attr(e1,'class') <- 'exam'
stuent(e1,90)
[1] “考试优秀"
attr(e2,'class') <- 'exam'
student(e2,66)
[1] ”考试及格"
通过R语言的面向对象的反省函数,我们就可以实现面向对象的编程。