对象的引用代码演示(七爪源码S3S4)

R、S3 和 S4 对象中的面向对象简介。

对象的引用代码演示(七爪源码S3S4)(1)

什么是面向对象编程?

面向对象(OO)是编程中的三个主要范例之一。它彻底改变了编程,以及结构化编程和函数式编程。 OO 非常流行,尤其是在 Python、Java、C# 等语言中。但是什么是面向对象编程? OO 通常由封装、多态和继承等特性定义。我更喜欢使用 R.C. 的定义。马丁:

OO 语言可以绝对控制所有源代码的方向。

换句话说,OO 可以以非常精确的方式控制依赖关系。

我们如何在 R 中利用这种力量?

R中的OOP

R 是一种设计的函数式语言。您分解问题并使用函数而不是对象来解决它。然而,R 支持 OO。事实上,在 R 中,您一直在使用对象和方法,即使您没有意识到这一点。 data.frame、list 和 vector 等实体都是类。他们都有自己的方法,例如打印或摘要。这些方法的行为取决于您使用它们的类。

R 支持两种类型的 OO 编程:

  • 函数式OO编程。在函数式 OO 编程中,方法属于泛型函数。函数式 OO 编程类似于标准函数式编程,至少从外部来看是这样。 R 中的功能对象是 S3 和 S4 对象。

# Note: this example is a mock up: it will not work if you try to run it. It only # serves as a guide to showcase the components of functional OOP # create an object of class "myObject" myObject <- object(list(a=5, b="hello"), class="myObject") # define a generic to print it. This generic takes # our object and dispathc the print call to the appropriate method # based on the class of myObject print <- funciton(obj){ useMethod(obj) } # now we define the exact method to print our object. The method is not # encapsulated into the function definition, it belongs to the generic # function print(). This is funcitonal OOP print.myObject <- function(obj){ cat(rep(obj$b, obj$a)) } # use the print method print(myObject)

  • 封装的OO编程。 在封装的 OO 编程中,方法属于类:它们是封装的。 如果你熟悉 Python 或 Java OO 编程,这也是同样的原理。 R 中封装的对象是 R6 和 RC 对象。

# Note: this example is a mock up: it will not work if you try to run it. It only # serves as a guide to showcase the components of encapsulated OOP # create an object. Note how the print method is defined inside the class: it is encapsulated # in the class myObject <- newObject(fields=list(a=5, b="hello"), methods=list(print = function(self){ cat(rep(self$a, self$b))})) # use the print method myObject$print()

在这个迷你系列中,我们将首先了解 R 的 OO 实现,以及不同解决方案的优缺点。

这个例子

在本系列中,我们将为我们将要探索的所有对象使用一个通用示例。 我们将定义一个具有一些属性和方法的类 Animals。 然后,我们将定义 Animals 的子类:Dog。 我们将使用 Dog 来显示继承。

一些属性和操作应该是公共的 ( ),而其他的应该是私有的 (-)。

对象的引用代码演示(七爪源码S3S4)(2)

与往常一样,我想为您提供基于正确决策设计解决方案所需的知识。 我的目标不是为您提供 OO 实现的每一个可能的细节。 如果你想了解所有细节,Hadley Wickham 有一本很棒的书:Advanced R。

S3

S3 是 R 中最简单和最常见的 OO 编程形式。它的实现相当轻松,尤其是与传统的 OO 语言相比。 然而,它的用途非常广泛,而且它的简化方法也有其优点。

为了理解我的意思,让我们开始研究我们的示例类:动物。

# Define the class "Animals" animals <- list(species = "Canis familiaris") class(animals) <- "Animals" # default print animals # $species # [1] "Canis familiaris" # attr(,"class") # [1] "Animals"

这是非常非正式的。 我们创建了一个列表并将其类属性从列表更改为动物。

