R语言面向对象指南

来源:互联网 发布:mac版cad2014破解文件 编辑:程序博客网 时间:2024/04/28 23:17

原文链接:OO field guide 。


面向对象指南:

这一章主要介绍怎样识别和使用 R 语言的面向对象系统(以下简称 OO)。R 语言主要有三种 OO 系统(加上基本类型)。本指南的目的不是让你精通 R 语言的 OO,而是让你熟悉各种系统,并且能够准确地区分和使用它们。
OO 最核心的就是类和方法的思想,类在定义对象的行为时主要是通过对象的属性以及它和其它类之间的关系。根据类的输入不同,类对方法、函数的选择也会不同。类的建造是有层次结构的:如果一个方法在子类中不存在,则使用父类中的方法;如果存在则继承父类中方法。

三种 OO 系统在定义类和方法的时候有以下不同:

  • S3 实现的是泛型函数式 OO ,这与大部分的编程语言不同,像 Java、C++ 和 C# 它们实现的是消息传递式的 OO 。如果是消息传递,消息(方法)是传给一个对象,再由对象去决定调用哪个方法的。通常调用方法的形式是“对象名.方法名”,例如:canvas.drawRect(“blue”) 。而 S3 不同,S3 调用哪个方法是由泛型函数决定的,例如:drawRect(canvas, “blue”)。S3 是一种非正式的 OO 模式,它甚至都没有正式定义类这个概念。
  • S4 与 S3 很相似,但是比 S3 正规。S4 与 S3 的不同主要有两点:S4 对类有更加正式的定义(描述了每个类的表现形式和继承情况,并且对泛型和方法的定义添加了特殊的辅助函数);S4 支持多调度(这意味着泛型函数在调用方法的时候可以选择多个参数)。
  • Reference classes (引用类),简称 RC ,和 S3、S4有很大区别。RC 实现的是消息传递式 OO ,所以方法是属于类的,而不是函数。对象和方法之间用”$”隔开,所以调用方法的形式如:canvas$drawRect(“blue”) 。RC 对象也总是可变的,它用的不是 R 平常的 copy-on-modify 语义,而是做了部分修改。从而可以解决 S3、S4 难以解决的问题。

还有另外一种系统,虽然不是完全的面向对象,但还是有必要提一下:

  • base types(基本类型),主要使用C语言代码来操作。它之所以重要是因为它能为其它 OO 系统提供构建块。

以下内容从基本类型开始,逐个介绍每种 OO 系统。你将学习到怎样识别一个对象是属于哪种 OO 系统、方法的调用和使用,以及在该 OO 系统下如何创建新的对象、类、泛型和方法。本章节的结尾也有讲述哪种情况应该使用哪种系统。

前提:

你首先需要安装 pryr 包来获取某些函数:install.packages(“pryr”) 。

问题:

你是否已经了解本文要讲述的内容?如果你能准确地回答出以下问题,则可以跳过本章节了。答案请见本文末尾的问题答案 。

  1. 你怎样区分一个对象属于哪种 OO 系统?
  2. 如何确定基本类型(如整型或者列表)的对象?
  3. 什么是类的函数?
  4. S3 和 S4 之间的主要差异是什么? S4 和 RC 之间最主要的差异又是什么?

文章梗概:

  • 基本类型
  • S3
  • S4
  • RC
  • 模式的选择


基本类型:

基本上每个 R 对象都类似于描述内存存储的 C 语言结构体,这个结构体包含了对象的所有内容(包括内存管理需要的信息,还有对象的基本类型)。基本类型并不是真的对象系统,因为只有 R 语言的核心团队才能创建新的类型。但结果新的基本类型竟然也很少见地被添加了:最近是在2011年,添加了两个你从来没在 R 里面见过的奇异类型(NEWSXP 和 FREESXP),它们能够有效地诊断出内存上的问题。在此之前,2005年为 S4 对象添加了一个特殊的基本类型(S4SXP)。

