这是用户在 2024-11-10 1:11 为 http://localhost:5941/S3%20classes.html 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

12  S3 classes

12.1 R 中的 S3 类

在 R 中,管理复杂数据结构可能会很有挑战性,尤其是在处理大型数据集(如矩阵和数据框)时。这就是 S3 类的作用,它提供了一种简单而有效的方式来实现面向对象编程。

12.1.1 抽象的必要性

人脑在处理过于具体的信息时会感到困难,这促使我们根据相似性对信息进行分组和分类。在编程中,这转化为数据表示中的抽象需求。与其记住大型对象的确切结构,不如使用它们的类属性更为方便。

12.1.2 使用 S3 进行面向对象编程

S3 是 R 中的一种轻量级面向对象系统,它简化了基于对象类的方法调用过程。以下是其关键特征的概述:

  • 泛型和方法:在 S3 中,泛型是根据传递给它的对象的类而表现出不同功能的函数。例如,print() 是 R 中的一个泛型函数。当您调用 print(y) 时,R 会自动根据 y 的类(在本例中是数据框)确定使用哪个方法。

  • 简单结构:与其他更复杂的面向对象系统不同,S3 的设计旨在简化。通过向对象添加属性来定义类,使 R 能够识别对象的类型,而无需正式的类声明。

  • 普遍性:S3 类在 R 编程中被广泛使用。许多核心数据结构(如因子、矩阵和数据框)都是基于 S3 原则构建的。

12.1.3 S3 类的优点

  1. 简单性:S3 系统易于理解和实现,使所有 R 用户都能轻松使用。

  2. 灵活性:您可以创建自定义类和方法,而无需大量的样板代码。

  3. 集成性:许多现有的 R 函数和包使用 S3 类,允许与其他代码无缝集成。

    最终,泛型和方法只是普通的 R 函数,而类只是附加的对象属性。

在 R 中,对象类型(object type)和(class)是两个相关但不同的概念。以下是它们之间的主要区别:

12.1.4 对象类型(Object Type)和类的关系

12.1.4.1 对象类型(Object Type)

  • 定义:对象类型是指 R 中数据的基本结构和存储方式。它描述了数据的基本特征和特性。
  • 示例:常见的对象类型包括:
    • 向量(vector):如 numericintegercharacterlogical
    • 矩阵(matrix):二维数据结构,所有元素必须是同一类型。
    • 数据框(data frame):表格数据结构,可以包含不同类型的列。
    • 列表(list):可以包含不同类型的元素,甚至可以包含其他列表。
  • 使用:了解对象类型有助于选择合适的操作和函数。

12.1.4.2 类(Class)

  • 定义:类是 R 中用于实现面向对象编程的概念。它定义了一组对象的属性和方法的集合。类通常是基于对象类型的扩展。
  • 示例:常见的类包括:
    • S3 类:一种简单的面向对象系统,例如,通过 class() 函数可以为对象指定一个或多个类。
    • S4 类:更严格的面向对象系统,支持多重继承和更强的类型检查。
    • R6 类:提供了更现代的面向对象编程特性,如封装和继承。
  • 使用:类定义了如何创建和操作对象的规则,以及如何处理特定类型的数据。

12.1.4.3 关系

  • 对象类型与类的关系:对象类型通常决定了类的特性。例如,一个 data frame 对象的类型是 data.frame,它是一个特定类的实例。
  • 行为与属性:类定义了对象的行为(方法)和属性(数据),而对象类型主要关注数据的存储和结构。

12.1.4.4 示例

以下是一个简单的示例,展示对象类型和类的关系:

# 创建一个向量
vec <- c(1, 2, 3)

# 检查对象类型
typeof(vec)  # 输出: "double" (对象类型)
[1] "double"
# 设置 S3 类
class(vec) <- "my_vector"

# 检查类
class(vec)  # 输出: "my_vector" (类)
[1] "my_vector"

在这个示例中: typeof(vec) 返回对象的基本类型(double)。 class(vec) 返回对象的类(my_vector),这定义了如何处理这个对象。

  • 对象类型:描述数据的基本结构和存储方式。
  • :定义了对象的属性和方法,是面向对象编程的核心概念。