一般来说,每当我们创建一个 S3 对象时,我们都希望创建三个函数:

  • 内部构造函数。 这将创建列表并将类更改为 Animals。 这不会暴露给用户。 这是一个开发者功能。 构造函数名称应以 new_ 开头。
  • 一个验证器,用于验证用户为创建类而提供的输入。
  • 导出给用户的用户友好的类构建器。 用户将调用它来构建对象。 构建器将调用验证器和内部构造器。

这就是我们如何为我们的类编写三个函数。

# define the internal constructor new_Animals <- function(obj){ # obj must be a string animals <- list(species = obj) class(animals) <- "Animals" return(animals) } # define a validator validate_Animals <- function(animals){ # obj is the character string that the user will pass to create an object. # It must of length 1 and not NA. Note that we already validate that the if(class(animals) != "Animals"){ stop("The object is not of class 'Animals'") } if (is.null(attr(animals, "names"))){ stop('Missing class attribute "names"') } if (attr(animals, "names") != "species"){ stop('Missing class attribute "species"') } if (!is.character(animals$species)){ stop("species must be a character") } if (is.na(animals$species)){ stop("species cannot be NA") } if (!stringr::str_detect(animals$species, '[a-z, A-Z] [a-z,A-Z]')){ stop("species must contain genus and species separated by a single space.") } return(TRUE) } # user exposed constructor. In roxygen, we use the @export tag # for this function Animals <- function(species = "Undefined Animal"){ animals <- new_Animals(species) validate_Animals(animals) return(animals) } # use the constructor res <- Animals('hello') #Error in validate_Animals(animals) : # species must contain genus and species separated by a single space. res <- Animals('Canis Familiaris') # no print, succesfull object creation

让我们打开包装。

new_Animals 是内部构造函数。这不会暴露给用户,但它是一种在内部构建对象的快速方法,即从包中构建对象。

validate_Animals 是对象验证器。它检查我们是否有一个正确类的对象和正确的属性。请注意,您可以在内部构造函数中包含这些简单的检查。根据经验,在验证器中包含昂贵的检查,在内部构造函数中包含便宜的检查。通过这种方式,您可以仅在需要时运行昂贵的检查。但是,我更喜欢更严格的职责划分。我将所有检查都放在验证器中,而不是构造器中。

Animals 是暴露给用户的构建器。此函数构建对象,然后对其进行验证。

泛型和方法

我们现在需要创建两个方法:print 和 formatSpecies。 print 将打印我们对象的用户友好摘要,而不是我们默认获得的摘要。 formatSpecies 将格式化物种字符串。

在 S3 中,我们不能创建私有方法。我们所有的方法都是公开的。

让我们从打印方法开始。 R 中已经存在通用打印。您可以在任何变量上调用它以查看其值打印到控制台。事实上,print 是 S3 的泛型。我们可以使用它并为类 Animals 创建一个新方法。

# s3 print method for the class Animals print.Animals <- function(obj){ cat(paste0("Object of class 'Animals'. The species is ", obj$species)) } # create a new object newAnimal <- Animals('Canis Familiaris') print(newAnimal) # Object of class 'Animals'. The species is Canis Familiaris

请注意 print.Animal 方法的约定。 将方法称为 generic.class 非常重要。 这是使调度机制工作所需的内部 R 约定。

现在让我们创建 formatSpecies。 这个方法没有泛型,所以我们需要创建它。

# the generic formatSpecies <- function(x) { UseMethod("formatSpecies") } # the method for the class Animals formatSpecies.Animals <- function(obj){ obj$species <- stringr::str_to_sentence(obj$species) return(obj) } # create a new object. Note the lower case in the species newAnimal <- Animals('canis familiaris') # call the generic formattedAnimals <- formatSpecies(newAnimal) # verify the results. Note the capitalization print(formattedAnimals) # Object of class 'Animals'. The species is Canis familiaris

通用的 formatSpecies 会将适当的方法与对象匹配。 然后它将调用该方法:formatSpecies.Animals。

遗产

