泛型函数和方法分派

正如本章前面提到的,一个类可以用多种方法定义它的行为,尤其是它与其他类的关
系。在 S3 系统中,我们可以创建泛型函数(generic function),对于不同的类,由泛型函数
决定调用哪个方法,这就是 S3 方法分派(method dispatch)的工作机理。对象的类不同,
其方法分派也不同。因此,区分对象的类就显得非常重要。

R 中有很多 S3 泛型函数的简单示例,每个都是基于某个通用目的定义的,但是允许不同
类的对象使用不同方法来达到这个目的。我们先看一下 head( ) 和 tail( ) 这两个函数。
它们的功能很简单:head( ) 展示一个数据对象的前 n 条记录,tail( ) 展示一个数据对
象的后 n 条记录。这与 x[1:n] 是不同的,因为对于不同类的对象,其记录的定义是不同的。
对于原子向量(数值向量、字符向量等),前 n 条记录就是指前 n 个元素。但对于数据框而言,
前 n 条记录是指前 n 行而不是前 n 列。由于数据框本质上就是列表,所以直接从数据框中取出
前 n 个元素(成分)就是指取出前 n 列,而这并不是 head( ) 函数的目的。
首先,我们输入 head 看一下函数的内部信息:
head
## function (x, ...)
## UseMethod("head")
## <bytecode: 0x000000000f052e10>
## <environment: namespace:utils>
我们发现函数中并没有实际的操作细节。相反,它调用 UseMethod("head") 来使所谓的
泛型函数 head( ) 执行方法分派,也就是说,对于不同的类,它可能有不同的行为方式。
现在,我们分别创建一个 numeric 类的数据对象和一个 data.frame 类的数据对象。
然后,依次把每个对象传递给泛型函数 head( ),看看方法分派如何工作:
num_vec <- c(1, 2, 3, 4, 5)
data_frame <- data.frame(x = 1:5, y = rnorm(5))
对于数值向量,head( ) 只是提取了它的前几个元素:
head(num_vec, 3)
## [1] 1 2 3
但是,对于数据框,head( ) 提取了数据框的前几行而不是前几列:
head(data_frame, 3)
## x y
## 1 1 0.8867848
## 2 2 0.1169713
## 3 3 0.3186301
这里使用一个函数来模拟 head( ) 的行为。以下代码是一个简单实现,它提取任意
给定对象 x 的前 n 个元素:
simple_head <- function(x, n) {
x[1:n]
}
对于数值向量,这个函数的工作方式与 head( ) 完全相同:
simple_ _head(num_vec, 3)
## [1] 1 2 3
但是,对于数据框,这个函数想要提取前 n 列。我们知道,一个数据框就是一个列表,
数据框的每一列对应列表的每一个成分。当 n 超过数据框的列数(或等价于列表的成分数)
时,就会产生报错信息:
simple_ _head(data_frame, 3)
## Error in `[.data.frame`(x, 1:n): undefined columns selected
为了改进实现方式,我们可以事先检查输入对象 x 是不是数据框:
simple_head2 <- function(x, n) {
if (is.data.frame(x)) {
x[1:n,]
} else {
x[1:n]
}
}
现在,对于原子向量和数据框,simple_head2( ) 的行为与 head( ) 几乎完全一样:
simple_ _head2(num_vec, 3)
## [1] 1 2 3
simple_ _head2(data_frame, 3)
## x y
## 1 1 0.8867848
## 2 2 0.1169713
## 3 3 0.3186301
但是,head( ) 能做的不止这些。我们可以调用 method( )(返回一个字符向量)
来查看 head( ) 可以实现的所有方法:
methods("head")
## [1] head.data.frame* head.default* head.ftable*
## [4] head.function* head.matrix head.table*
## see '?methods' for accessing help and source code
从返回结果可以看出,head( ) 已经有一系列内置方法适用于多种类,而不止向量和数据
框。注意到,方法都是以 method.class 形式表示。如果我们输入一个 data.frame 对象,
head( )内部就会调用 head.data.frame 方法。类似地,如果我们输入一个 table 对象,
head( ) 内部会调用 head.table 方法。如果我们输入一个数值向量会怎样呢?如果函数定
义了method.default 方法,那么,当没有方法可以匹配输入对象的类时,函数便会自动转向
method.default 方法。在这种情况下,所有的原子向量都会被匹配到 head.default 方法。
这个通过泛型函数为给定输入对象选择合适方法的过程称为方法分派。
似乎我们总是可以在一个函数中检查输入对象的类来完成方法分派。但是,拓展泛型
函数的功能,使其对其他类也能分派方法会更简单一点,因为你不需要每次增加特定的类
检查条件来修改原始泛型函数。我们将会在本章的后续部分讲解这些内容。

posted @ 2019-02-11 10:48  NAVYSUMMER  阅读(209)  评论(0编辑  收藏  举报
交流群 编程书籍