Data structures 章节讲述了大部分普通的基本类型(原子向量和列表),但基本类型还包括 functions、environments,以及其它更加奇异的对象,如 names、calls、promises,之后你将会在本书中学到。你可以使用 typeof() 来了解对象的基本类型。但基本类型的名字在 R 中并不总是有效的,并且类型和 “is” 函数可能会使用不同的名字:

# The type of a function is "closure"f <- function() {}typeof(f)#> [1] "closure"is.function(f)#> [1] TRUE# The type of a primitive function is "builtin"typeof(sum)#> [1] "builtin"is.primitive(sum)#> [1] TRUE

你可能听过 mode() 和 storage.mode(),我建议不要使用这两个函数,因为它们只是 typeof() 返回值的别名,而且只使用与 S 语言。如果你想了解它们具体如何实现,可以去看一下它们的源代码。

不同基本类型的函数一般都是用 C 语言编写的,在调度时使用switch语句(例如:switch(TYPEOF(x)))。尽管你可能没有写过 C 语言,但理解基本类型仍然很有必要,因为其他系统都是在此基础上的:S3 对象可以建立在所有基本类型上,S4 使用一个特殊的基本类型,而 RC 对象是 S4 和 environments(一个特殊的基本类型)的结合体。查看对象是否是一个单纯基本类型(即它不同时含 S3、S4、RC 的行为),使用 is.object(x) ,返回TRUE/FALSSE。


S3:

S3 是 R 语言的第一种也是最简单的一种 OO 系统。它还是唯一一种在基础包和统计包使用的 OO 系统,CRAN包中最平常使用的 OO 系统。

识别对象、泛型函数、方法:

你遇到的大部分对象都是 S3 对象。但不幸的是在 R 中并没有可以简单检测一个方法是否是 S3 的方法。最接近的方法就是 is.object(x) & !isS4(x),即它是一个对象,但不是 S4 对象。一个更简单的方法就是使用 pryr::otype() :

library(pryr)df <- data.frame(x = 1:10, y = letters[1:10])otype(df)    # A data frame is an S3 class#> [1] "S3"otype(df$x)  # A numeric vector isn't#> [1] "base"otype(df$y)  # A factor is#> [1] "S3"

在 S3,方法是属于函数的,这些函数叫做泛型函数,或简称泛型。S3 的方法不属于对象或者类。这和大部分的编程语言都不同,但它确实是一种合法的 OO 方式。

你可以调用 UseMethod() 方法来查看某个函数的源代码,从而确定它是否是 S3 泛型。和 otype() 类似,prpy 也提供了 ftype() 来联系着一个函数(如果有的话)描述对象系统。

mean#> function (x, ...) #> UseMethod("mean")#> <bytecode: 0x24bfa50>#> <environment: namespace:base>ftype(mean)#> [1] "s3"      "generic"