S3 中的继承围绕着类可以是长度大于 1 的向量这一概念展开。也就是说,一个对象可以有两个或多个类。 您可以在 tibble 包中找到此行为的示例。

# create a tibble mtCarsTibble <- tibble::as_tibble(mtcars) class(mtCarsTibble) # [1] "tbl_df" "tbl" "data.frame"

一个 tibble 对象具有三个类:tbl_df、tbl 和 data.frame。 这反映了继承的顺序:tbl_df 是 tbl 的子类,而后者又是 data.frame 的子类。

当我们在像这样的对象上使用泛型时,R 将尝试以指定的顺序分派给一个方法。 如果没有找到 tbl_df 的方法,那么 R 将寻找类 tbl 的方法,最后寻找类 data.frame。

让我们看看如何在我们的示例中使用它。 在创建子类之前,我们需要更改超类。 这是因为在 S3 中继承不是自动的:我们需要将它添加到超类中。

# Animals class accepting subclasses new_Animals <- function(obj, ..., class = character()){ animals <- list(species = obj) class(animals) <- c(class, "Animals") return(animals) }

我们做了三处改动:

  • 我们添加了参数...。这允许我们将构造函数所需的任何额外参数传递给超类。
  • 我们添加了参数 class=character()。 这允许我们指定子类并创建我们在 tibble 示例中看到的类向量。
  • 我们将类定义为 c(class, "Animals"),而不仅仅是 "Animals"。 再一次,这允许我们创建长度大于 1 的类向量。

现在超类可以接受子类,让我们创建 Dog 子类。

# create the Dog subclass constructor new_dog <- function(x, age) { newObj <- new_Animals("Canis familiaris", class = "Dog") newObj$name <- x newObj$age <- age return(newObj) } # create an object myDog <- new_dog('Pluto') # check the class class(myDog) [1] "Dog" "Animals" # print the object print(myDog) #> Object of class 'Animals'. The species is Canis familiaris

在第 3 行中,我们调用了 Animals 超类的构造函数,但我们指定要使用类 Dog。 在第 14 行,我们看到 myDog 的类是如何成为具有两个元素的向量:Dog(子类)和 Animals(超类)。 如果我们尝试使用 print 方法,R 将使用为类 Animals 定义的 print 方法。 这是因为我们还没有为 Dog 定义打印方法。 让我们这样做。

# print method for class Dog print.Dog <- function(x) { cat(paste0("The dog ", x$name, " is ", x$age)) } print(myDog) #> The dog Pluto is 4

现在我们有了 Dog 的 print 方法,R 将使用它而不是 Animal 的方法。 我们不需要定义通用打印,因为它已经在基础 R 中定义。我们只为子类创建方法。

最后,我们可以使用相同的语法来创建新方法humanAge。 这种方法将以“人类”年为单位计算狗的年龄。

# the generic humanAge. We could use it to plug in # more animals, rather than just dogs humanAge <- function(x) { UseMethod("humanAge") } # the method for the class Dog humanAge.Dog <- function(obj){ return(obj$age * 7) } humanAge(myDog) #> 28

这里没有什么我们没有看到的:我们定义了泛型(humanAge),然后定义了与泛型和类相关的方法(Dog)。

S3 不可变

考虑这种情况。 这是你的狗的生日。 您想要一种可以更新您的狗的年龄的新方法。 它应该返回以前的年龄并更新内部年龄。 S3 对象无法做到这一点。 S3 对象是不可变的。 它们不能修改输入参数,因为它们是基于函数的。

如果我们想更新一个对象并从同一个调用中返回一个结果,我们必须使用一种解决方法。 我们需要创建一个函数:

  • 修改输入对象以更新年龄。
  • 在列表中返回修改后的对象。
  • 将上一个年龄附加到列表中。
  • 在主环境中,使用赋值语句解压缩列表。

