R语言面向对象编程

R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大。

R语言作为统计学一门语言,一直在小众领域闪耀着光芒。直到大数据的爆发,R语言变成了一门炙手可热的数据分析的利器。随着越来越多的工程背景的人的加入,R语言的社区在迅速扩大成长。现在已不仅仅是统计领域,教育,银行,电商,互联网….都在使用R语言。

要成为有理想的极客,我们不能停留在语法上,要掌握牢固的数学,概率,统计知识,同时还要有创新精神,把R语言发挥到各个领域。让我们一起动起来吧,开始R的极客理想。

关于作者:

  • 张丹(Conan), 程序员Java,R,PHP,Javascript
  • weibo:@Conan_Z
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/r-object-oriented-intro/

R-OO

前言

面向对象是一种对现实世界理解和抽象的方法,当代码复杂度增加难以维护的时候,面向对象就会显得非常重要。我经历过Java和Javascript两种语言从面向过程到面向对象思路的改造,并感觉这种变化也会出现在R语言中。在工业界的引导下,R将走向大规则的企业应用,因此面向对象的编程方式将成为R语言的一种非常重要的发展方向,动起来迎接R的进步。

目录

  1. 什么是面向对象?
  2. R为什么要进行面向对象编程?
  3. R的面向对象编程
  4. 与其他语言的对比

1 什么是面向对象?

面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。早期的计算机编程是基于面向过程的方法,例如实现算术运算2+3+4=9,通过设计一个算法就可以解决当时的问题。

随着计算机技术的不断提高,计算机被用于解决越来越复杂的问题。一切事物皆对象,通过面向对象的方式,将现实世界的事物抽象成对象,现实世界中的关系抽象成类、继承,帮助人们实现对现实世界的抽象与数字建模。通过面向对象的方法,更利于用人理解的方式对复杂系统进行分析、设计与编程。同时,面向对象能有效提高编程的效率,通过封装技术,消息机制可以像搭积木的一样快速开发出一个全新的系统。面向对象是指一种程序设计范型,同时也是一种程序开发的方法。对象指的是类的集合。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。

面向对象的3个特征:封装,继承,多态

封装:是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

oo1

我们通过面向对象的思想,定义老师和学生两个对象,并分别定义老师和学生的行为。

  • 老师的行为:讲课,布置作业,批作业
  • 学生的行为:听课,写作业,考试

通过封装就把两个客观事物进行了抽象,并设置了事情的行为。

继承:子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”;被继承的类称为“基类”、“父类”或“超类”。

oo2

通常每门课都会从学生中选出这门课的课代表,来帮助老师和其他同学的沟通。课代表会有一些比普通同学更多特权。通过继承关系,把普通同学和课代表区别为两个子类,课代表不仅有普通同学的行为,还有帮助老师批作业的行为。

多态: 指由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应。

oo3

临近期末考试时,总有考的好的同学和考的不好的同学。所以,对于优等生来说,他的考试结果是优;次等生,考试结果就不是太好。相同行为对于由继承而产生的相关的不同的对象,结果是不同的。

所以,通过面向对象的思想,我们可以把客观世界的事物都进行抽象。

is a 和 has a

在客观世界中有若干类,这些类之间有一定的结构关系。通常有两种主要的结构关系,is a,和 has a。

  • is a: 为继承关系,比如 菱形、圆形和方形都是一种形状
  • has a:为组合关系或聚合关系,比如 电脑是由显示器、CPU、硬盘等组成

2 R为什么要进行面向对象编程?

R主要面向统计计算,而且代码量一般不会很大,几十行,几百行,使用面向过程的编程方法就可以很好地完成编程的任务。

不过,虽然R语言的持续手热,伴随着越来越多的工程背景的人的加入,R语言开始向更多的领域发展。原来的少量的代码的面向过程的编码方式,会越来越难以维护海量代码的项目,所以必须有一种新的编程方式来代码原来的面向过程的编码思路,这种新的编程方式就是面向对象编程(Object Oriented Programming, OOP)。

面向对象编程,早在C++/Java时代就被广泛使用了,几乎90%以上的Java框架都是按面向对象的方法设计的;8年前Javascript各种面向过程编码让前端开发困难重重,直到Google的Gmail的Web端出现,才让大家认识到原来Javascript也可以面向对象编程,随后的jQuery, ExtJS等类库的完全面向对象的实现,终于让Javascript承得起前端的天空,后来的Node的诞生更是让Javascript拓宽了应用领域。

当R语言被大家所看好的同时,我们也要开始思考,如何才能让R成为工业界的开发语言?应用如何构建非统计计算的项目?如何用R有效的编写10万行以上的代码?

我想这个答案就是以面向对象进行编程,现在的R就像8年前的Javascript,需要大公司和牛人来推动。从我的观察来看,以Hadley Wickham为代表的R语言领军人物,已经开始在R包中全面引入面向对象思路进行R包的开发了。以面向对象思想开发的R包memoise,请参考文章:R语言本地缓存memoise