有些 S3 泛型,例如 [ 、sum()、cbind(),不能调用 UseMethod(),因为它们是用 C 语言来执行的。不过它们可以调用 C 语言的函数 DispatchGroup() 和 DispatchOrEval()。利用 C 代码进行方法调用的函数叫作内部泛型。可以使用 ?”internal generic” 查看。

给定一个类,S3 泛型的工作是调用正确的 S3 方法。你可以通过 S3 方法的名字来识别(形如 generic.class())。例如,泛型 mean() 的 Date 方法为 mean.Date(),泛型print() 的向量方法为 print.factor() 。
这也就是为什么现代风格不鼓励在函数名字里使用 “.” 的原因了。类的名字也不使用 “.” 。pryr::ftype() 可以发现这些异常,所以你可以用它来识别一个函数是 S3 方法还是泛型:

ftype(t.data.frame) # data frame method for t()#> [1] "s3"     "method"ftype(t.test)       # generic function for t tests#> [1] "s3"      "generic"

你可以调用 methods() 来查看属于某个泛型的所有方法:

methods("mean")#> [1] mean.Date     mean.default  mean.difftime mean.POSIXct  mean.POSIXlt #> see '?methods' for accessing help and source codemethods("t.test")#> [1] t.test.default* t.test.formula*#> see '?methods' for accessing help and source code

(除了在基础包里面定义的一些方法,大多数 S3 的方法都是不可见的使用 getS3method() 来阅读它们的源码。)

你也可以列出一个给出类中包含某个方法的所有泛型:

methods(class = "ts")#>  [1] aggregate     as.data.frame cbind         coerce        cycle        #>  [6] diffinv       diff          initialize    kernapply     lines        #> [11] Math2         Math          monthplot     na.omit       Ops          #> [16] plot          print         show          slotsFromS3   time         #> [21] [<-           [             t             window<-      window       #> see '?methods' for accessing help and source code

你也可以从接下来的部分知道,要列出所有的 S3 类是不可能的。

定义类和创建对象:

S3 是一个简单而特殊的系统,它对类没有正式的定义。要实例化一个类,你只能拿一个已有的基础对象,再设置类的属性。你可以在创建类的时候使用 structure(),或者事后用 class<-():

# Create and assign class in one stepfoo <- structure(list(), class = "foo")# Create, then set classfoo <- list()class(foo) <- "foo"

S3 对象的属性通常建立在列表或者原子向量之上(你可以用这个属性去刷新你的内存属性),你也能把函数转成 S3 对象,其他基本类型要么在 R 中很少见,要么就是该语义不能很好地在属性下运行。
你可以通过 class() 把类看作任意的对象,也可以通过 inherits(x, “classname”) 来查看某个对象是否继承自某个具体的类。

class(foo)#> [1] "foo"inherits(foo, "foo")#> [1] TRUE

S3 对象所属于的类可以被看成是一个向量,一个通过最重要的特性来描述对象行为的向量。例如对象 glm() 的类是 c(“glm”, “lm”),它表明着广义线性模型的行为继承自线性模型。类名通常是小写的,并且应该避免使用 “.” 。否则该类名将会混淆为下划线形式的 my_class,或者 CamelCase 写法的 MyClass。

大多数的 S3 类都提供了构造函数:

foo <- function(x) {  if (!is.numeric(x)) stop("X must be numeric")  structure(list(x), class = "foo")}

如果它是可用的,则你应该使用它(例如 factor() 和 data.frame())。这能确保你在创造类的时候使用正确的组件。构造函数的名字一般是和类名是相同的。

开发者提供了构造函数之后,S3 并没有对它的正确性做检查。这意味着你可以改变现有对象所属于的类:

# Create a linear modelmod <- lm(log(mpg) ~ log(disp), data = mtcars)class(mod)#> [1] "lm"print(mod)#> #> Call:#> lm(formula = log(mpg) ~ log(disp), data = mtcars)#> #> Coefficients:#> (Intercept)    log(disp)  #>      5.3810      -0.4586# Turn it into a data frame (?!)class(mod) <- "data.frame"# But unsurprisingly this doesn't work very wellprint(mod)#>  [1] coefficients  residuals     effects       rank          fitted.values#>  [6] assign        qr            df.residual   xlevels       call         #> [11] terms         model        #> <0 rows> (or 0-length row.names)# However, the data is still theremod$coefficients#> (Intercept)   log(disp) #>   5.3809725  -0.4585683

如果你在之前使用过其他的 OO 语言,S3 可能会让你觉得很恶心。但令人惊讶的是,这种灵活性带来的问题很少:虽然你能改变对象的类型,但你并不会这么做。R 并不用提防自己:你可以很容易射自己的脚,只要你不把抢瞄在你的脚上并扣动扳机,你就不会有问题。

创建新的方法和泛型:

如果要添加一个新的泛型,你只要创建一个叫做 UseMethod() 的函数。UseMethod() 有两个参数:泛型函数的名字和用来调度方法的参数。如果第二个参数省略了,则根据第一个参数来调度方法。但是没有必要去省略 UseMethod() 的参数,你也不应该这么做。

f <- function(x) UseMethod("f")

没有方法的泛型是没有用的。如果要添加方法,你只需要用 generic.class 创建一个合法的函数:

f.a <- function(x) "Class a"a <- structure(list(), class = "a")class(a)#> [1] "a"f(a)#> [1] "Class a"

用同样的方法可以对已有的泛型添加方法:

mean.a <- function(x) "a"mean(a)#> [1] "a"

如你所看到的,它并没有确保类和泛型兼容的检查机制,它主要是靠编程者自己来确定自己的方法不会违反现有代码的期望。

方法调度:

S3 的方法调度比较简单。UseMethod() 创建一个向量或者一个函数名字(例如:paste0(“generic”, “.”, c(class(x), “default”))),并逐个查找。default 类作为回落的方法,以防其他未知类的情况。

f <- function(x) UseMethod("f")f.a <- function(x) "Class a"f.default <- function(x) "Unknown class"f(structure(list(), class = "a"))#> [1] "Class a"# No method for b class, so uses method for a classf(structure(list(), class = c("b", "a")))#> [1] "Class a"# No method for c class, so falls back to defaultf(structure(list(), class = "c"))#> [1] "Unknown class"

组泛型方法增加了一些复杂性,组泛型为一个函数实现复合泛型的多个方法提供了可能性。它们包含的四组泛型和函数如下:

  • Math: abs, sign, sqrt, floor, cos, sin, log, exp, …
  • Ops: +, -, *, /, ^, %%, %/%, &, |, !, ==, !=, <, <=, >=, >
  • Summary: all, any, sum, prod, min, max, range
  • Complex: Arg, Conj, Im, Mod, Re

组泛型是相对比较先进的技术,超出了本章的范围。但是你可以通过 ?groupGeneric 查看更多相关信息。区分组泛型最关键的是要意识到 Math、Ops、Summary 和 Complex 并不是真正的函数,而是代表着函数。注意在组泛型中有特殊的变量 .Generic 提供实际的泛型函数调用。

如果你有复数类模板的层次结构,那么调用“父”方法是有用的。要准确定义它的意义的话有点难度,但如果当前方法不存在的话它基本上都会被调用。同样的,你可以使用 ?NextMethod 查看相关信息。

因为方法是正规的 R 函数,所以你可以直接调用它:

c <- structure(list(), class = "c")# Call the correct method:f.default(c)#> [1] "Unknown class"# Force R to call the wrong method:f.a(c)#> [1] "Class a"

不过这种调用的方法和改变对象的类属性一样危险,所以一般都不这样做。不要把上膛了的枪瞄在自己的脚上。使用上述方法的唯一原因是它可以通过跳过方法调用达到很大的性能改进,你可以查看性能章节查看详情。

非 S3 对象也可以调用 S3 泛型,非内部的泛型会调用基本类型的隐式类。(因为性能上的原因,内部的泛型并不会这样做。)确定基本类型的隐式类有点难,如下面的函数所示:

iclass <- function(x) {  if (is.object(x)) {    stop("x is not a primitive type", call. = FALSE)  }  c(    if (is.matrix(x)) "matrix",    if (is.array(x) && !is.matrix(x)) "array",    if (is.double(x)) "double",    if (is.integer(x)) "integer",    mode(x)  )}iclass(matrix(1:5))#> [1] "matrix"  "integer" "numeric"iclass(array(1.5))#> [1] "array"   "double"  "numeric"

练习:

  1. 查阅 t() 和 t.test() 的源代码,并证明 t.test() 是一个 S3 泛型而不是 S3 方法。如果你用 test 类创建一个对象并用它调用 t() 会发生什么?
  2. 在 R 语言的基本类型中什么类有 Math 组泛型?查阅源代码,该方法是如何工作的?
  3. R 语言在日期时间上有两种类,POSIXct 和 POSIXlt(两者都继承自 POSIXt)。哪些泛型对于这两个类是有不同行为的?哪个泛型共享相同的行为?
  4. 哪个基本泛型定义的方法最多?
  5. UseMethod() 通过特殊的方式调用方法。请预测下列代码将会返回什么,然后运行一下,并且查看 UseMethod() 的帮助文档,推测一下发生了什么。用最简单的方式记下这些规则。
y <-1g <-function(x) {  y <-2UseMethod("g")}g.numeric <-function(x) yg(10)h <-function(x) {  x <-10UseMethod("h")}h.character <-function(x) paste("char", x)h.numeric <-function(x) paste("num", x)h("a")
  1. 内部泛型不分配在基类类型的隐式类。仔细查阅 ?”internal generic”,为什么下面例子中的 f 和 g 的长度不一样?哪个函数可以区分 f 和 g 的行为?
f <- function() 1g <- function() 2class(g) <- "function"class(f)class(g)length.function <- function(x) "function"length(f)length(g)


S4:

S4 工作的方式和 S3 比较相似,但它更加正式和严谨。方法还是属于函数,而不是类。但是:

  • 类在描述字段和继承结构(父类)上有更加正式的定义。
  • 方法调用可以传递多个参数,而不仅仅是一个。
  • 出现了一个特殊的运算符——@,从 S4 对象中提取 slots(又名字段)。

所以 S4 的相关代码都存储在 methods 包里面。当你交互运行 R 程序的时候这个包都是可用的,但在批处理的模式下则可能不可用。所以,我们在使用 S4 的时候一般直接使用 library(methods) 。
S4 是一种丰富、复杂的系统,并不是一两页纸能解释完的。所以在此我把重点放在 S4 背后的面向对象思想,这样大家就可以比较好地使用 S4 对象了。如果想要了解更多,可以参考以下文献:

  • S4 系统在 Bioconductor 中的发展历程
  • John Chambers 写的《Software for Data Analysis》
  • Martin Morgan 在 stackoverflow 上关于 S4 问题的回答

识别对象、泛型函数和方法:

要识别 S4 对象 、泛型、方法还是很简单的。对于 S4 对象:str() 将它描述成一个正式的类,isS4() 会返回 TRUE,prpy::otype() 会返回 “S4” 。对于 S4 泛型函数:它们是带有很好类定义的 S4 对象。
常用的基础包里面是没有 S4 对象的(stats, graphics, utils, datasets, 和 base),所以我们要从内建的 stats4 包新建一个 S4 对象开始,它提供了一些 S4 类和方法与最大似然估计:

library(stats4)# From example(mle)y <- c(26, 17, 13, 12, 20, 5, 9, 8, 5, 4, 8)nLL <- function(lambda) - sum(dpois(y, lambda, log = TRUE))fit <- mle(nLL, start = list(lambda = 5), nobs = length(y))# An S4 objectisS4(fit)#> [1] TRUEotype(fit)#> [1] "S4"# An S4 genericisS4(nobs)#> [1] TRUEftype(nobs)#> [1] "s4"      "generic"# Retrieve an S4 method, described latermle_nobs <- method_from_call(nobs(fit))isS4(mle_nobs)#> [1] TRUEftype(mle_nobs)#> [1] "s4"     "method"

用带有一个参数的 is() 来列出对象继承的所有父类。用带有两个参数的 is() 来验证一个对象是否继承自该类:

is(fit)#> [1] "mle"is(fit, "mle")#> [1] TRUE

你可以使用 getGenerics() 来获取 S4 的所有泛型函数,或者使用 getClasses() 来获取 S4 的所有类。这些类包括 S3 对 shim classes 和基本类型。另外你可以使用 showMethods() 来获取 S4 的所有方法。

定义类和新建对象

在 S3,你可以通过更改类的属性就可以改变任意一个对象,但是在 S4 要求比较严格:你必须使用 setClass() 定义类的声明,并且用 new() 新建一个对象。你可以用特殊的语法 class?className(例如:class?mle)找到该类的相关文档。
S4 类有三个主要的特性:

  • 名字:一个字母-数字的类标识符。按照惯例,S4 类名称使用 UpperCamelCase 。
  • 已命名的 slots(字段),它用来定义字段名称和允许类。例如,一个 person 类可能由字符型的名称和数字型的年龄所表征:list(name = "character", age = "numeric")
  • 父类。你可以给出多重继承的多个类,但这项先进的技能增加了它的复杂性。

slotscontains,你可以使用setOldClass()来注册新的 S3 或 S4 类,或者基本类型的隐式类。在slots,你可以使用特殊的ANY类,它不限制输入。
S4 类有像 validity 方法的可选属性,validity 方法可以检验一个对象是否是有效的,是否是定义了默认字段值的 prototype 对象。使用?setClass查看更多细节。
下面的例子新建了一个具有 name 字段和 age 字段的 Person 类,还有继承自 Person 类的 Employee 类。Employee 类从 Person 类继承字段和方法,并且增加了字段 boss 。我们调用 new() 方法和类的名字,还有name-values这样成对的参数值来新建一个对象。

setClass("Person",  slots = list(name = "character", age = "numeric"))setClass("Employee",  slots = list(boss = "Person"),  contains = "Person")alice <- new("Person", name = "Alice", age = 40)john <- new("Employee", name = "John", age = 20, boss = alice)

大部分 S4 类都有一个和类名相同名字的构造函数:如果有,可以直接用它来取代 new()
要访问 S4 对象的字段,可以用 @ 或者 slot()

alice@age#> [1] 40slot(john, "boss")#> An object of class "Person"#> Slot "name":#> [1] "Alice"#> #> Slot "age":#> [1] 40

@$ 等价,slot()[] 等价)
如果一个 S4 对象继承自 S3 类或者基本类型,它会有特殊的属性 .Data

