链接环境
环境有父环境,如果一个符号不存在于环境中,R 就会到它的父环境中查找。假设我
们对环境中的一个变量使用 get( ) 函数。如果在此环境中找到该变量,则获取其值,否
则函数将会在父环境中查找该变量。
和前面创建环境 e1 一样,我们再创建一个新的环境 e2,令 e1 为 e2 的父环境:
e2 <- new.env(parent = e1)
不同的环境有不同的内存地址:
e2
## <environment: 0x000000001772ef70>
e1
## <environment: 0x0000000014a45748>
根据定义,e1 为 e2 的父环境,那么 e2 的父环境的内存地址应该和 e1 一致,我们可
以通过 parent.env( ) 函数来验证:
parent.env(e2)
## <environment: 0x0000000014a45748>
现在,我们在环境 e2 中创建一个变量 y:
e2$y <- 2
使用 ls( ) 函数检查 e2 中的所有变量名:
ls(e2)
## [1] "y"
同样,也可以用 $、[[、exists( ) 或 get( ) 来访问变量的值:
e2$y
## [1] 2
e2[["y"]]
## [1] 2
exists("y", e2)
## [1] TRUE
get("y", e2)
## [1] 2
然而,提取操作符($ 和 [[)和环境访问函数有一个明显的区别。操作符只在单个环
境域内可用,而函数在一个环境链上都可用。
注意,我们没有在 e2 中定义名为 x 的变量。毫无意外,用两个操作符提取 x 都会得
到 NULL:
e2$x
## NULL
e2[["x"]]
## NULL
当我们使用 exists( ) 和 get( ) 函数时,父环境就派上用场了。由于在 e2 中没
能找到 x,函数就会继续在父环境 e1 中寻找:
exists("x", e2)
## [1] TRUE
get("x", e2)
## [1] 1
这就是调用这两个函数都能得到有效结果的原因。如果不想让函数搜索父环境,可以
设置 inherits = FALSE。在这种情况下,就算在给定环境中没有找到变量,函数也不
会继续搜索父环境,因此 exists( ) 返回值为 FALSE:
exists("x", e2, inherits = FALSE)
## [1] FALSE
而调用 get( ) 函数则会报错:
get("x", e2, inherits = FALSE)
## Error in get("x", e2, inherits = FALSE): 找不到对象'x'
环境链可以有很多层。例如创建一个环境 e3,令 e2 为其父环境。当对 e3 中的一个
变量调用 get( ) 函数时,它将沿着环境链搜索。
1.在引用语义下使用环境
环境具有引用语义。这意味着,与原子向量和列表等数据类型不同,修改环境时不会
复制该环境,无论它有多个名称还是作为参数传递给函数。
例如,我们将 e1 的值赋给另一个变量 e3:
ls(e1)
## [1] "x"
e3 <- e1
如果我们有两个变量指向同一个列表,修改其中一个变量,首先会创建一个列表副本,
然后再修改这个列表的副本,而不影响原始列表。引用语义则不同。当我们通过任意一个
变量修改环境时,不会创建环境副本。因此,可以通过 e1 和 e3 观察变化,因为它们指向
完全相同的环境。以下代码演示了引用语义是如何工作的:
e3$y
## NULL
e1$y <- 2
e3$y
## [1] 2
首先 e3 中没有定义 y。接着我们在 e1 中创建了一个新的变量 y。由于 e1 和 e3 指向
完全相同的环境,便可以通过 e3 访问 y。
将一个环境作为参数传递给一个函数时也会发生同样的情况。假设我们定义了如下函
数,将 e 中的 z 设为 10:
modify <- function(e) {
e$z <- 10
}
可以将一个列表传递给这个函数,但修改不会生效。相反,函数会创建并修改一个
局
部副本,只是这个副本在函数调用结束后便丢失了:
list1 <- list(x = 1, y = 2)
list1$z
## NULL
modify(list1)
list1$z
## NULL
然而,如果将一个环境传递给函数,修改环境不会产生局部副本,而是在环境中直接
创建一个新变量 z:
e1$z
## NULL
modify(e1)
e1$z
## [1] 10
2.内置环境
环境是 R 中一种特殊类型的对象,但从实现函数调用到词法作用域机制,无一不是基
于环境实现的。事实上,一段 R 代码的运行就是在一个环境中进行的。要想知道我们是在
哪个环境中运行代码,可以调用 environment( ) 函数:
environment()
## <environment: R_GlobalEnv>
结果显示当前环境就是全局环境。事实上,每次新开启一个可供用户输入的 R 会话,
其工作环境都是全局环境。在做数据分析时,我们通常都是在这个环境中创建变量和函数。
正如前面的例子所示,环境也是可以创建和使用的对象。例如,我们可以将当前环境
赋值给变量,并在此环境中创建新的符号:
global <- environment()
global$some_obj <- 1
上述赋值等价于直接调用 some_obj <- 1,因为这已经在全局环境中了。只要运行
上面的代码,全局环境就会被修改,且 some_obj 会获取一个值:
some_obj
## [1] 1
还有其他方法可以访问全局环境。例如,globalenv( ) 和 .GlobalEnv:
globalenv()
## <environment: R_GlobalEnv>
.GlobalEnv
## <environment: R_GlobalEnv>
全局环境(globalenv( ))是用户的工作空间,而基础环境(baseenv( ))则提
供基础函数和运算符:
baseenv()
## <environment: base>
如果在 RStudio 编译器中输入 base::,会出现一长串函数。我们在前面章节中介绍的
大多数函数都是在基础环境中定义的,例如用于创建基本数据结构的函数(例如 list( )
和 data.frame( ))和运算符(例如 [ 、: 和 +)。
全局环境和基础环境是最重要的内置环境。你可能会问“谁是全局环境的父环境?谁又
是基础环境的父环境?它们的父环境的父环境呢?”
我们可以用以下函数找出给定环境的环境链:
parents <- function(env) {
while (TRUE) {
name <- environmentName(env)
txt <- if (nzchar(name)) name else format(env)
cat(txt, "\n")
env <- parent.env(env)
}
}
此函数依次输出每个环境及其父环境的名称,现在可以找出全局环境所有层级的父
环境:
parents(globalenv())
## R_GlobalEnv
## package:stats
## package:graphics
## package:grDevices
## package:utils
## package:datasets
## package:methods
## Autoloads
## base
## R_EmptyEnv
## Error in parent.env(env): the empty environment has no parent
我们注意到环境链终止于一个名为空环境(empty environment)的环境,它是唯一一个没
有任何绑定且没有父环境的环境。我们还可以通过 emptyenv( ) 函数查看空环境,但是调
用 parent.env(emptyenv( )) 则会报错。这也是 parents( ) 函数最终总会报错的原因。
环境链是内置环境和扩展包环境的组合。调用 search( ) 函数从全局环境的视角来
获取查找符号的搜索路径:
search()
## [1] ".GlobalEnv" "package:stats"
## [3] "package:graphics" "package:grDevices"
## [5] "package:utils" "package:datasets"
## [7] "package:methods" "Autoloads"
## [9] "package:base"
明白了查找符号是沿着环境链的路径进行的,我们就能明白以下代码在全局环境中的
具体计算过程:
median(c(1, 2, 1+3))
表达式看起来很简单,但是计算过程要复杂得多。首先,R 在环境链中寻找 median( )
函数,此函数在 stats 包的环境中。然后在基础环境中找到 c( ) 函数。最终,可能会令
你感到惊讶的是还需要找到 +(这也是一个函数!),它也在基础环境中。
事实上,每当你加载一个扩展包,这个包的环境都会插入搜索路径,并位于全局环境
之前。如果需要调用两个包中的同名函数,则会优先选取后加载的包中定义的函数(后添
加包的同名函数会屏蔽掉之前包中的函数),因为后加载的包更接近全局环境。