3 R的面向对象编程

R的面向对象编程是基于泛型函数(generic function)的,而不是基于类层次结构。接下来,我从面向对象的3个特征入手,分别用R语言进行实现,使用的案例为上文中,老师和学生的3幅图。

3.1 R语言实现封装


# 定义老师对象和行为
> 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] "布置作业"

> attr(b,'class') <- 'homework'
> student(b)
[1] "写作业"
 
> attr(a,'class') <- 'correcting'
> teacher(a)
[1] "批改作业"
 
> attr(b,'class') <- 'exam'
> student(b)
[1] "考试"

# 定义一个变量,既是老师又是同学 
> ab<-'student_teacher'
# 分别设置不同对象的行为
> attr(ab,'class') <- c('lecture','homework')
# 执行老师的行为
> teacher(ab)
[1] "讲课"
# 执行同学的行为
> student(ab)
[1] "写作业"

3.2 R语言实现继承


# 给同学对象增加新的行为
> 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
[1] "普通同学"
attr(,"type")
[1] "normal"     "StudentObj"
attr(,"class")
[1] "homework"   "attend" "exam"      

# 查看课代表的对象实例
> s2
[1] "课代表"
attr(,"type")
[1] "leader"     "StudentObj"
attr(,"class")
[1] "homework"   "attend" "exam"       "correcting"

3.3 R语言实现多态


# 创建优等生和次等生,两个实例
> e1 <- NewInstance('优等生',classes='excellent', parents='StudentObj')
> e2 <- NewInstance('次等生',classes='poor', parents='StudentObj')

# 修改同学考试的行为,大于85分结果为优秀,小于70分结果为及格
> student.exam <- function(x,score) {
+     p<-"考试"
+     if(score>85) print(paste(p,"优秀",sep=""))
+     if(score<70) print(paste(p,"及格",sep=""))
+ }

# 执行优等生的考试行为,并输入分数为90
> attr(e1,'class') <- 'exam'
> student(e1,90)
[1] "考试优秀"

# 执行次等生的考试行为,并输入分数为66
> attr(e2,'class') <- 'exam'
> student(e2,66)
[1] "考试及格"

这样通过R语言的泛型函数,我们就实现了面向对象的编程。

4 R的面向过程编程

接下来,我们再次对比用R语言用面向过程实现上面的逻辑。

4.1 定义老师和同学两个对象和行为


# 辅助变量用于设置初始值
> char0 = character(1)

# 定义老师对象和行为
> teacher_fun<-function(x=char0){
+     if(x=='lecture'){
+         print("讲课")
+     }else if(x=='assignment'){
+         print("布置作业")
+     }else if(x=='correcting'){
+         print("批改作业")
+     }else{
+         print("你不是teacher")
+     }
+ }

# 定义同学对象和行为 
> student_fun<-function(x=char0){
+     if(x=='attend'){
+         print("听课")
+     }else if(x=='homework'){
+         print("写作业")
+     }else if(x=='exam'){
+         print("考试")
+     }else{
+         print("你不是student")
+     }
+ }

# 执行老师的一个行为
> teacher_fun('lecture')
[1] "讲课"

# 执行同学的一个行为
> student_fun('attend')
[1] "听课"

4.2 区别普通同学和课代表的行为


# 重定义同学的函数,增加角色判断
> student_fun<-function(x=char0,role=0){
+     if(x=='attend'){
+         print("听课")
+     }else if(x=='homework'){
+         print("写作业")
+     }else if(x=='exam'){
+         print("考试")
+     }else if(x=='correcting'){
+         if(role==1){#课代表
+             print("帮助老师批改作业")  
+         }else{
+             print("你不是课代表")  
+         }
+     }else{
+         print("你不是student")
+     }
+ }

# 以普通同学的角色,执行课代表的行为
> student_fun('correcting')
[1] "你不是课代表"

# 以课代表的角色,执行课代表的行为
> student_fun('correcting',1)
[1] "帮助老师批改作业"

我在修改student_fun()函数的同时,已经增加了原函数的复杂度。

4.3 参加考试,以成绩区别出优等生和次等生


# 修改同学的函数定义,增加考试成绩参数
> student_fun<-function(x=char0,role=0,score){
+     if(x=='attend'){
+         print("听课")
+     }else if(x=='homework'){
+         print("写作业")
+     }else if(x=='exam'){
+         p<-"考试"
+         if(score>85) print(paste(p,"优秀",sep=""))
+         if(score<70) print(paste(p,"及格",sep=""))
+     }else if(x=='correcting'){
+         if(role==1){#课代表
+             print("帮助老师批改作业")  
+         }else{
+             print("你不是课代表")  
+         }
+     }else{
+         print("你不是student")
+     }
+ }

