专注系列化、高质量的R语言教程
(本号已支持快捷转载,无需白名单即可转载)
本篇接上篇。主要参考资料有:
1、http://adv-r.had.co.nz/Environments.html[1]
2、https://adv-r.hadley.nz/environments.html[2]
3、https://www.likecs.com/ask-5533633.html[3]
本篇目录如下:
2 函数环境
2.1 绑定环境
2.2 封闭环境
2.3 执行环境
2.4 调用环境
3 环境作为数据结构
2 函数环境
通过一个简单的例子,就可以理解函数有自己的环境:
x <- 2
f <- function(y) {
x <- 1
y + x
}
如上代码,在函数定义式之外存在一个变量x <- 2
(全局环境),函数定义式内部也有一个同名变量x <- 1
。显然,函数在运行时会使用内部的x
变量,而且不会改变外部x
变量的取值。
f(1)
## [1] 2
x
## [1] 2
而如果函数内部没有定义x
,则运行时会使用外部的x
:
f <- function(y) {
y + x
}
f(1)
## [1] 3
这符合上篇介绍的环境的元素检索特点:首先会在当前环境中检索,检索不到则向父环境中检索。从中我们也可以推测,全局环境应当是这个函数环境(function environment)的父环境。
上述推测不完全准确。实际上,函数存在四个环境:绑定环境、封闭环境、执行环境和调用环境。上例中我们感受到的函数环境,实际是执行环境。
2.1 绑定环境
函数作为一个对象,本身就是一个环境的元素,这个环境称为函数的绑定环境(binding environment)。如上例中的f
位于全局环境中,则全局环境就是它的绑定环境。find()
函数可以根据名称查找函数的绑定环境:
find("f")
## [1] ".GlobalEnv"
pryr
工具包的where()
是专门查找函数绑定环境的函数:
library(pryr)
where("f")
## <environment: R_GlobalEnv>
对于工具包的函数而言,它们的绑定环境是package:*
:
library(dplyr)
find("select")
## [1] "package:dplyr"
2.2 封闭环境
函数的封闭环境(enclosing environment)不太容易理解,但却十分重要,它能解决我们在上篇“引子”中所提出的问题。简而言之,封闭环境就是函数在被最初定义时所对应的当前环境(current environment),它具有不可变性,即不会随着函数所处位置的变化而变化。
使用environment()
函数可以查看函数的封闭环境。对于上例中的f
而言,它的封闭环境仍然是全局环境:
environment(f)
## <environment: R_GlobalEnv>
对于工具包的函数而言,它们的封闭环境是namespace:*
:
environment(select)
## <environment: namespace:dplyr>
函数的封闭环境和绑定环境有何区别呢?或者说,对于每个工具包而言,R为何要为其设计两个环境?
很容易理解,函数是哪个环境的元素,它的绑定环境就是哪个环境,因此上述问题就转变成封闭环境存在的必要性了。
以sd()
函数为例,它的作用是计算序列的标准差,位于基础包stats
中,定义式如下:
sd
## function (x, na.rm = FALSE)
## sqrt(var(if (is.vector(x) || is.factor(x)) x else as.double(x),
## na.rm = na.rm))
## <bytecode: 0x0000021607f94ca0>
## <environment: namespace:stats>
而函数是可以进行传递的:
g <- sd
注意:
sd
后不能带括号。
这样我们实际上就在全局环境中增加了一个和sd()
功能一致的函数g
,而g
绑定环境显然是全局环境,不同于sd()
:
find("g")
## [1] ".GlobalEnv"
但是从功能上看,g
的定义式是在stats
工具包中完成的,它和sd()
的封闭环境仍然相同:
environment(g)
## <environment: namespace:stats>
从定义上可以看出,sd()
依赖于var()
等函数。现在,我们在全局环境中重新定义一个var()
函数,它并不会影响g
和sd()
函数的运行结果:
var <- function() 1
sd(1:4)
## [1] 1.290994
g(1:4)
## [1] 1.290994
因此可以说,封闭环境的不变性保证了函数功能在传递时的一致性,不过这种一致性还得依靠执行环境来完成。
2.3 执行环境
执行环境(execution environment)是函数在调用时所形成的环境,一旦函数运行结束,执行环境就不存在了。
函数的封闭环境是执行环境的父环境;如果在定义一个函数(子函数)时使用了另一个函数(父函数),那么父函数的执行环境就是子函数的封闭环境。这也就是为什么在上例中,函数g
在运行时会向封闭环境namespace:stats
中查找var()
函数,而不是使用全局环境所定义的var()
函数。至此,我们就可以回答“引子”中问题了:前两种情况不会影响,后两者情况会影响。
2.4 调用环境
调用环境(calling environment)可以理解成函数在运行时的外部环境。使用parent.frame()
函数可以查看调用环境(注意区分与parent.env()
函数)。前面的例子中,全局环境的var()
函数就位于调用环境中。
下例中,x <- 1
位于函数的执行环境中,而x <- 2
就位于函数的调用环境中:
x <- 2
f <- function(y) {
x <- 1
x1 <- get("x", environment())
x2 <- get("x", parent.frame())
if (y == 1) return(x1)
if (y == 2) return(x2)
}
f(1)
## [1] 1
f(2)
## [1] 2
3 环境作为数据结构
环境本身就是一种数据结构,前面介绍的proto对象就是一种环境。
对于普通数据结构来说,修改取值实际是通过重新定义一个同名对象来完成的:
x <- 1
x <- x + 1
x
## [1] 2
如果你不把修改值再赋给x
,那么程序运行完后就会把结果回收而不会改变对象值:
x <- 1
x + 1
x
## [1] 1
而环境对象则不然,这一点在proto对象中已有体现:
e <- new.env()
e$x <- 1:4
e$f <- function() {e$x = e$x + 1}
e$f()
e$x
## [1] 2 3 4 5
按照Hadley Wickham的说法,环境相比于普通数据结构有如下三个作用[4]:
避免通过备份来修改对象值(Avoiding copies of large data);
更好管理工具包的函数(Managing state within a package);
绘制哈希图(As a hashmap)。
参考资料
[1]
Advanced R (1st Edition) - Environments: http://adv-r.had.co.nz/Environments.html
[2]Advanced R (2nd Edition) - Environments: https://adv-r.hadley.nz/environments.html
[3]论坛问答: https://www.likecs.com/ask-5533633.html
[4]Environment as data structures: https://adv-r.hadley.nz/environments.html#explicit-envs