# define the generic updateAge <- function(x) { UseMethod("updateAge") } # define the method updateAge.Dog <- function(obj){ oldAge <- obj$age obj$age <- obj$age 1 # we need to wrap the updated object and the desired outcome # into a list return(list(obj = obj, oldAge = oldAge)) } # call the generic res <- updateAge(myDog) # assignments oldAge <- res$oldAge myDog <- res$obj print(oldAge) #> 4 print(myDog) #> The dog Pluto is 5

updateAge.Dog 方法返回一个包含先前年龄和修改对象的列表。

与对象可变的传统封装 OO 相比,这很尴尬,也没有那么优雅。当我们探索 R6 对象时,我们将看到不同之处。

S3 总结

S3 是简单的对象。他们的定义是非正式的和放松的。您甚至可以在创建它们后更改它们的结构。没有正式的验证器。 S3 没有“私有”的概念,一切都是“公有的”。此外,S3 是不可变的。他们面向功能的行为可能会让非 R 人群感到反感。从本质上讲,S3 对象不仅仅是美化的列表。

但不要过快地解雇他们。 S3 的简单性中包含美感和目的。例如,很容易将新类的新方法插入到现有泛型中。

如果您需要封装的可变方法或可靠的继承机制,S3 不是最佳选择。此外,在相同代码库上开发的大型团队可能会因为界面缺乏结构而陷入困境。

然而,在大多数情况下,S3 应该是第一个考虑的选项。毕竟,如果它们是最常见的类型或 R 对象,肯定是有原因的。简化代码以使其与 S3 一起工作有很多好处。

S4 对象

S4 是 S3 的更严格的实现。 S4 仍然是功能性 OO,但类是正式定义的。创建对象后,您将无法再更改其结构。 S4 支持更复杂的继承和分派机制。

让我们看看如何在 S4 中创建 Animals 类。

# define the class setClass("Animals", representation(species = "character") ) # create an object newAnimal <- new("Animals", species="Canis familaris") # defuat print print(newAnimal) # access slots, method 1 newAnimal@species # > "Canis familaris" # access slots, method 2 slot(newAnimal, 'species') # > "Canis familaris"

我们可以看到与 S3 的一些不同之处。 我们需要使用表示来定义类内容(插槽)。 如果我们在创建对象后尝试向对象添加另一个插槽,或者将字符以外的其他内容分配给物种,我们将收到错误消息。 我们还注意到,要访问一个槽,我们使用 @ 而不是 $,或者我们可以使用 S4 函数 slot()。

我们现在有了基本对象,让我们添加一个验证器。

# without validations, this will work newAnimal <- new("Animals", species=NA_character_) # define a validator validate_Animals <- function(object){ # species cannot be NA if (is.na(slot(object, 'species'))){ stop("species cannot be NA") } # there must be a white space between species and genus if (!stringr::str_detect(slot(animals, 'species'), '[a-z, A-Z] [a-z,A-Z]')){ stop("species must contain genus and species separated by a single space.") } return(TRUE) } # add the validator to the class setValidity('Animals', validate_Animals) # now this will fail newAnimal <- new("Animals", species = NA_character_) # > Error in validityMethod(object) : species cannot be NA

我们的验证器 validate_Animals 基于我们用于 S3 的验证器,但它更轻量级。我们不是在测试物种是一个字符还是为 NULL。这是因为在我们将物种定义为构造函数中的字符之后,这些检查是免费的。我们只需要检查物种是否符合我们想要的形式。

在我们定义了验证器函数 validate_Animals 之后,我们使用 setValidity 将它添加到构造函数中。通过这样做,每次创建对象时都会自动调用验证函数。

泛型和方法

如果我们不能对它做任何事情,我们的新 S4 对象就不是特别有用。让我们从创建一个显示其内容的方法开始。在 S4 中,您通常不使用 print,而是使用 show。如果您创建一个通用的 print 将覆盖基本 R print ,然后它将停止工作。

show 是 S4 内置的泛型,所以我们不需要定义一个。我们只需要为我们的类定义方法。

