环境(environments)是管理作用域的数据结构 (emm 就是无限套娃)
环境基础
环境组成
- 框架(frame),其中包含名字和对象的绑定(行为很像一个命名列表)
- 父环境。
环境的工作是关联或者绑定一组名称到一组值,每一个名字关联了一个对象,这个对象保存在内存中的某个地方
- 每个名字都指向一个对象,这个对象保存在内存中的某个地方
- 对象不存在于环境中,所以多个名字可以关联同一个对象
- 可以指向具有相同值的不同对象
- 如果一个对象没有名字指向它,那么它会被垃圾收集器(garbage collector)自动删除
- 每个环境都有父环境,也就是另一个环境。 父环境是用来实现词法作用域的:如果在一个环境中没有找到某个名称,那么 R 将在它的父环境中查找,以此类推。 只有一种环境没有父环境: 空环境
环境和列表的区别
一般来说,环境类似于列表,但是有四个重要区别:
- 在一个环境中的每个对象都有唯一的名称
- 在一个环境中的对象是无序的。 (比如,在一个环境中,查询排在”第一位”的对象是没有意义的)
- 环境有一个父环境
- 环境具有引用语义
引用语义 :一个对象被系统标准的复制函数复制后,与被复制的对象共享底层资源,只要一个改变了另外一个就会改变。.
特殊环境
globalenv()
或者叫全局环境,是交互工作空间。 这是普通的工作环境。 全局环境的父环境是上一个你使用library()
或require()
加载的包。baseenv()
,或者叫基础环境,是base
包的环境。 它的父环境是空环境。emptyenv()
,或者叫空环境,是所有环境的终极祖先,也是唯一没有父环境的环境。environment()
是当前环境。
使用search()
函数可以列出全局环境中的所有父环境, 这被称为搜索路径,因为在这些环境中的对象,都可以从顶层的交互工作区中找到。 它为每一个加载的包以及其它 attach()
的对象都包含一个环境。
> search()
[1] ".GlobalEnv" "tools:rstudio" "package:stats" "package:graphics"
[5] "package:grDevices" "package:utils" "package:datasets" "package:methods"
[9] "Autoloads" "package:base"
Autoloads
: 它用于按需加载程序包,以便节省内存
使用as.environment()
访问搜索列表中的任何环境
> as.environment("package:stats")
<environment: package:stats>
attr(,"name")
[1] "package:stats"
attr(,"path")
[1] "D:/soft/R/R-4.0.3/library/stats"
每当你使用library()
加载一个新的包,它就会被插入在全局环境以及先前位于搜索路径顶部的包之间
创建环境
使用new.env()
创建一个环境,使用$
、[[
或者get()
查看环境中的绑定关系,同时可以修改绑定关系,设置参数all.names = TRUE
可以显示环境中所有的绑定关系
get()
使用普通的作用域规则,如果绑定关系找不到,则抛出错误。- 使用
exists()
来确定某个绑定关系是否在环境中存在,不会搜索父环境 (inherits = FALSE
时,get()
也可以) - 使用
rm()
来删除绑定关系 要比较环境,必须使用
identical()
而不是==
e <- new.env("e1")
e$a <- 1
e$b <- 2
ls(e)
e$a <- 2
也可以使用
ls.str()
查看环境中所有的名字对应的对象,all.names
也支持e <- new.env("e1")
e$a <- 1
e$b <- 2
e$d <- "c"
> ls.str(e)
a : num 1
b : num 2
d : chr "c"
函数环境
有四类和函数相关的环境四类与函数相关的环境
封闭环境:封闭环境是函数创建时所处的环境。 每个函数都有且只有一个封闭环境
- 绑定环境:使用
<-
把一个函数绑定到一个名称,就定义了一个绑定环境 - 执行环境:调用函数,创建了一个短暂的执行环境,它用于存储执行期间创建的变量
- 调用环境:每一个执行环境都关联了一个调用环境,它告诉你函数是从哪里调用的。
封闭环境
创建一个函数时,它会得到它所处的环境的引用。 这就是封闭环境,用于词法作用域。 你可以调用environment()
来确定一个函数的封闭环境,并把函数作为它的第一个参数f <- function(x){
x <- x ** 2
x
}
environment(f)
#<environment: R_GlobalEnv>
绑定环境
函数的绑定环境是所有与它有绑定关系的环境 ,如下,y
是和函数有绑定关系的值,此时的绑定环境和封闭环境是同一个环境<environment: R_GlobalEnv>
y <- 1
f <- function(x){
x <- x ** y
x
}
如果将函数的封闭环境改一下,那封闭环境和绑定环境就不是同一个了e <- new.env()
e$g <- function() 1
封闭环境属于函数,并且从来都不会改变,即使函数被搬到一个不同的环境中也是。 封闭环境决定函数如何寻找值; 绑定环境决定我们如何找到函数命名空间
namespac是许多编程语言使用的一种代码组织的形式,通过命名空间来分类,区别不同的代码功能,避免不同的代码片段(通常由不同的人协同工作或调用已有的代码片段)同时使用时由于不同代码间变量名相同而造成冲突
对于包的命名空间(namespace)来说, 绑定环境和封闭环境的区别是很重要的。 包的命名空间保持了包的独立性,命名空间是使用环境来实现的,基于这样的事实,那么函数不需要存在于它们的封闭环境中。 以基本函数sd()
为例。 它的绑定环境和封闭环境是不同的
environment(sd)
#> <environment: namespace:stats>
where("sd")
# <environment: package:stats>
每一个包都有与它关联的两个环境: 包环境和命名空间环境。 包环境包含所有可公开访问的函数,并且被放置在了搜索路径之上。 命名空间环境包含所有函数(包括内部函数),并且其父环境是一个特殊的导入(import)环境,它包含着这个包需要的所有函数的绑定关系。 包中的每个导出(exported)函数都被绑定到包环境,但是被命名空间环境进行封闭
输入 var 时,它首先在全局环境中被发现。 而当 sd()寻找 var()时,它首先在其命名空间环境中发现 var(),因此永远都不会搜索 globalenv()
执行环境
函数的调用执行”全新的开始原则”(fresh start principle)
,每当函数被调用的时候,一个新的环境将被创建出来管理执行过程。 执行环境的父环境是函数的封闭环境。 一旦函数执行完毕,这个环境就会被抛弃
当你在一个函数中创建另一个函数时, 子函数的封闭环境就是父函数的执行环境,并且执行环境不再是临时的
plus <- function(x) {
function(y) x + y
}
plus_one <- plus(1)
identical(parent.env(environment(plus_one)), environment(plus))
#> [1] TRUE
调用环境
每一个执行环境都关联了一个调用环境,它告诉你函数是从哪里调用的。
顶层的 x(被绑定到 20)其实与结果没有什么关系, h()
使用普通的作用域规则,在它被定义的环境中进行搜索,然后发现关联到 x 的值是 10。 然而,在 i()被调用的环境中,询问 x 关联到什么值,仍然是有意义的:在**h()**
被定义的环境中,x 是 10,但是在 h()被调用的环境中,它是 20。
h <- function(){
x <- 10
function(){
x
}
}
i <- h()
x <- 20
i()
# 此时i的值为 10
更复杂的场景中,不是只有一个父环境被调用,而是一系列的调用,它会导致从顶层的发起函数开始,一直调用到最底层函数。 下面的代码生成了一个三层深的调用栈。 开放式的箭头表示每一个执行环境的调用环境
x <- 0
y <- 10
f <- function() {
x <- 1
g()
}
g <- function() {
x <- 2
h()
}
h <- function() {
x <- 3
x + y
}
f()
# 此时f()输出为13
赋值
<-
在一个环境中, 赋值就是把一个值绑定(或重新绑定)到一个名字的行为。 它是与作用域相对应的,是决定如何找到与一个名字相关联的值的规则集合,名字通常由字母、数字、 .和组成,而不能以``, 数字开始。并且不能是保留字
<<-
深赋值(deep assignment
)箭头符号, <<-
,不会在当前环境下创建一个变量,而是不断向上搜索父环境,直到找到变量后,直接修改现有变量。 你也可以使用assign()
进行深度绑定:name <<- value
相当于assign("name",value, inherits = TRUE)
如果<<-
没有找到现有变量,那么它将在全局环境中创建一个。 这通常是不可取的,因为全局变量会引入不易察觉的函数之间的依赖关系。
延迟绑定
延迟绑定创建和存储一个表达式的承诺,仅在需要的时候进行计算,而不是立即赋予表达式的结果,使用特殊赋值运算符%<d-%
来创建延迟绑定,它由pryr
包提供
system.time(b %<d-% {Sys.sleep(1); 1}, 1)
活动绑定
每次访问它们的时候,都会重新进行计算, %<a-%
x1 <- runif(1)
x1
x1
[1] 0.2595692
[1] 0.2595692
x2 %<a-% runif(1)
x2
x2
[1] 0.6626286
[1] 0.7056665
显式环境
显式环境(Explicit environments) ,不像 R 中的其它大多数对象,当你修改一个环境时,它不会进行复制
modify <- function(x) {
x$a <- 2
invisible()
}
x1 <- list()
modify(x1)
x1
# [1] list()
x_env <- new.env()
modify(x_env)
x_env$a
# [1] 2
避免复制
由于环境具有引用语义,因此你永远都不会意外地创建了一个副本。 这使得它成为可以包含大对象的很有用的容器。 这是 bioconductor
包用于管理大基因组对象,经常需要使用的技术。 但是,R 3.1.0
中的改变使这种技术变得不那么重要了,因为修改列表不再会进行深拷贝(deep copy)了。 此前,修改列表中的一个元素会导致复制所有元素,如果某些元素很大,那么这是一种开销很大的操作。 而现在,修改列表时会有效地重用已有的向量,节省了大量时间
管理一个包的状态
在包中,显式的环境是有用的,因为它们允许你在函数调用之间维护包的状态。通常,包中的对象是锁定的,所以你不能直接修改它们, emptyenv()
可以替换成其他环境,这样就能修改包中的变量的了
my_env <- new.env(parent = emptyenv())
my_env$a <- 1
get_a <- function(){
my_env$a
}
set_a <- function(value){
old <- my_env$a
my_env$a <- value
invisible(old)
}
get_a()
set_a(11)
get_a()
高效地通过名字查找值
哈希表(hashmap)是一种数据结构,使用它根据名字来查找对象时,查找的时间复杂度是常数的, O(1)。 在默认情况下,环境提供了这种行为,所以它可以用来模拟一个哈希表