参考这篇文章:
R语言面向对象编程 (dataxujing.github.io)
S3对象的介绍 #
在R语言中,基于S3对象的面向对象编程,是一种基于泛型函数的实现方式。泛型函数是一种特殊的函数,根据传入对象的类型决定调用那个具体的方法。基于S3对象实现面向对象编程,不同其他语言的面型对象编程,是一种动态函数调用的模拟实现。S3对象被广泛应用于R的早期的开发包中。
R的S3系统有三个组成部分:属性(attribute)(尤其是class属性)、泛型函数(genericfunction)和方法(method)
创建S3对象 #
注意:本文会用到pryr,为了方便我们检查对象的类型,引入pryr包作为辅助工具。
library(pryr)
#通过变量创建S3对象
x <- 1
attr(x,'class') <- 'foo'
x
## [1] 1
## attr(,"class")
## [1] "foo"
## [1] "foo"
## [1] "foo"
#用pryr包的otype函数,检查x的类型
otype(x)
## [1] "S3"
通过structure()函数创建S3对象
y <- structure(2,class="foo")
y
## [1] 2
## attr(,"class")
## [1] "foo"
## [1] "foo"
## [1] "foo"
## [1] "S3"
创建一个多类型的S3对象,S3独享没有明确结构关系,一个S3对象可以有多个类型,S3对象的class属性可以是一个向量,包括多种类型
x <- 1
attr(x,"class") <- c("foo","bar") # 给了x对象两个class属性
class(x)
## [1] "foo" "bar"
## [1] "S3"
如果分配至少一个class属性,就是S3对象,如果没有class属性,就是base对象。
泛型函数和方法调用 #
对于S3对象的使用,通常用UseMethod()函数来定义一个泛型函数的名称,通过传入参数的class属性,来确定方法调用。
R的S3系统有三个组成部分:属性(attribute)(尤其是class属性)、泛型函数(genericfunction)和方法(method)
由泛型函数、方法和基于类的分派方式所构成的系统就是R的S3系统。之所以叫作S3是由于它起源于S语言的第三个版本,语言S-PLUS是R语言的前身。许多常见的R函数都是S3泛型函数,它们可以支持多种不同的类方法函数。比如说,summary和head就会调用UseMethod函数以识别对象的类属性。
每一个S3方法的名称都包含两个部分。前一部分指明该方法对应的函数,后一部分则指明类属性(attitubute)。这两个部分的名称用英文句点.分隔。
可以用attributes函数查看一个对象的属性。
定义一个teacher的泛型函数
用UseMethod()定义teacher泛型函数
用teacher.xxx的语法格式定义teacher对象的行为(类属性)
其中teacher.default是默认行为
# 用UseMethod()定义teacher泛型函数
teacher <- function(x,...) UseMethod("teacher") # 这时teacher也变成了一个对象
# 用pryr包中ftype()函数,检查teacher类型
ftype(teacher)
[1] "s3" "generic"
# 定义teacher内部函数, 都是teacher的属性
teacher.lecture <- function(x,...) print("讲课")
teacher.assignment <- function(x,...) print("布置作业")
teacher.correcting <- function(x,...) print("批改作业")
teacher.default <- function(x,...) print("你不是teacher")
teacher.character <- function(x, ...) print("请先定义")
警告:因为点号.是S3实现泛型函数的方法,为了防止歧义,应避免在普通变量名中使用.号。普通变量命名推荐使用 para_name, paraName,类名使用 ParaName。
方法调用通过传入参数的class属性,来确定不同方法调用
定义一个变量a,并设置a的class属性为lecture
把变量a传入到teacher泛型函数中
函数teacher.lecture()函数的行为被调用
a <- "teacher" # 这时a是个字符串
teacher(a)
# 给老师变量设置行为
attr(a,"class") <- 'lecture' # 给a一个类属性 lecture
# 执行老师的行为
teacher(a)
[1] “讲课”
attributes(a)
# $class
# [1] "lecture"
当然我们可以直接调用teacher中定义的行为,如果这样做就失去了面向对象封装的意义
teacher.lecture()
[1] "讲课"
teacher.lecture(a)
[1] "讲课"
teacher()
[1] "你不是teacher"
另外一个例子
## 第一步:定义一个泛型函数,这个函数的函数体只有一个固定的语句
doit = function(...) UseMethod("doit")
## 第二步:设置针对特定CLASS的动作函数
doit.character = function(...) {
cat("With STRING class attribute\n")
}
doit.integer = function(...) {
cat("With INTEGER class attribute\n")
}
doit.God = function(...) {
cat("With God class attribute\n")
}
### 第三步:设置一个默认的动作函数
doit.default = function(...) {
cat("UNKNOWN class attribute\n")
}
##上面三个步骤就完成了doit泛型函数的设置。看看效果吧:
a = "ABCDE"
doit(a) # 这里a本来是string
## With STRING class attribute
attr(a, "class") = "integer"
class(a) #[1] "integer"
doit(a)
## With INTEGER class attribute
attr(a, "class") = "God"
doit(a)
## With God class attribute
a = as.factor(a)
doit(a)
## UNKNOWN class attribute
这里面还有一个问题:针对特定类型的动作函数并没有用于识别数据类型的代码,函数调用的形式都是一样的,doit函数怎么知道该执行那个操作?关键就在于UseMethod这个函数。这个函数只能在函数体内使用,它可以有两个参数:
UseMethod(generic, object)
generic 是泛型函数的名称(字符串), object 是用于确定动作函数的对象,如果缺省将使用泛型函数的第一个参数,UseMethod取其CLASS属性。如果要用其他参数进行类型判断,只需修改泛型函数。下面修改后的泛型函数使用第二个参数进行动作函数选择:
doit("abc", 1:10)
## With STRING class attribute
doit(1:10, "abc")
## With INTEGER class attribute
doit = function(...) {
xx = list(...)
UseMethod("doit", xx[[2]])
}
# 注意下面结果与修改泛型函数前的差别
doit(1:10, "abc")
## With STRING class attribute
查看S3对象的函数 #
当我们使用S3队形进行面向对象封装后,可以使用methods()函数来查看S3对象中的定义的内部行为函数。
# 查看teacher对象
> teacher
function(x,...) Usemethod("teacher")
# 查看teacher对象的内部函数
> methods(teacher)
[1] teacher.assignment teacher.correcting teacher.default teacher.lecture
#通过methods()的generic.function参数,来匹配泛型函数名字
> methods(generic.function = predict)
[1] predict.ar* ......
通过methods()的class参数,来匹配类的名字
> methods(class=lm)
[1]add1.lm* ......
用getAnywhere()函数,查看所有函数
#查看teacher.lecture函数
>getAnywhere(teacher.lecture)
# A single object matching ‘teacher.lecture’ was found
# It was found in the following places
# .GlobalEnv
# registered S3 method for teacher
# with value
#
# function(x,...) print("讲课")
使用getS3method()函数,也同样可以查看不可见的函数
S3对象的继承关系 #
S3独享有一种非常简单的继承方式,用NextMethod()函数来实现。
定义一个node泛型函数
node <- function(x) UseMethod("node",x)
node.default <- function(x) "Default node"
father函数
node.father <- function(x) c("father")
son函数,通过NextMethod()函数只想father函数
node.son <- function(x) c('son',NextMethod())
#定义n1
n1 <- structure(1,class=c("father"))
# 在node函数中传入n1,执行node.father()函数
node(n1)
[1] "father"
# 定义n2,设置class属性为两个
<- structure(1,class=c("son","father"))
# 在node函数中传入n2,执行node.son()函数和node.father()函数
node(n2)
# [1] "son" "father"
通过对node()函数传入n2的参数,node.son()先被执行,然后通过NextMethod()函数继续执行了node.father()函数。这样其实就模拟了,子函数调用父函数的过程,实现了面向对象编程中的继承。
另外一个更详细的例子
doit = function(...) UseMethod("doit")
doit.character = function(...) {
cat("With STRING class attribute\n")
NextMethod()
}
doit.integer = function(...) {
cat("With INTEGER class attribute\n")
NextMethod()
}
doit.God = function(...) {
cat("With God class attribute\n")
NextMethod()
}
doit.default = function(...) {
cat("UNKNOWN class attribute\n")
}
## 多CLASS属性对象
x = "abc"
class(x) = c("UNKNOWN", "integer", "character", "God")
doit(x)
# With INTEGER class attribute
# With STRING class attribute
# With God class attribute
# UNKNOWN class attribute
# 或者同样的
x = "abcd"
attr(x, "class") <- c("UNKNOWN", "integer", "character", "God")
doit(x)
# With INTEGER class attribute
# With STRING class attribute
# With God class attribute
# UNKNOWN class attribute
- 循环最外层从CLASS属性向量第一个“已知”类属性开始,依次嵌套
- default方法在循环最内层,而且不管有几个“未知”类属性,它只执行一次
- 循环层次和“未知”类属性的位置无关
S3对象的缺点 #
从上面S3对象的介绍上来看,S3对象并不是完全的面向对象实现,而是一种通过泛型函数模拟的面向对象的实现。
S3用起来简单,但在实际的面向对象编程的过程中,当对象关系有一定的复杂度,S3对象所表达的意义就变得不太清楚
S3封装的内部函数,可以绕过泛型函数的检查,以直接被调用
S3参数的class属性,可以被任意设置,没有预处理的检查
S3参数,只能通过调用class属性进行函数调用,其他属性则不会被class()函数执行
S3参数的class属性有多个值时,调用时会被按照程序赋值顺序来调用第一个合法的函数
所以,S3只是R语言面向对象的一种简单的实现。
与python区别 #
Python是面向对象的语言,类的定义很简介,一个类的属性和方法都是在一个代码块中,很容易让人理解。但是R不一样了,R的类属性和方法是分离的,需要泛型函数将他们连接起来。Python是实实在在对对象进行编程,不存在方法派送的问题,而R中的S3似乎更像是面向函数的编程。
一个python中的类的例子
>>> class Student(object):
... def __init__(self,name,age):
... self.name = name
... self.age = age
...
>>>
>>> bart = Student('zth',20)
>>>
>>> bart.name
'zth'
>>> bart.age
20
实用的例子 #
j <- list(name = "Joe", salary=5000, union=T)
class(j) <- "employee" ## 创建了类,employee
print.employee <- function(wrkr){
cat("Name:", wrkr$name, "\n")
cat("Salary: ", wrkr$salary, "\n")
cat("Union member: ", wrkr$union, "\n")
}
print(j)
# Name: Joe
# Salary: 5000
# Union member: TRUE
如果使用python来写
class employee(object):
def __init__(self,name,salary):
self.name = name
self.salary = salary
j = employee("Joe", 5000)
或者这样写
class Employee:
'所有员工的基类'
empCount = 0
def __init__(self, name, salary):
self.name = name
self.salary = salary
Employee.empCount += 1
def displayCount(self):
print "Total Employee %d" % Employee.empCount
def displayEmployee(self):
print "Name : ", self.name, ", Salary: ", self.salary
另一个例子
# 创建类
## 使用函数创建了一个类,一句话创建类
my_person <- function(ln, a, ht){
structure(list(lastName = ln, age = a, height = ht), class = "my_person")
}
## 创建第一个泛型函数
display <- function(obj) UseMethod("display")
# 创建方法
display.my_person <- function(obj){
cat("Last name : ", obj$lastName, "\n")
cat("Age : ", obj$age, "\n")
cat("Height : ", obj$height, "\n")
}
## 创建第二个泛型函数,如果泛型函数的方法又多个参数,务必加上...
nYear <- function(obj,...) UseMethod("nYear")
# 创建方法
nYear.my_person <- function(obj,n){
obj$age <- obj$age + n
cat("Age after",n,"years is:", obj$age, "\n")
}
# 由类创建对象
Tom <- my_person("Godden", 19, 176)
# 第一个泛型函数派送
display(Tom)
## Last name : Godden
## Age : 19
## Height : 176
# 第二个泛型函数派送
nYear(Tom,7)
## Age after 7 years is: 26
最后一次修改于 2022-03-29