基本数据结构类型与类

typeof(NULL)
[1] "NULL"
class(NULL)
[1] "NULL"
cat("-------------------\n")
-------------------
typeof(c(TRUE,FALSE,NA))
[1] "logical"
class(c(TRUE,FALSE,NA))
[1] "logical"
cat("-------------------\n")
-------------------
typeof(c(1,3,5,6))
[1] "double"
class(c(1,3,5,6))
[1] "numeric"
cat("-------------------\n")
-------------------
typeof("hello")
[1] "character"
class("hello")
[1] "character"
cat("-------------------\n")
-------------------
typeof(list(1,3,5,6))
[1] "list"
class(list(1,3,5,6))
[1] "list"
# 函数类型与类
typeof(\(x)x)
[1] "closure"
class(\(x)x)
[1] "function"

12.1.5 设置class

使用structure设置属性:

xt <- structure(123, class = "POSIXct")
dt <- structure(123, class = "Date")

使用typeof查看xt和dt的类型:

# 两者的存储方式一样
c(typeof(xt), typeof(dt))
[1] "double" "double"
# 1970-01-01T00:00:00+0000(时间戳) 起的秒数
print(xt)
[1] "1970-01-01 08:02:03 CST"
cat("-------------\n")
-------------
# 1970-01-01T00:00:00+0000(时间戳) 起的天数
print(dt)
[1] "1970-05-04"

在 R 中,POSIXct 和 Date 都是用于表示日期和时间的类,但它们有一些重要的区别。以下是这两者的主要差异:

  1. 表示的内容 POSIXct: 表示日期和时间的完整信息,包括日期、时间(小时、分钟、秒)以及时区。 以自1970年1月1日(UTC)以来的秒数进行内部存储。 Date: 仅表示日期,不包括时间信息。 以自1970年1月1日(UTC)以来的天数进行内部存储。
  2. 使用场景 POSIXct: 适用于需要精确时间戳的场景,例如记录事件的确切时间、时间序列分析等。 可以处理时区问题,适合跨时区的数据处理。 Date: 适用于只关注日期而不需要时间的场景,例如统计分析中的日期数据、日历事件等。 更加简单,存储和计算效率更高。

使用attr查看class

attr(xt, "class")
[1] "POSIXct"
attr(dt, "class")
[1] "Date"

类型转换

y <- as.numeric(xt)
y
[1] 123

把属性去掉

y <- unclass(dt)
y
[1] 123
# 使用替代函数去掉属性
`attr<-`(dt,"class", NULL)
[1] 123

12.1.5.1 iris示例

data("iris")

x <- iris[1:5, 1:2]
print(x)
  Sepal.Length Sepal.Width
1          5.1         3.5
2          4.9         3.0
3          4.7         3.2
4          4.6         3.1
5          5.0         3.6
typeof(x)
[1] "list"
class(x)
[1] "data.frame"

虽然目前还没有详细讨论data.frame,但从当前的角度来看,我们应该知道,R 数据框实际上就是同长度向量的列表,并且附带了 names 和 row.names 属性。

attr(x,"row.names")
[1] 1 2 3 4 5

揭示 x 的实际表示方式使我们能够利用我们已经通过学习本书前半部分的材料(包括所有练习)所掌握的广泛技能来处理它。这一点值得注意,因为某些内置和第三方数据类型并不是特别设计得很合理。

属性是 R 对象的简单附加。然而,某些属性是特殊的,而类就是其中之一。特别地,我们只能将类设置为字符向量(可能长度大于一)。

x <- 123
attr(x, "class") <- 1

Error in attr(x, "class") <- 1 : attempt to set invalid 'class' attribute

class函数可以阅读class属性,也可以对其进行替换。

x <- 123
class(x)
[1] "numeric"
class(x) <- "Date"
class(x)
[1] "Date"

12.2 泛型与方法调度

12.2.1 泛型,默认,和自定义方法

print(print)
function (x, ...) 
UseMethod("print")
<bytecode: 0x10eb8ee38>
<environment: namespace:base>

从现在开始,我们将称上述任何函数为 “泛型”(S3 泛型,来自 S 版本 )。它的唯一任务是调用 UseMethod("print")。它根据第一个参数的类将控制流调度到另一个函数,称为方法。传递给泛型的所有参数在被调度的方法中也将可用。