# 执行考试函数,考试成绩为大于85分,为优等生
> student_fun('exam',score=90)
[1] "考试优秀"

# 执行考试函数,考试成绩为小于70分,为次等生
> student_fun('exam',score=66)
[1] "考试及格"

我再一次用面向过程的代码,实现了整个的编辑逻辑。再用到面向过程来写程序的时候,每一次的需求变化,都需要对原始代码进行修改,从而不仅增加了复杂度,而且不利于长久的维护。更多思考留给了大家!

本文抛砖引玉地讲了R语言的面向对象的编程,其中部分代码有些不够严谨,本文只希望给大家思路上的认识,更具体的面向对象编程实例,会在以后的文章中进行讨论。

转载请注明出处:
http://blog.fens.me/r-object-oriented-intro/

打赏作者

This entry was posted in R语言实践

0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

18 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
beaderchen

博主这里的student和teacher,概念上是class,但实质上又是generic function,这样做理解起来好像有点点困难,这么设计是有什么目的么?
(另外,如果能把代码块中每行前面的 > 和 + 去掉,就太好了。)

下面这种方法可行吗?
lecture <- function(x, …) UseMethod("lecture")

assignment <- function(x, …) UseMethod("assignment")

exam <- function(x, …) UseMethod("exam")

lecture.teacher <- function(x) print("讲课")

assignment.teacher <- function(x) print("布置作业")

exam.teacher <- function(x) print("出考卷")

lecture.student <- function(x) print("听课")

assignment.student <- function(x) print("写作业")

exam.student <- function(x) print("考试")

a<-'teacher'

b<-'student'

class(a) <- 'teacher'

class(b) <- 'student'

lecture(a)

lecture(b)

assignment(a)

assignment(b)

exam(a)

exam(b)

Conan Zhang

1. generic function是S3对象的实现,还有S4和RC另外两种对象实现,后面会写。
2. 面向对象设计,主要是为了解决代码结构设计问题,文章中已经写了。

3. <和+,你可复制出来,自己替换一下就行了。

Conan Zhang

你的这种设计,以lecture ,assignment ,exam 对象也可以,只是与大家的习惯很不相同。一般对象的实体是名称,动词做这个对象的行为。你这样定义,当代码量大一点出了你自己,没人能看懂。

beaderchen

谢谢博主的回答。
当然我这里lecture ,assignment ,exam名字应该使用动词。只是我这里想把他们当成generic function,而不是class。

譬如我们为某个student设计print函数,可以写成
print.student <- function(x){}
然后这样调用
a <- structure("a", class = "student")
print(a)

按照您的写法,每次调用前得在assign一个class上去,一个对象的class是漂泊不定的。

attr(a,'class') <- 'print'
student(a)

会不会比较不方便?

Conan Zhang

就像你发现的,每次调用要assgin一个class,这就是我在文章中最后说明的 “其中部分代码有些不够严谨”。

S3只是一种通过generic function的面向对象的模拟实现,从根本上并不适合于完全面向对象的设计。所以,才会出现你的这样的设计;同时有一些R的内置S3对象,也是你这样的设计。回到R语言本身,R的自由性,就是这种不够标准化的设计。

你用lecture ,assignment ,exam作为对象的问题在于,不能很好的表达,封装,继承,多态。

S4和RC,会比S3有更严格的结构性要求,会避免上面的问题。

beaderchen

我好像有点明白了。这是否就是程序员觉得R语言奇怪的地方之一?

Conan Zhang

有可能吧,呵呵

[…] 关于面向对象的介绍,请参考文章:R语言面向对象编程 […]

dj123jary

继承这一步的疑惑
> student(s1)
[1] “写作业”
> student(s2)
[1] “写作业”
貌似其余的动作都不生效,所谓S3应该是个假的封装和多态吧,后面为啥要修改student.exam,而不是重新定义s1.exam,这样才能体现多态性吧

Conan Zhang

继续读S3,S4,RC的文章,回头再想这个问题。
http://blog.fens.me/r-class-s3/

524052035

又看到你了,这个世界真小

Conan Zhang

🙂

wddoer

很棒~,谢谢博主

Conan Zhang

🙂

ArchosaurRise

在R语言实现继承中,

mro <- c(mro, ancestors[ancestors != name])
这句话我看不懂,尤其是这句话里涉及ancestor的部分。
能解释一下吗?谢谢博主。

Conan Zhang

把祖先的所有class属性的值,复制给子类。

Mingyu Zhang (Brian)

想问一下for语句中:
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)
}
请问“name in parents”指的是什么name呢?不是很理解这句话。parents本来就是character而不是list或者dataframe,为何会用到for语句呢?

谢谢!

Conan Zhang

s3类似的class属性,是以字符串定义的。就是把所的父类继承给子类

18
0
Would love your thoughts, please comment.x
()
x