检查对象类型

虽然 R 中的一切都是对象,但它们有不同的类型。
试想我们正在处理一个用户自定义的对象,那么,就要根据输入对象的类型来创建相
应的函数。例如,我们需要创建一个名为 take_it 的函数,如果输入对象是一个原子向量(例
如数值向量、字符向量或者逻辑向量),函数就返回输入对象的第 1 个元素;如果输入对象
是一个包含数据和索引的列表,函数就返回一个用户自定义的元素。
举个例子:如果输入一个数值向量,例如 c(1,2,3),函数返回第 1 个元素 1;同样,
若输入一个字符向量,例如 c("a", "b", "c"),函数就会返回 a。但是,若输入一个列
表 list(data=c("a","b","c"),index=3),函数就应该返回 data 的第 3 个元素
(index=3),也就是 c。
为了创建这样的函数,我们分析一下可能的逻辑流。首先,由于函数的输出取决于输
入对象的类型,需要使用 is.* 函数来判别输入对象是否属于某个类型;其次,输入对象
的类型不同,函数的处理方式也不同,那么,就需要使用条件表达式,例如使用 if else 来
对逻辑流进行分支;最后,函数会从输入对象中提取某个元素,因此,我们需要使用元素
提取操作符(element-extraction operator)。现在,函数的实现流程就变得相当清晰了:
take_it <- function(x) {
if (is.atomic(x)) {
x[[1]]
} else if (is.list(x)) {
x$data[[x$index]]
} else {
stop("Not supported input type")
}
}
上面这个函数的行为随 x 取值类型的不同而不同。当 x 为原子向量(例如数值向量)
时,函数提取该原子向量的第 1 个元素;当 x 为一个包含数据和索引的列表时,函数根据
索引 index 的取值来抽取数据 x$data 的相应元素:
take_ _it(c(1, 2, 3))
## [1] 1
take_ _it(list(data = c("a", "b", "c"), index = 3))
## [1] "c"
若输入不支持的类型,函数不会输出其他任何值,而是终止运行并返回错误信息。例
如,take_it 不能处理函数型输入。注意到,一般情况下,就像任何其他对象一样,我们
可以将函数传递给其他函数作为参数。然而,在这种情况下,如果均值函数作为函数传递
给 take_it,它将转向 else 条件并停止运行:
take_ _it(mean)
## Error in take_it(mean): Not supported input type
如果输入一个既不包含 data 又不包含 index 的列表会怎样呢?我们做个试验看看
将会发生什么?输入一个包含 input(而不是 data),不包含 index 的列表:
take_ _it(list(input = c("a", "b", "c")))
## NULL
你可能会感到惊讶,函数居然没有报错!因为 x$data 为 NULL,而从 NULL 中提取
任何值仍为 NULL,所以函数输出了 NULL:
NULL[[1]]
## NULL
NULL[[NULL]]
## NULL
然而,如果一个列表只有 data 没有 index,函数就会报错:
take_ _it(list(data = c("a", "b", "c")))
## Error in x$data[[x$index]]: attempt to select less than one element
报错是因为 x$index 为 NULL,根据 NULL 从一个向量中提取元素就产生了错误:
c("a", "b", "c")[[NULL]]
## Error in c("a", "b", "c")[[NULL]]: attempt to select less than one
element
第 3 种情况和第 1 种情况有点类似,从 NULL 提取任何值仍为 NULL:
take_ _it(list(index = 2))
## NULL
如果你对 NULL 参与计算的这些极端情况不熟悉的话,就会觉得上述异常情况的错误
信息并没有提供有价值的信息。对于更复杂的情况,即使产生了错误信息,你也难以在短
时间内找出确切原因。一个不错的解决办法是,在运行函数的时候检查输入,并通过参数
来反映函数的假设条件。
为了避免上述误用情况,下面这个函数对每个参数的类型都进行了检查:
take_it2 <- function(x) {
if (is.atomic(x)) {
x[[1]]
} else if (is.list(x)) {
if (!is.null(x$data) && is.atomic(x$data)) {
if (is.numeric(x$index) && length(x) ==1) {
x$data[[x$index]]
} else {
stop("Invalid index")
}
} else {
stop("Invalid data")
}
} else {
stop("Not supported input type")
}
}
当 x 是一个列表时,我们检查 x$data 是否不为空,同时是一个原子向量。如果确
实如此,再检查 x$index 是否被正确设定为一个单元素的数值向量(或标量)。其中任
何一个条件被违背,函数就会停止运行,并输出一条错误信息告知用户有关输入的错误
原因。
但是,内置的检查函数也会出现古怪行为。例如,is.atomic(NULL) 返回 TRUE。
因此,即使列表 x 不包含名为 data 的成分,分支 if(is.atomic(x$data)) 也会被触
发(返回 TRUE),但该行判断语句仍会返回 NULL。包含了这些参数检验条件之后,函数
会变得更加稳健,而且,当假设条件被违背时,也会输出有参考价值的错误信息:
take_ _it2(list(data = c("a", "b", "c")))
## Error in take_it2(list(data = c("a", "b", "c"))): Invalid index
take_ _it2(list(index = 2))
## Error in take_it2(list(index = 2)): Invalid data
这个函数的另一种实现方法是利用 S3 方法分派,我们将在第 10 章中进行介绍。
识别对象的类和类型
除了使用 is.* 函数,我们也可以用 class( ) 或者 typeof( ) 实现这个功能。在
直接判断对象的类型前,有必要了解一下这两个函数的区别。
接下来的例子展示了 class( )和 typeof( )作用在不同类型的对象上时,它们输
出信息的不同之处。
以下例子中,对于每种对象 x,class( )和 typeof( )都被调用,并使用 str( )来
展示对象的结构。
对于数值向量:
x <- c(1, 2, 3)
class(x)
## [1] "numeric"
typeof(x)
## [1] "double"
str(x)
## num [1:3] 1 2 3
对于整数向量:
x <- 1:3
class(x)
## [1] "integer"
typeof(x)
## [1] "integer"
str(x)
## int [1:3] 1 2 3
对于字符向量:
x <- c("a", "b", "c")
class(x)
## [1] "character"
typeof(x)
## [1] "character"
str(x)
## chr [1:3] "a" "b" "c"
对于列表:
x <- list(a = c(1, 2), b = c(TRUE, FALSE))
class(x)
## [1] "list"
typeof(x)
## [1] "list"
str(x)
## List of 2
## $ a: num [1:2] 1 2
## $ b: logi [1:2] TRUE FALSE
对于数据框:
x <- data.frame(a = c(1, 2), b = c(TRUE, FALSE))
class(x)
## [1] "data.frame"
typeof(x)
## [1] "list"
str(x)
## 'data.frame': 2 obs. of 2 variables:
## $ a: num 1 2
## $ b: logi TRUE FALSE
可以看到,typeof( )返回对象的低级内部类型,class( )返回对象的高级类。我
们之前提到过,data.frame 本质上就是具有等长成分的 list。因此,一个数据框的类
(class)就是 data.frame,但 typeof( )返回它的内部类型就是 list。
这个主题与 S3 面向对象编程机制有关,具体细节将在后续章节中介绍。但这里有必要
提一下 class( )和 typeof( )的区别。
从上面的输出结果中,你也可以清晰地认识 str( )的功能,它展示了一个对象的结
构。这一点我们在前面的章节也介绍过。对于对象中的向量,str( )会展示它们的内部类
型(typeof( ))。

 

posted @ 2019-01-22 11:01  NAVYSUMMER  阅读(193)  评论(0编辑  收藏  举报
交流群 编程书籍