x <- structure(
  c(1, 3, 2, 1, 1, 1, 3), 
  levels=c("a", "b", "c"), 
  class="categorical"
)

我们假设这样的对象是一个由小正整数(代码)组成的序列。它配备了 levels 属性,这是一个长度不低于上述整数最大值的字符向量。特别地,第一个级别解码代码 1 的含义。因此,上述向量表示一个序列 a, c, b, a, a, a, c。

对于 categorical 类的对象,没有专门的显示方法。因此,当我们调用 print 时,将调用默认的(后备)方法:

print(x)
[1] 1 3 2 1 1 1 3
attr(,"levels")
[1] "a" "b" "c"
attr(,"class")
[1] "categorical"

这是用于显示数值向量的标准函数。我们对此非常熟悉。它的名称是 print.default,我们始终可以直接调用它。

print.default(x)
[1] 1 3 2 1 1 1 3
attr(,"levels")
[1] "a" "b" "c"
attr(,"class")
[1] "categorical"

自定义一个打印分类数据的函数:

print.categorical <- function(x,...)
{
  x_character <- attr(x, "levels")[unclass(x)]
  print(x_character) # 调用print.default
  cat(sprintf("Categories: %s\n",
              paste(attr(x, "levels"), collapse = ", ")))
  invisible(x)
}

print(x)
[1] "a" "c" "b" "a" "a" "a" "c"
Categories: a, b, c

12.2.2 创建泛型

引入新的 S3 泛型与定义一个调用 UseMethod 的函数一样简单。

  • 定义泛型:首先定义一个函数,该函数内部调用 UseMethod。这就是泛型的定义。

  • 方法调度:当调用该泛型时,UseMethod 会根据传递给它的对象的类来决定调用哪个具体的方法。

  • 隐藏变量:在调度过程中,UseMethod 会创建一些隐藏变量(如 .Generic.Class),以提供更多关于调度的信息。

# 定义一个 S3 泛型
my_print <- function(x) {
  UseMethod("my_print")
}

# 为 'numeric' 类型定义一个方法
my_print.numeric <- function(x) {
  cat("这是一个数字向量:\n")
  print(x)
}

# 为 'character' 类型定义一个方法
my_print.character <- function(x) {
  cat("这是一个字符向量:\n")
  print(x)
}

# 测试
my_print(c(1, 2, 3))         # 调用 my_print.numeric
这是一个数字向量:
[1] 1 2 3
my_print(c("a", "b", "c"))   # 调用 my_print.character
这是一个字符向量:
[1] "a" "b" "c"
# 如果调用my_print函数打印逻辑值函数,则会报错:
# my_print(c(TRUE,FALSE))
# Error in UseMethod("my_print") : 
#   no applicable method for 'my_print' applied to an object of class "logical"

定义泛型:通过定义 my_print 函数并在其内部调用UseMethod(“my_print”),我们创建了一个新的 S3 泛型。

具体方法:定义了两个具体方法 my_print.numeric 和my_print.character,分别处理数字向量和字符向量。

调用示例:当调用 my_print(c(1, 2, 3)) 时,R 会根据 c(1, 2, 3) 的类(numeric)调度到 my_print.numeric 方法;

同理,my_print(c(“a”, “b”, “c”)) 会调度到 my_print.character 方法。 通过这种方式,S3 泛型允许在运行时根据对象的类动态选择方法,使代码更加灵活和可扩展。

as.categorical <- function(x, ...)
{
  UseMethod("as.categorical")
}

定义一个默认方法:

as.categorical.default <- function(x, ...)
{
  if(!is.character(x))
    x <- as.character(x)
  xu <- unique(sort(x)) 
  structure(
    match(x, xu),
    class = "categorical",
    levels = xu
  )
}
x <- c(1,3,8,2,4,9,2,NA_real_)
as.categorical(x)
[1] "1" "3" "8" "2" "4" "9" "2" NA 
Categories: 1, 2, 3, 4, 8, 9
as.categorical(c("a", "c", "a", "a", "d", "c"))
[1] "a" "c" "a" "a" "d" "c"
Categories: a, c, d
as.categorical.list <- function(x, ...)
{
  stop("conversion of lists to categorical is not supported")
}