# create an object newAnimal <- new("Animals", species="Canis familaris") # define the method setMethod("show", signature("Animals"), function(object){ cat(paste0("Object of class 'Animals'. The species is ", slot(object, 'species'))) }) show(newAnimal) # > Object of class 'Animals'. The species is Canis familaris

如果您还记得我们的 S3 示例,这看起来很熟悉。我们首先用 setGeneric 定义一个泛型,然后用 setMethod 定义一个方法。

使用相同的语法,让我们实现 formatSpecies 方法。

# define the generic setGeneric("formatSpecies", function(object) { standardGeneric("formatSpecies") }) # define the method setMethod("formatSpecies", signature("Animals"), function(object){ slot(object, 'species') <- stringr::str_to_sentence( slot(object, 'species')) return(object) }) # usage newAnimal <- new("Animals", species="canis familaris") show(newAnimal) # > Object of class 'Animals'. The species is canis familaris newAnimal <- formatSpecies(newAnimal) show(newAnimal) # > Object of class 'Animals'. The species is Canis familaris

至于S3,S4没有私有方法的概念,所以两个方法都是公有的。

遗产

让我们创建一个继承自 Animals 的 S4 类 Dog。

setClass('Dog', contains = 'Animals',representation(name='character', age = 'numeric')) newDog <- new('Dog', species="Canis familaris", name = 'Pluto', age = 4) newDog #> An object of class "Dog" #> Slot "name": #> [1] "Pluto" #> Slot "age": #> [1] 4 #> Slot "species": #> [1] "Canis familaris" show(newDog) #> Object of class 'Animals'. The species is Canis familaris

使用参数 contains 使继承变得明确,在 Dog 的定义中使用。 你会注意到 Dog 继承了 Animals 的 show 方法。 我们可以创建一个专用的显示方法。 在此过程中,我们还可以创建 humanAge 方法。

# Define the methos for print setMethod("show", signature("Dog"), function(object){ cat(paste0("The dog ", slot(object, 'name'), " is ", slot(object, 'age'))) }) show(newDog) #> The dog Pluto is 4 # define the generic setGeneric("humanAge", function(object) { standardGeneric("humanAge") }) # define the method setMethod("humanAge", signature("Dog"), function(object){ return(slot(object, 'age') * 7) }) humanAge(newDog) #> 28

S4 不可变

至于 S3 对象,S4 是不可变的。 我们不能同时修改一个对象并返回一个结果。 我们需要实现与 S3 对象相同的解决方法。

# define the generic setGeneric("updateAge", function(object) { standardGeneric("updateAge") }) # define the method setMethod("updateAge", signature("Dog"), function(object){ oldAge <- slot(object, 'age') slot(object, 'age') <- slot(object, 'age') 1 return(list(object = object, oldAge = oldAge)) }) #usage show(newDog) #> The dog Pluto is 4 res <- updateAge(newDog) updatedDog <- res$object show(updatedDog) #> The dog Pluto is 5 res$oldAge #> 4

在上面的代码片段中,我们创建了一个名为 updateAge 的方法。在该方法中,我们更新了 slot age,并且我们还返回了 old age。为了能够同时执行这两个操作,我们必须从方法中返回一个列表。该列表将包含修改后的对象和所需的返回。最后,在主环境中,我们需要用更多的任务解压列表。

S4 结束

S4 基于与 S3 相同的思想。它们都是功能性 OO 系统,并且都是不可变的。 S4 有更严格的定义。我们需要在类中准确指定我们想要的内容,并且一旦创建对象的结构就无法修改。这可以帮助更大的团队,因为界面更清晰。

S4 添加的形式可能听起来不错,但您应该仔细评估是否值得。通常最好使用更简单且对 R 更友好的 S3。这是因为传统 R 开发人员提供了更好的文档和改进的代码可读性。

关注我并订阅以在本系列的第 2 部分发布时收到通知,以及有关 R 编程的其他提示。

关注七爪网,获取更多APP/小程序/网站源码资源!

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页