此答案摘自我最初在 2016 年作为社区导师在约翰霍普金斯大学 R 编程 课程中撰写的一篇文章:Demystifying makeVector()。
makeVector()和cachemean()的整体设计
cachemean.R 文件包含两个函数,makeVector() 和 cachemean()。文件中的第一个函数makeVector() 创建了一个存储向量及其均值的 R 对象。第二个函数cachemean() 需要makeVector() 返回的参数,以便从makeVector() 对象环境中存储的缓存值中检索平均值。
makeVector() 中发生了什么?
在makeVector() 中要理解的关键概念是它构建了一组函数并将列表中的函数返回给父环境。也就是说,
myVector <- makeVector(1:15)
产生一个对象 myVector,它包含四个函数:set()、get()、setmean() 和 getmean()。它还包括两个数据对象,x 和 m。
由于词法作用域,myVector 包含makeVector() 环境的完整副本,包括在设计时(即编码时)在makeVector() 中定义的任何对象。环境层次结构图清楚地表明了myVector 中可以访问的内容。
图示为层次结构,全局环境包含makeVector() 环境。所有其他内容都存在于makeVector() 环境中,如下图所示。
由于每个函数在 R 中都有自己的环境,因此层次结构说明对象 x 和 m 是四个函数get()、set()、getmean() 和 setmean() 的兄弟。
一旦函数运行并实例化(即创建)makeVector() 类型的对象,包含 myVector 的环境如下所示:
请注意对象x 包含向量1:15,即使myVector$set() 尚未执行。这是因为值1:15 作为参数传递给makeVector() 函数。什么解释了这种行为?
当一个 R 函数将一个包含函数的对象返回到它的父环境时(就像 myVector <- makeVector(1:15) 这样的调用的情况),myVector 不仅可以访问其列表中的特定函数,而且它还保留对makeVector() 定义的整个环境的访问权限,包括用于启动函数的原始参数。
为什么会这样? myVector 包含指向函数结束后位于makeVector() 环境中的函数的指针,因此这些指针可以防止makeVector() 消耗的内存被垃圾收集器释放。因此,整个makeVector() 环境保留在内存中,myVector 可以访问其函数以及该环境中在其函数中引用的任何数据。
此功能解释了为什么 x(在原始函数调用中初始化的参数)可以通过随后对 myVector 上的函数(例如 myVector$get())的调用访问,它还解释了为什么代码无需显式发出即可工作myVector$set() 设置x 的值。
makeVector() 一步一步
现在,让我们逐步分解函数的行为。
第一步:初始化对象
函数中发生的第一件事是初始化两个对象x和m。
makeVector(x = numeric()) {
m <- NULL
...
}
注意x 被初始化为函数参数,因此函数内不需要进一步初始化。 m 设置为 NULL,将其初始化为 makeVector() 环境中的对象,以供函数中的后续代码使用。
此外,函数声明的形式部分将x 的默认值定义为空数值向量。使用默认值初始化向量很重要,因为没有默认值,data <- x$get() 会生成以下错误消息。
Error in x$get() : argument "x" is missing, with no default
第 2 步:为 makeVector() 类型的对象定义“行为”或函数
初始化在makeVector() 中存储关键信息的关键对象后,代码提供了object-oriented program 中数据元素的四种典型行为。它们被称为“getter 和 settters”,更正式地称为mutator and accessor 方法。正如人们所预料的那样,“getter”是检索(访问)对象内数据的程序模块,而“setter”是设置(改变)对象内数据值的程序模块。
首先makeVector() 定义set() 函数。 makeVector() 中的大部分“魔法”发生在 set() 函数中。
set <- function(y) {
x <<- y
m <<- NULL
}
set() 采用名为y 的参数。假设这个值是一个数值向量,但没有在函数形式中直接说明。对于set() 函数,此参数是否称为y、aVector 或x 以外的任何对象名称都没有关系。为什么?由于在makeVector() 环境中已经定义了一个x 对象,因此使用相同的对象名称会使代码更难理解。
在set() 中,我们使用<<- form of the assignment operator,它将运算符右侧的值分配给由运算符左侧的对象命名的父环境中的对象。
当set()被执行时,它做了两件事:
- 将输入参数分配给父环境中的
x 对象,并且
- 将 NULL 的值分配给父环境中的
m 对象。这行代码清除了先前执行cachemean() 缓存的任何m 值。
因此,如果m中已经缓存了一个有效的均值,那么每当x被重置时,缓存在对象内存中的m的值就会被清除,强制后续调用cachemean()重新计算平均值而不是从缓存中检索错误的值。
注意set() 中的两行代码与主函数中的前两行完全相同:设置x 的值,将m 的值设置为NULL。
其次,makeVector() 定义了向量 x 的 getter。
get <- function() x
同样,此函数利用了 R 中的词法范围功能。由于符号 x 未在 get() 中定义,因此 R 从 makeVector() 的父环境中检索它。
第三,makeVector() 定义了均值 m 的设置器。
setmean <- function(mean) m <<- mean
由于m是在父环境中定义的,我们需要在setmean()完成后访问它,所以代码使用赋值运算符的<<-形式将输入参数赋值给m中的值父环境。
最后,makeVector() 定义了均值 m 的吸气剂。就像 x 的 getter 一样,R 利用词法作用域找到正确的符号 m 来检索其值。
getmean <- function() m
此时,我们为 makeVector() 对象中的两个数据对象定义了 getter 和 setter。
第 3 步:通过返回 list() 创建一个新对象
这是makeVector()函数操作中“魔法”的另一部分。最后一段代码将这些函数中的每一个分配为 list() 中的一个元素,并将其返回给父环境。
list(set = set, get = get,
setmean = setmean,
getmean = getmean)
当函数结束时,它返回一个完整的makeVector() 类型的对象,供下游 R 代码使用。这段代码的另一个重要细节是列表中的每个元素都是named。也就是说,列表中的每个元素都是使用elementName = value 语法创建的,如下所示:
list(set = set, # gives the name 'set' to the set() function defined above
get = get, # gives the name 'get' to the get() function defined above
setmean = setmean, # gives the name 'setmean' to the setmean() function defined above
getmean = getmean) # gives the name 'getmean' to the getmean() function defined above
命名列表元素允许我们使用$form of the extract operator按名称访问函数,而不是使用提取运算符的[[形式,如myVector[[2]](),获取内容向量。
这里需要注意的是cachemean() 函数需要makeVector() 类型的输入参数。如果将一个正则向量传递给函数,如
aResult <- cachemean(1:15)
函数调用将失败并出现错误,说明cachemean() 无法访问输入参数上的$getmean(),因为$ 不适用于原子向量。这是准确的,因为原始向量不是列表,也不包含$getmean() 函数,如下图所示。
> aVector <- 1:10
> cachemean(aVector)
Error in x$getmean : $ operator is invalid for atomic vectors
解释 cachemean()
没有cachemean(),makeVector() 功能不完整。为什么?按照设计,cachemean() 需要从makeVector() 类型的对象中填充或检索平均值。
cachemean <- function(x, ...) {
...
与makeVector() 一样,cachemean() 以单个参数 x 和一个省略号开头,允许调用者将其他参数传递给函数。
接下来,该函数尝试从作为参数传入的对象中检索平均值。首先,它在输入对象上调用getmean() 函数。
m <- x$getmean()
然后检查结果是否为NULL。由于makeVector() 将缓存的均值设置为NULL 每当一个新的向量被设置到对象中,如果这里的值不等于NULL,我们就有一个有效的、缓存的均值并且可以将它返回给父环境
if(!is.null(m)) {
message("getting cached data")
return(m)
}
如果!is.null(m)的结果是FALSE,cachemean()从输入对象中获取向量,计算一个mean(),对输入对象使用setmean()函数来设置输入对象的均值,然后通过打印均值对象将均值的值返回给父环境。
data <- x$get()
m <- mean(data, ...)
x$setmean(m)
m
请注意cachemean() 是执行mean() 函数的唯一位置,这就是为什么没有cachemean() 时makeVector() 是不完整的。
将各个部分放在一起:函数在运行时如何工作
现在我们已经解释了每个函数的设计,下面是一个说明它们在 R 脚本中使用时的工作原理。
aVector <- makeVector(1:10)
aVector$get() # retrieve the value of x
aVector$getmean() # retrieve the value of m, which should be NULL
aVector$set(30:50) # reset value with a new vector
cachemean(aVector) # notice mean calculated is mean of 30:50, not 1:10
aVector$getmean() # retrieve it directly, now that it has been cached
结论:是什么让 cachemean() 起作用?
总而言之,R 编程 中的词法范围分配利用了词法范围以及返回 list() 类型对象的函数也允许访问在原来的功能。在makeVector() 的具体实例中,这意味着后续代码可以通过使用getter 和setter 来访问x 或m 的值。这就是cachemean() 能够计算和存储输入参数的平均值(如果它是makeVector() 类型)的方式。因为makeVector() 中的列表元素是用名称定义的,所以我们可以使用$ form of the extract operator 访问这些函数。
有关解释作业如何使用 S3 对象系统功能的其他评论,请查看makeCacheMatrix() as an Object。
附录 A:这个作业的重点是什么?
学生完成作业后,他们经常会询问有关其价值和目的的问题。 Lexical Scoping and Statistical Computing 是一篇很好的文章,解释了统计计算中词法作用域的价值,由奥克兰大学的 Robert Gentleman 和 Ross Ihaka 撰写。
附录 B:cachemean.R
这是 cachemean.R 的完整列表
makeVector <- function(x = numeric()) {
m <- NULL
set <- function(y) {
x <<- y
m <<- NULL
}
get <- function() x
setmean <- function(mean) m <<- mean
getmean <- function() m
list(set = set, get = get,
setmean = setmean,
getmean = getmean)
}
cachemean <- function(x, ...) {
m <- x$getmean()
if(!is.null(m)) {
message("getting cached data")
return(m)
}
data <- x$get()
m <- mean(data, ...)
x$setmean(m)
m
}
附录 C:常见问题解答
问:为什么cachemean() 不返回缓存值?我的代码如下:
cachemean(makeVector(1:100))
cachemean(makeVector(1:100))
答:以这种方式编写的代码会创建两个不同类型的 makeVector() 对象,因此对 cachemean() 的两次调用会初始化每个实例的方法,而不是从单个实例缓存和检索。说明上述代码如何运行的另一种方式如下。
注意第一次调用cachemean() 是如何设置缓存的,第二次调用是如何从中检索数据的。
问:为什么set() 从未在代码中使用?
答:set() 包含在内,因此一旦创建了 makeVector() 类型的对象,就可以在不初始化该对象的另一个实例的情况下更改其值。第一次实例化makeVector() 类型的对象是不必要的。为什么?首先,将x 的值设置为函数参数,如makeVector(1:30)。然后,函数中的第一行代码设置m <- NULL,同时为m分配内存并将其设置为NULL。当函数结束时将此对象的引用传递给父环境时,x 和 m 都可以被它们各自的 get 和 set 函数访问。
以下代码说明了set()的使用。
问:为什么x 在makeVector() 中设置了默认值?
A:由于x 是一个参数,唯一可以为其设置默认值的地方是在形式中。未设置默认值时cachemean()返回的错误类型,
Error in x$get() : argument "x" is missing, with no default
是不可取的。我们的代码应该直接处理错误情况,而不是依赖于 R 中的底层错误处理。
创建makeVector() 类型的对象而不在初始化期间填充其值是完全有效的。 makeVector() 包含一个 setter 函数,因此可以在创建对象后设置其值。但是,在执行cachemean() 之前,该对象必须具有有效数据,即数字向量。
理想情况下,cachemean() 将包含在计算平均值之前验证x 不为空的逻辑。 x的默认设置使cachemean()返回NaN,这是一个合理的结果。
参考文献
- Chi, Yau -- R-Tutor Named List Members,2016 年 7 月 20 日检索。
- Wickham, Hadley -- Advanced-R Functions,于 2016 年 7 月 17 日检索。
- Wickham, Hadley -- Advanced-R Scoping Issues,于 2016 年 7 月 17 日检索。