因此,如果使用列表,应该在用户文档里面标注清楚,传入的是列表时,应该先转换为其他格式:

l1 <- list(c(1,3,4,5),c(9,3,4))
as.categorical(l1)

# Error in as.categorical.list(l1) : 
#   conversion of lists to categorical is not supported
as.categorical.logical <- function(x, ...)
{
  if(!is.logical(x))
    x <- as.logical(x)
  structure(
    x + 1, # 只有1,2和NA会被转换
    class = "categorical",
    levels = c("FALSE", "TRUE")
  )
}

as.categorical(c(TRUE, FALSE, NA))
[1] "TRUE"  "FALSE" NA     
Categories: FALSE, TRUE

12.2.3 内置泛型

S3 generics: print, head, [, [[, [<-, [[<-, length, +, <=, is.numeric, as.numeric, is.character, as.character, as.list, round, log, sum, rep, c, and na.omit

as.character(x)
[1] "1" "3" "8" "2" "4" "9" "2" NA 
x <- structure(
  c(1, 3, 2, 1, 1, 1, 3), 
  levels=c("a", "b", "c"), 
  class="categorical" 
)

as.character.categorical <- function(x, ...)
  attr(x, "levels")[unclass(x)]

as.character(x)
[1] "a" "c" "b" "a" "a" "a" "c"

12.2.4 重载unique.categorical

unique.categorical <- function(x, ...)
  attr(x,"levels")

unique.categorical(x)
[1] "a" "b" "c"

12.2.5 重载rep.categorical

x <- structure(
  c(1, 3, 2, 1, 1, 1, 3), 
  levels=c("a", "b", "c"), 
  class="categorical" 
)


rep.categorical <- function(x,times = 1L, ...)
  rep(attr(x,"level"),times)

rep(x,2)
[1] "a" "b" "c" "a" "b" "c"

在设置新类型的时候要特别注意,比如忘记重载to-numeric转换会导致使用者感到迷惑:

(x <- as.categorical(c(4, 9, 100, 9, 9, 100, 42, 666, 4)))
[1] "4"   "9"   "100" "9"   "9"   "100" "42"  "666" "4"  
Categories: 100, 4, 42, 666, 9
as.double(x)
[1] 2 5 1 5 5 1 3 4 2

为此,要把分类数据转换为浮点数的方法重载:

as.double.categorical <- function(x,...)
{
  # as.double.default(as.character.categorical(x))
  as.double(as.character(x))
}

# 正常使用as.categorical
(x <- as.categorical(c(4, 9, 100, 9, 9, 100, 42, 666, 4)))
[1] "4"   "9"   "100" "9"   "9"   "100" "42"  "666" "4"  
Categories: 100, 4, 42, 666, 9
# 正常使用as.double <===> as.double.categorical
as.double(x)
[1]   4   9 100   9   9 100  42 666   4
unclass(x)
[1] 2 5 1 5 5 1 3 4 2
attr(,"levels")
[1] "100" "4"   "42"  "666" "9"  

在 S3 中,调度通常基于只有一个参数的类:默认情况下是参数列表中的第一个参数。

例如,c 函数是一个泛型,它根据第一个参数的类进行调度。我们可以为 categorical 对象重载它。换句话说,我们将创建一个函数,当泛型在第一个元素属于该类的一系列对象上调用时,由该函数来处理。

c.categorical <- function(...)
{
  as.categorical(
    unlist(
      lapply(list(...), as.character)
    )
  )
}

# x是一个数字向量
x <- c(9,5,7,7,2,3,5)
# xc是一个类型向量
xc <- as.categorical(x)
# 当调用c函数,并且第一个参数是categorical时,调度将转到c.categorical方法
c(xc, x)
 [1] "9" "5" "7" "7" "2" "3" "5" "9" "5" "7" "7" "2" "3" "5"
Categories: 2, 3, 5, 7, 9

在这种情况下,默认的 c 方法会忽略 xc 的类属性,将其视为一个普通的数值向量,也就是说在合并之前,先执行以下代码:

`attributes<-`(xc,NULL)
## [1] 4 2 3 3 1
# 当调用c函数,第一个参数不是categorical时,调度将转到默认方法
c(x,xc)
 [1] 9 5 7 7 2 3 5 5 3 4 4 1 2 3
  • 不是错误:这种行为并不是一个错误,而是一个经过充分文档化的行为。复合类型(如带有类属性的对象)是通过基本类型模拟的。

  • 类调度:当第一个参数是 categorical 类时,c.categorical 方法会被调用;如果不是,则会使用默认的 c 方法。

  • 理解 S3 机制:这种设计展示了 S3 类系统的灵活性和动态性,尽管可能会在某些情况下引入复杂性。用户需要理解如何在不同类型之间进行转换,以避免混淆。

在大多数情况下,可以直接调用 S3 方法以获得所需的结果:

c.categorical(x, xc) # 强制调用特定方法

[1] "9" "5" "7" "7" "2" "9" "5" "7" "7" "2" 
Categories: 2, 5, 7, 9

我们之所以说“在大多数情况下”,是因为方法可能会有以下几种情况:

在 C 语言级别硬编码:例如,根本没有定义 c.default 方法,这意味着某些基本操作可能不支持直接调用。

隐藏:某些方法在包的命名空间中定义但未导出。这意味着它们无法通过常规方式访问,用户需要特别注意这些方法的可用性。

作为组重载:某些方法可能被作为一组重载。在这种情况下,需要参考相关文档(如 help("groupGeneric") )来了解如何使用这些方法。

res <- kmeans(iris[-5], centers=3, nstart=10)
# res是一个列表类型的数据,但是一个k-means类,打印的时候会调用stats:::print.kmeans进行打印,但这个print.kmeans是在stats包下的,是隐藏的。
print(res)
K-means clustering with 3 clusters of sizes 38, 50, 62

Cluster means:
  Sepal.Length Sepal.Width Petal.Length Petal.Width
1     6.850000    3.073684     5.742105    2.071053
2     5.006000    3.428000     1.462000    0.246000
3     5.901613    2.748387     4.393548    1.433871

Clustering vector:
  [1] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 [38] 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 1 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
 [75] 3 3 3 1 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 3 1 1 1 1 3 1 1 1 1
[112] 1 1 3 3 1 1 1 1 3 1 3 1 3 1 1 3 3 1 1 1 1 1 3 1 1 1 1 3 1 1 1 3 1 1 1 3 1
[149] 1 3

Within cluster sum of squares by cluster:
[1] 23.87947 15.15100 39.82097
 (between_SS / total_SS =  88.4 %)

Available components:

[1] "cluster"      "centers"      "totss"        "withinss"     "tot.withinss"
[6] "betweenss"    "size"         "iter"         "ifault"      
class(res)
[1] "kmeans"
typeof(res)
[1] "list"
unclass(res)
$cluster
  [1] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 [38] 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 1 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
 [75] 3 3 3 1 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 3 1 1 1 1 3 1 1 1 1
[112] 1 1 3 3 1 1 1 1 3 1 3 1 3 1 1 3 3 1 1 1 1 1 3 1 1 1 1 3 1 1 1 3 1 1 1 3 1
[149] 1 3

$centers
  Sepal.Length Sepal.Width Petal.Length Petal.Width
1     6.850000    3.073684     5.742105    2.071053
2     5.006000    3.428000     1.462000    0.246000
3     5.901613    2.748387     4.393548    1.433871

$totss
[1] 681.3706

$withinss
[1] 23.87947 15.15100 39.82097

$tot.withinss
[1] 78.85144

$betweenss
[1] 602.5192

$size
[1] 38 50 62

$iter
[1] 2

$ifault
[1] 0
print.kmeans
# Error: object 'print.kmeans' not found
# 使用getS3method方法得到隐藏重载函数。
getS3method("print", "kmeans")
function (x, ...) 
{
    cat("K-means clustering with ", length(x$size), " clusters of sizes ", 
        paste(x$size, collapse = ", "), "\n", sep = "")
    cat("\nCluster means:\n")
    print(x$centers, ...)
    cat("\nClustering vector:\n")
    print(x$cluster, ...)
    cat("\nWithin cluster sum of squares by cluster:\n")
    print(x$withinss, ...)
    ratio <- sprintf(" (between_SS / total_SS = %5.1f %%)\n", 
        100 * x$betweenss/x$totss)
    cat(sub(".", getOption("OutDec"), ratio, fixed = TRUE), "Available components:\n", 
        sep = "\n")
    print(names(x))
    if (!is.null(x$ifault) && x$ifault == 2L) 
        cat("Warning: did *not* converge in specified number of iterations\n")
    invisible(x)
}
<bytecode: 0x12db3eb18>
<environment: namespace:stats>

12.2.6 多个类嵌套

12.2.6.1 日期时间类的讨论

# t1为数值向量
(t1 <- Sys.time())
[1] "2024-10-13 18:13:37 CST"
# t2为列表
(t2 <- strptime("2024-08-15T12:59:59+1000", "%Y-%m-%dT%H:%M:%S%z"))
[1] "2024-08-15 10:59:59"
# "POSIXct" "POSIXt"
class(t1)
[1] "POSIXct" "POSIXt" 
# "POSIXlt" "POSIXt" 
class(t2)
[1] "POSIXlt" "POSIXt" 

前者作为数值向量表示,而后者则作为列表。因此,这两者应主要视为两种不同类型的实例。然而,由于它们有许多共同之处,因此将它们视为同一通用类别的 POSIX 时间对象的代表是一个明智的设计选择。

12.2.6.2 调用泛型函数的调度机制

重要提示:当在一个类为 class1class2、…、classK 的对象 x 上调用泛型函数 f 时,UseMethod(f, x) 的调度顺序如下:

  1. 如果 f.class1 可用,则调用该方法;
  2. 否则,如果 f.class2 可用,则调用该方法;
  3. ……
  4. 否则,如果 f.classK 可用,则调用该方法;
  5. 否则,参考后备方法 f.default

这种调度机制使得 R 的 S3 系统能够根据对象的类动态选择合适的方法,从而实现灵活的对象处理。了解这一机制对于有效使用和扩展 R 的功能至关重要。

示例 10.12:POSIXt 类的 diff 方法

对于 POSIXt 类的对象,有一个 diff 方法,其中包含如下语句:

r <- if (inherits(x, "POSIXlt")) as.POSIXct(x) else x

通过这种方式,我们可以使用相同的过程处理 POSIXctPOSIXlt 实例。

机制解释: s我们不应将这一简单方案视为魔法。这只是一种确定特定 R 对象调用哪个方法的方式。它可以作为一种机制,模仿面向对象编程语言中继承的概念。然而,S3 系统并不允许以任何正式方式定义类。我们不能说 POSIXct 类的对象继承自 POSIXt。我们也不能说每个 POSIXct 类的对象也是 POSIXt 的实例。类属性仍然可以在每个对象的基础上任意设置。我们可以创建仅为 POSIXct 的对象(不包含 POSIXt 部分),或者甚至是 c("POSIXt", "POSIXct")(按此顺序)。这种灵活性使得 S3 系统能够支持多态性,但同时也带来了对类结构和继承关系的限制。理解这些限制对于有效地使用 R 的面向对象特性至关重要。

`==.categorical` <- function(e1, e2)
  as.character(e1) == as.character(e2)

c1 <- structure(
  c(1,3,2,1,2,3),
  levels = c("a","b","c"),
  class = "categorical"
)

c2 <- structure(
  c(1,3,2,1,3,3),
  levels = c("a","b","c"),
  class = "categorical"
)

`==.categorical`(c1,c2)
[1]  TRUE  TRUE  TRUE  TRUE FALSE  TRUE
as.categorical(c(1,3,5,1)) == as.categorical(c(1,3,1,1))
[1]  TRUE  TRUE FALSE  TRUE

这段代码定义了一个自定义的比较操作符 ==,专门用于处理 categorical 类的对象。下面是对这段代码的详细解释:

`==.categorical` <- function(e1, e2) {
  as.character(e1) == as.character(e2)
}
  1. 定义操作符

    • 这段代码使用反引号(`)来定义一个新的二元操作符 ==.categorical。在 R 中,您可以通过这种方式定义自定义的操作符,覆盖默认行为。
  2. 函数参数

    • e1e2 是输入的两个对象,它们是 categorical 类的实例。
  3. 类型转换

    • as.character(e1)as.character(e2)e1e2 转换为字符型。这是为了确保在比较时使用的是它们的字符表示,而不是原始的 categorical 类型。
  4. 比较

    • == 运算符用于比较两个字符向量的元素。如果它们相等,则返回 TRUE,否则返回 FALSE

    使用方法:定义了这个操作符后,您可以直接使用 == 来比较两个 categorical 对象。例如:

# 假设您已经定义了 `categorical` 类和相应的 `as.categorical` 函数
# 创建两个 `categorical` 对象
cat1 <- as.categorical(c("A", "B", "C"))
cat2 <- as.categorical(c("A", "B", "D"))

# 使用 `==` 操作符进行比较
result <- cat1 == cat2

# 输出结果
print(result)  # 应该输出 TRUE FALSE FALSE
  • 在这个例子中:
    • cat1cat2 是两个 categorical 对象。
    • cat1 == cat2 会调用您定义的 ==.categorical 函数,比较它们转换为字符后的内容。
    • 输出结果会是一个逻辑向量,指示每对元素是否相等。
c(1,3,5,1) == as.categorical(c(1,3,1,1))
[1]  TRUE  TRUE FALSE  TRUE

12.2.7 操作符重载

`==.A` <- function(e1, e2)"A"
`==.B` <- function(e1, e2)"B"

structure(c(1,2,3), class = "A") == structure(c(2,NA,3), class = "B")
Warning: Incompatible methods ("==.A", "==.B") for "=="
[1] FALSE    NA  TRUE

12.3 重新认识S3

在R语言中,基于S3对象的面向对象编程,是一种基于泛型函数的实现方式。泛型函数是一种特殊的函数,根据传入对象的类型决定调用那个具体的方法。基于S3对象实现面向对象编程,不同其他语言的面型对象编程,是一种动态函数调用的模拟实现。S3对象被广泛应用于R的早期的开发包中。

12.3.1 创建S3对象

注意:本文会用到pryr,为了方便我们检查对象的类型,引入pryr包作为辅助工具。

12.3.1.1 通过变量创建S3对象

#install.packages("pryr")

# 通过变量创建S3对象
x <- 1
attr(x, "class") <- "foo"
x
[1] 1
attr(,"class")
[1] "foo"
attr(x,"class")
[1] "foo"
class(x)
[1] "foo"
#用pryr包的otype函数,检查x的类型
library(pryr)
pryr::otype(x)
[1] "S3"

12.3.1.2 通过structure()函数创建S3对象

y <- structure(
  c(1,2,4),
  class = "foo"
)
y
[1] 1 2 4
attr(,"class")
[1] "foo"
attr(y, "class")
[1] "foo"
class(y)
[1] "foo"
pryr::otype(y)
[1] "S3"

12.3.1.3 创建多类型的S3对象

创建一个多类型的S3对象,S3独享没有明确结构关系,一个S3对象可以有多个类型,S3对象的class属性可以是一个响亮,包括多种类型。

x <- 3
attr(x, "class") <- c("foo", "bar", "xyz")
class(x)
[1] "foo" "bar" "xyz"
attr(x,"class")
[1] "foo" "bar" "xyz"
pryr::otype(x)
[1] "S3"

12.4 泛型函数和方法调用

对于S3对象的使用,通常用UseMethod()函数来定义一个泛型函数的名称,通过传入参数的class属性,来确定方法调用。

定义一个teacher的泛型函数

  • 用UseMethod()定义teacher泛型函数

  • 用teacher.xxx的语法格式定义teacher对象的行为

  • 其中teacher.default是默认行为

# 用UseMethod()定义teacher泛型函数
teacher <- function(x, ...)
  UseMethod("teacher")

# 用pryr包中的ftype()函数,检查teacher类型
pryr::ftype(teacher)
[1] "s3"      "generic"
# 定义teacher内部函数
teacher.lecture <- function(x, ...) print("讲课")
teacher.assignment <- function(x, ...) print("布置作业")
teacher.correcting <- function(x, ...) print("批改作业")
teacher.default <- function(x, ...) print("你不是teacher")

方法调用通过传入参数的class属性,来确定不同方法调用

  • 定义一个变量a,并设置a的class属性为lecture

  • 把变量a传入到teacher泛型函数中

  • 函数teacher.lecture()函数的行为被调用

a <- "teacher"
# 给老师变量设置一个class
attr(a, "class") <- "lecture"

# 执行老师的行为
teacher(a)
[1] "讲课"

当然我们可以直接调用teacher中定义的行为,如果这样做就失去了面向对象封装的意义。

teacher.assignment()
[1] "布置作业"
teacher.correcting("hello")
[1] "批改作业"

12.5 查看S3对象的函数

当我们使用S3队形进行面向对象封装后,可以使用methods()函数来查看S3对象中的定义的内部行为函数。

# 查看teacher对象
teacher
function(x, ...)
  UseMethod("teacher")

12.5.1 methods函数

# 查看teacher对象的内部函数
methods(teacher)
[1] teacher.assignment teacher.correcting teacher.default    teacher.lecture   
see '?methods' for accessing help and source code
methods(print)[1:5]
[1] "print.acf"               "print.activeConcordance"
[3] "print.AES"               "print.anova"            
[5] "print.aov"              
#通过methods()的generic.function参数,来匹配泛型函数名字
methods(generic.function = teacher)
[1] teacher.assignment teacher.correcting teacher.default    teacher.lecture   
see '?methods' for accessing help and source code

通过methods()的class参数,来匹配类的名字

methods(class = lm)[1:3]
[1] "add1.lm"  "alias.lm" "anova.lm"

12.5.2 getAnywhere函数

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("讲课")
getAnywhere(print)
A single object matching 'print' was found
It was found in the following places
  package:base
  namespace:base
with value

function (x, ...) 
UseMethod("print")
<bytecode: 0x10eb8ee38>
<environment: namespace:base>

12.5.3 getS3method函数

getS3method("teacher", "lecture")
function(x, ...) print("讲课")

12.6 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属性为两个,NextMethod()函数是根据class的类型逐个查找。
n2 <- structure(2, class = c("son", "father"))

# 在node函数中传入n2,执行node.son()函数和node.father()函数
node(n2)
[1] "son"    "father"

通过对node()函数传入n2的参数,node.son()先被执行,然后通过NextMethod()函数继续执行了node.father()函数。这样其实就模拟了,子函数调用父函数的过程,实现了面向对象编程中的继承。

12.7 S3对象的缺点

从上面S3对象的介绍上来看,S3对象并不是完全的面向对象实现,而是一种通过泛型函数模拟的面向对象的实现。

  • S3用起来简单,但在实际的面向对象编程的过程中,当对象关系有一定的复杂度,S3对象所表达的意义就变得不太清楚

  • S3封装的内部函数,可以绕过泛型函数的检查,以直接被调用

  • S3参数的class属性,可以被任意设置,没有预处理的检查

  • S3参数,只能通过调用class属性进行函数调用,其他属性则不会被class()函数执行

  • S3参数的class属性有多个值时,调用时会被按照程序赋值顺序来调用第一个合法的函数

所以,S3只是R语言面向对象的一种简单的实现。

12.8 S3对象的使用

S3对象系统,被广泛的应用于R语言早期的开发中。在base包中,就有很多S3对象

base包的S3对象

mean
function (x, ...) 
UseMethod("mean")
<bytecode: 0x107882f28>
<environment: namespace:base>
pryr::ftype(mean)
[1] "s3"      "generic"
pryr::ftype(t)
[1] "s3"      "generic"
# 定义泛型函数
f1 <- function(x){
  UseMethod("f1")
}

# 定义f1的内部函数
f1.numeric <- function(x) a

a <- 2

# 给f1()传入变量a
f1(a)
[1] 2
f1("hello")

Error in UseMethod("f1") : 
  no applicable method for 'f1' applied to an object of class "character"
  
传入的是一个字符串,f1函数没有定义字符串函数,因此会报错。
f1.character <- function(x) paste("char:", x)

# 定义character后,就不会报错了
f1("Hello")
[1] "char: Hello"