R语言入门学习笔记(六)
前言
通过前面的学习,我们已经学习了如何去编写R程序,但对于一些个性化的需求可能前面所学的知识还不能满足。比如我们在命令行输入R对象时,命令行可以显示出R对象的内容,其实是调用了print函数来实现的,但是某些时候,print函数返回的格式不是我们想要看到的,如何去更改呢?这就要涉及到这节我们要学到的R的S3系统。
一、S3系统
S3指的是R自带的类系统,它掌管着R如何处理具有不同类的对象。一些函数在调用时会先查询对象的S3类,然后再根据其属性做出相应的反应。(也就是说,该系统其实是一个分配系统,函数调用时会根据对象的类去分配给相应的代码。)
print
就是这样的一个函数。输入数值型向量时,print会显示一个数字:
num <- 666666
print(num)
#> [1] 666666
但如果赋予该数字日期时间的类属性:
class(num) <- c("POSIXct", "POSIXt")
print(num)
#> [1] "1970-01-09 01:11:06 CST"
它就会显示时间。如果使用的对象具有类属性,那么他就会接触到R的S3系统。这个系统的行为乍看起来会觉得奇怪,因为平时并不会在意这个系统的存在,但是当理解了它的工作模式后,就会变得理所当然了。
S3系统有三个组成部分:属性(attribute)(特别是class属性)、泛型函数(generic function)和方法(method)。
1.属性
在R语言入门学习笔记(二)中,我们了解到R对象具有属性的概念。属性不会影响对象的具体取值,是作为对象某种类型元数据的存在,可以用于R控制和管理该对象。比如,数据框会将其行名和列名存储为属性,还会将类data.frame存储为一个属性。
前面我们知道,可以用attributes函数查看一个对象的属性。R还提供了一些函数可以帮助设置和获取一些常见的属性。此前已经用过的函数如:names、dim、class等对应着同名的属性。R还有如row.names、levels等函数也可以用于获取或者通过赋值来改变某个属性的值。如果对象本身没有的属性,也可以通过这些函数来赋予。
关于属性,R持放任态度,允许用户为对象添加任何属性。可以使用attr
函数给某个对象添加任何属性,也可以用它来查询相应的属性。它接受两个参数,R对象和属性名称(字符串形式),如果要赋予R对象某个属性,需要将某个值保存到attr的输出结果:
attributes(num)
#> $class
#> [1] "POSIXct" "POSIXt"
attr(num, "description") <- c("year", "month", "day", "time")
attributes(num)
#> $class
#> [1] "POSIXct" "POSIXt"
#>
#> $description
#> [1] "year" "month" "day" "time"
# attr(num, "description")可用于查找相应属性
如果将某个新属性赋予原子型向量,R通常会在向量值得下方显示该属性。但改变了向量的类(class)属性时,R的显示方法可能会变,比如前面POSIXct对象就是添加新属性依然保持自己的显示方式,这和后续我们将学到的方法(S3的组成部分)有关:
num
#> [1] "1970-01-09 01:11:06 CST"
class(num) <- NULL
num
#> [1] 666666
#> attr(,"description")
#> [1] "year" "month" "day" "time"
除非赋予的属性是R原本就存在的属性(或者说设置过的)比如names或class等,否则R通常会会略这个属性。
另外,还有个更加强大的structure
函数,它可以创建带有一组属性的R对象。该函数的第一个参数是R对象或者R对象的取值,剩下的参数是想要添加个这个对象的属性。用法如下:
a = 6
b = 8
structure(num, A = a, B = b)
#> [1] 666666
#> attr(,"description")
#> [1] "year" "month" "day" "time"
#> attr(,"A")
#> [1] 6
#> attr(,"B")
#> [1] 8
2.泛型函数
泛型函数指的是在不同的场合下,完成不同任务的函数。print就是一个泛型函数,所以它面对不同类(class)的对象有着不同的输入方式。这个函数的使用非常广泛,因为它的调用很多时候发生在后台,所以可能我们并未察觉。在命令行键入某个对象(请注意函数也是R对象的一种),都会进行调用以显示对象内容。
可以通过改写print函数,更改R的显示方式,来达到个性化的效果。接下来,让我们进入print的源代码看看它的实现逻辑。
3.方法
调用print的时候,其实它调用了一个特别的函数叫UseMethod。
print
#> function (x, ...)
#> UseMethod("print")
#> <bytecode: 0x000001e0d2bd3dc0>
#> <environment: namespace:base>
UseMethod函数会检查提供给print函数的第一个参数的类属性,然后将该对象交给专门为这一类设计的新函数处理。比如提供给print一个POSIXct对象时,UseMethod函数就会将它交给print.POSIXct函数处理:
print.POSIXct
#> function (x, tz = "", usetz = TRUE, max = NULL, ...)
#> {
#> if (is.null(max))
#> max <- getOption("max.print", 9999L)
#> FORM <- if (missing(tz))
#> function(z) format(z, usetz = usetz)
#> else function(z) format(z, tz = tz, usetz = usetz)
#> if (max < length(x)) {
#> print(FORM(x[seq_len(max)]), max = max + 1, ...)
#> cat(" [ reached 'max' / getOption(\"max.print\") -- omitted",
#> length(x) - max, "entries ]\n")
#> }
#> else if (length(x))
#> print(FORM(x), max = max, ...)
#> else cat(class(x)[1L], "of length 0\n")
#> invisible(x)
#> }
#> <bytecode: 0x000001e0d3056258>
#> <environment: namespace:base>
对于其他class属性的对象,print也会有相应的函数处理,比如print.factor、print.table等。像这样的就被称为是print函数的方法(method)。这些方法本身也是R函数,但是特别的是UseMethod会调用他们去处理相应类的对象。
将某个泛型函数作为输入对象允许methods
函数,可以看到该泛型函数所支持的方法:
methods(print)
#> [1] print.acf*
#> [2] print.activeConcordance*
#> [3] print.anova*
#> [4] print.aov*
#> [5] print.aovlist*
#> [6] print.ar*
#> ...
这里省略了它的输出,总共有195种方法。像这样的一个由泛型函数、方法和基于类的分派方式组成的系统就是R的S3系统。之所以这个叫法,是因为它起源于S语言的第三个版本,这时R语言的前身。许多常见的R函数都是泛型函数,比如summary和head等,都是调用UseMethod函数识别对象的类属性。另外还有许多基本R函数,比如c
、+
、-
和<
等,其工作方式类似于泛型函数,只是它们不调用UseMethod函数,而是调用.primitive
函数。
二、S3系统的使用
1.方法分配
UseMethod在匹配方法与函数时使用了一个非常简单的系统。每个S3方法的名称都包含两个部分,即泛型函数名.类属性名
。当UseMethod需要调用某个方法时,会搜索是否存在一个R函数的名称符合以上的S3风格即可。
我们可以试着写一个自己的函数,为其取一个S3风格的名称。比如,我们赋给num一个新属性,属性名称不重要,这里我们假设是new
。
class(num) <- "new"
现在,可以为new类写一个S3型的print类方法函数了。这个函数不需要特别之处,但必须要命名为print.new
,且它接受的输入要与print函数一致:
args(print)
#> function (x, ...)
#> NULL
print.new <- function(x, ...){
cat("test")
}
之后在输入num时:
num
#> test
# 删除该函数
rm(print.new)
对于具有多个类属性的对象,UseMethod会先匹配其第一个属性,匹配不到再匹配第二个,以此类推。如果均不行,将会使用默认方法,在print中是print.default
。
补充两个可能会用到的函数:cat()函数
用于打印结果,其打印的字符串不带引号;paste()
函数用于连接字符串,paste(R对象, collapse = "...")
是将R对象中字符压成一个字符串,以...
作为间隔,paste(R对象1, R对象2, sep = ",")
是将两个或多个R对象组合起来,中间加入,
。
2.类
可以利用R的S3系统为对象创建一个稳健的类。需要:
(1)给类取名字
(2)给属于该类的对象赋class属性
(3)给属于该类的对象编写常用的泛型函数方法
许多R包都是建立在类似方式创建的类上。其实为类创建方法并不轻松。可以针对某个类调用methods函数来查找指定class类属性的方法。
methods(class = "factor")
#> [1] [ [[ [[<- [<- all.equal
#> [6] as.character as.data.frame as.Date as.list as.logical
#> [11] as.POSIXlt as.vector c coerce droplevels
#> [16] format initialize is.na<- length<- levels<-
#> [21] Math Ops plot print relevel
#> [26] relist rep show slotsFromS3 summary
#> [31] Summary xtfrm
#> see '?methods' for accessing help and source code
注意如果有些特定包中的函数,未加载这个包是不会出现在methods的返回结果中的。
3.S3调试与S4、R5简介
S3系统可能会给理解R函数带来困扰,因为我们在尝试理解某个函数时会看到其调用了UseMethod函数。但是现在,我们已经知道了,所以我们可以直接去找到其类方法函数来查看其源代码。这个函数是符合function.class
或function.default
的。
关于查看函数源代码的方法,下面介绍几种:
(1)通过RStudio快捷键F2,光标放置在函数处,点F2即可进入独立查看窗口。
(2)直接输入函数名查看,对于一些简单未封装的函数可以这样查看。
(3)针对S3函数,运行methods查看到具体类方法函数后,对于不带星号的,可以直接输入函数名查看;带星号的运行getAnywhere("函数名")
查看。
(4)使用edit()
函数查看,用此函数还可以直接改写保存某个函数。
R还有另外两个创建类属性行为的系统,是S4和R5系统,后者也叫引用类系统。相比S3,其使用难度更大,也更少见。然而,它们提供了S3系统没有的防护措施。在这里先不过多介绍了。
总结
本节介绍了R的S3系统,这是一个为不同类属性数据个性化定制处理方案的一个系统。运用它可以实现许多关于class属性的操作。许多R函数都遵循这一系统,它们是泛型函数,是依据类属性的不同来分配对应的类方法。了解并学习这些函数的创建规则有助于日后自己创建属于自己的特定类属性。