setClass("RangedNumeric",  contains = "numeric",  slots = list(min = "numeric", max = "numeric"))rn <- new("RangedNumeric", 1:10, min = 1, max = 10)rn@min#> [1] 1rn@.Data#>  [1]  1  2  3  4  5  6  7  8  9 10

因为 R 是响应式编程的语言,所以它可以随时创建新的类或者重新定义现有类。这将会造成一个问题:当你在响应式地调试 S4 的时候,如果你更改了一个类,你要知道你已经把该类的所有对象都更改了。

新建方法和泛型函数

S4 提供了特殊的函数来新建方法和泛型。setGeneric() 将产生一个新的泛型,或者把已有函数转成泛型。

setGeneric("union")#> [1] "union"setMethod("union",  c(x = "data.frame", y = "data.frame"),  function(x, y) {    unique(rbind(x, y))  })#> [1] "union"

如果你要重新创建了一个泛型,你需要调用 standardGeneric() :

setGeneric("myGeneric", function(x) {  standardGeneric("myGeneric")})#> [1] "myGeneric"

S4 中的 standardGeneric() 相当于 UseMethod()


测试的答案

  1. 要确定一个对象属于哪种面向对象系统,你可以用排除法,如果 !is.object(x) 返回 TRUE,那么它是一个基本对象。如果 !isS4(x) 返回 TRUE,那么它是一个 S3 。如果 !is(x, "refClass") 返回 TRUE, 那么它是一个 S4 ,否则它是 RC 。
  2. typeof() 来确定基本类型的对象。
  3. 泛型函数调用特殊方法的时候主要是通过它的参数输入来确定的,在 S3 和 S4 系统,方法属于泛型函数,不像其他编程语言那样属于类。
  4. S4 比 S3 更加正式,并且支持多重继承和多重调度,RC 对象的语义和方法是属于类的,而不属于函数。
0 0
原创粉丝点击