简介

环境是R语言比较困难的概念, 一般用户也不需要了解环境就能很好地使用R, 也不影响自定义函数。 环境是支持变量作用域、命名空间、R6类型等功能的数据结构, 了解环境有助于更好地理解作用域等概念。

环境作为一个数据结构与有名的列表相似, 但是其中的名字必须都互不相同, 且没有次序(类似集合), 环境都有一个父环境, 修改环境内容时都不制作副本。

初识环境

生成新环境

rlang扩展包可以比较方便地操作R的语法内容。 可以用rlang::env()生成新的环境, 这类似于list() 函数的用法, 如:

  1. e1 <- rlang::env(
  2. a = FALSE,
  3. b = "a",
  4. c = 2.3,
  5. d = 1:3)

环境的作用是将一系列的名字(变量名、函数名等)与R对象绑定起来, 即建立从名字到对象的对应关系, 不计次序。

但此刻,我们创建的这些变量都是在e1 环境下的,而不存在于用户所在的主环境中:

> ls()
[1] "dot_every" "e1"        "i"         "make.pf"   "p"         "sim"       "square"    "x"

直接打印显示环境, 只会显示一个地址信息, 对用户没有什么用处:

e1
## <environment: 0x000001d185e88860>

我们可以通过env_names 获取某环境中变量的名称,以及env_print 获取某环境中变量名称及类型:

rlang::env_print(e1)
## <environment: 000001D185E88860>
## parent: <environment: global>
## bindings:
##  * a: <lgl>
##  * b: <chr>
##  * c: <dbl>
##  * d: <int>
##  * e: <list>

rlang::env_names(e1)
## [1] "a" "b" "c" "d" "e"

如果想获取当前主环境下的变量,除了使用ls() 还可以使用env_names(current_env()),除此之外,environment() 也可以返回代码调用时所在的环境。

对环境的修改是直接进行而不制作副本的。 如:

e1$e <- list(x=1, y="abcd")
> env_print(e1)
<environment: 0x7ffd253568e8>
parent: <environment: global>
bindings:
 * a: <lgl>
 * b: <chr>
 * c: <dbl>
 * d: <int>
 * e: <named list>

重要环境

rlang::current_env() 或基本 R 的 environment() 返回调用代码时所在的环境,比如,在命令行调用时,返

rlang::global_env() 和基本 R 的 globalenv() 返回全局环境,这是另一个重要环境,也称为 “工作空间”,是
在命令行运行时所处的环境。

如果想要比较两个环境,可以使用函数identical,而不能使用==。

父环境

每个环境都有一个父环境,这样在按句法规则查找变量的绑定对象时,就依次查找环境本身、其父环境、父环境的父环境,等等。内嵌函数的父环境是定义它的函数的内部环境。

当 rlang::env() 没有输入父环境时,父环境就设为调用时的环境。用rlang::env_parent() 获得父环境,用 rlang::env_parents() 获得各层父环境,如:

> env_parent(e1)
<environment: R_GlobalEnv>
> env_parents(e1)
[[1]] $ <env: global>
> env_parents(e2)
[[1]]   <env: 0x7fca682ffd58>
[[2]] $ <env: global>

对于以全局环境为父环境的环境,env_parents() 的输出截止到全局环境为止;但是,全局环境的上层还有加载的各个扩展包的环境,这也是查找变量的较后面的搜索路径:

> env_parents()
 [[1]] $ <env: package:maftools>
 [[2]] $ <env: package:rlang>
 [[3]] $ <env: tools:RGUI>
 [[4]] $ <env: package:stats>
 [[5]] $ <env: package:graphics>
 [[6]] $ <env: package:grDevices>
 [[7]] $ <env: package:utils>
 [[8]] $ <env: package:datasets>
 [[9]] $ <env: package:methods>
[[10]] $ <env: Autoloads>
[[11]] $ <env: package:base>
[[12]] $ <env: empty>

为了制造一个上层不包含全局环境的环境,可以用 rlang::empty_env() 作为父环境,这个环境称为空环境,记 为 R_EmptyEnv。

> e3 = env(empty_env(), a = 2, c = 3)
> 
> e3
<environment: 0x7fca731095a0>
> env_print(e3)
<environment: 0x7fca731095a0>
parent: <environment: empty>
bindings:
 * a: <dbl>
 * c: <dbl>

上层环境赋值

在R 赋值中我提到过,<<- 表示在各级父环境中赋值,最先在那一层父环境中找到变量就在那一层中赋值,如果直到全局环境都没有找到 变量,就在全局环境中新建一个变量。

在使用闭包时,通常通过这种手段保存并修改闭包中的状态:

f0 <- function(){ x <- 0
f1 <- function(){ f2 <- function(){
x <<- x+1
x }
f2() }
f1 }
f01 <- f0() f01()
## [1] 1
f01()
## [1] 2

访问环境中的元素

类似于列表元素访问,用 “环境 $ 名字” 格式或者 “环境 [[“ 名字”]]” 读取环境的元素,不存在时返回 NULL。
如:

> e1$a
[1] 1

07. 环境 - 图1

如果希望在找不到变量时出错,可以用 rlang::env_get(环境名, “ 名字”),如:

 rlang::env_get(e2, "x")
## Error in rlang::env_get(e2, "x") : 找不到对象'x'

可以设置 env_get() 在查找不到时的缺省值,如:

rlang::env_get(e2, "x", default=NA) ## [1] NA

另外,不能直接读取父环境中的变量。

添加或修改环境中的元素

为了在环境中增加绑定或重新绑定,可以用 $或 [[格式直接赋值,可以用 rlang::env_poke()或 rlang::env_bind(), rlang::env_bind() 运行同时进行多个绑定,如:

e1 <- rlang::env(x=1, y=2) e1$z <- 3
rlang::env_poke(e1, "a", 11) rlang::env_bind(e1, b=12, c=13) rlang::env_names(e1)
## [1] "x" "y" "z" "a" "b" "c"

用 rlang::env_has() 检查某个环境中是否绑定了指定的名字,如:

rlang::env_has(e1, c("x", "c", "f")) ## x c f
## TRUE TRUE FALSE

为了在环境中删除一个名字的绑定,需要用 rlang::env_unbind(),如:

rlang::env_unbind(e1, c("z", "c")) rlang::env_names(e1)
## [1] "x" "y" "a" "b"

注意 env_unbind() 只是解除了绑定,原来的对象并不会马上被删除,如果没有其它名字引用该对象,R 的垃圾 收集器会随后删除该对象。

基本 R 的 get(),assign(), exists(),rm() 等函数起到与 rlang 包中的 env_get(),env_poke(), env_has(), env_unbind() 类似的功能,但是这些函数通常都针对调用时的当前环境,不容易处理其他环境,另外它们都有一个 inherits 选项默认为 TRUE,可以自动搜索父环境,所以不如 rlang 包的函数功能明确。

特殊环境

实际上,一般用户都不会用到 rlang::env() 生成的环境;但是用户都会接触到使用 R 语言时自然产生的各种环
境,只不过许多用户可能没有认识到自己是在使用环境。 R 语言使用中涉及到环境的情景有:
• 扩展包的环境和搜索路径; • 函数环境;
• 命名空间;
• 运行环境。

拓展包环境

每次用 library() 或 require() 命令载入一个扩展包,它定义的变量和函数就构成一个环境,这个环境变成全 局环境的父环境。这样,全局环境的各层父环境包括用户载入的扩展包和启动 R 会话时会自动载入的扩展包(如 stats 等),载入最晚的一个扩展包的环境是全局环境的父环境,载入越早的扩展包的环境距离全局环境的层次越远。 实际上,在查找某个名字时,在当前环境没有找到时会逐层向父环境查找,这些父环境一般就包括全局环境和全局环境上层的各个加载了的扩展包形成的环境,这种搜索次序称为当前搜索路径,search() 返回当前搜索路径,如:

search()
## [1] ".GlobalEnv" "tools:rstudio" "package:stats" ## [4] "package:graphics" "package:grDevices" "package:utils" ## [7] "package:datasets" "package:methods" "Autoloads"
## [10]"package:base"
# rlang::search_envs() 返回以环境为元素的搜索路径。

其中有两个比较特殊:
• Autoloads 用类似于函数缺省值懒惰求值的方法在需要用到某个变量时才从磁盘将其载入到内存中,适用于 占用存储空间很大的数据框之类的对象。
• package:base 是基本 R 的环境,必须先载入这一环境才能加载其它环境。可以直接用 rlang::base_env() 返回这一环境。

函数内部环境

自定义函数包括形参表、函数体和定义时绑定的环境三个部分,非内嵌的也不在扩展包中定义的函数一般都与全局环境绑定,这样的函数的绑定环境没有什么用处,即使不了解环境部分也能够很好地使用这样的函数。

内嵌在函数内定义的函数称为闭包,闭包的绑定的环境是定义它的函数的内部环境,如果这个闭包作为定义它的函数的输出,闭包对象带有一个私有环境,即定义它的函数的内部环境,可以用来保存闭包函数的状态。

用 rlang::fn_env(f) 可以求函数 f 的绑定环境。

比如:

f1 <- function(x) 2*x rlang::fn_env(f1)
## <environment: R_GlobalEnv>

# 闭包中
f1 <- function(){ times <- 0
f2 <- function(){
times <<- times + 1
cat("NO. ", times, "\n", sep="") }
print(rlang::fn_env(f2))
f2 }
f2b <- f1()
## <environment: 0x00000201dc5096d8> print(rlang::fn_env(f2b))
## <environment: 0x00000201dc5096d8> f2b()
## NO. 1
f2b()
## NO. 2

这个例子显示的 f2 和 f2b 的环境都是 f1 内部的环境,在现实 f2b 的环境时虽然 f1() 已经结束运行,但是闭 包可以保存其定义时的环境。

两个更特殊的环境

rlang::env_bind_lazy() 可以创造延迟的绑定,就是类似于 R 函数形参缺省值的懒惰求值那样,第一次使用其 值的时候才进行绑定。利用这种技术,可以实现类似 autoload() 的功能,autoload() 可以使得用到某个扩展包中指定的名字时才自动载入该扩展包,利用延迟绑定,可以使得数据框看起来像是已经在内存中,但实际是用到该 数据框时才中硬盘中读入。基本 R 中提供了类似的 delayedAssign() 函数。

rlang::env_bind_acitive() 可以制造一个环境,每次访问环境中的名字都重新求值并绑定一次。基本 R 中提 供了类似的 makeActiveBinding() 函数。

逐层向上访问环境

我们可以写个递归函数实现:

where <- function(name, env = rlang::caller_env()) { if (identical(env, empty_env())) {
# 找到了顶层都没有找到
stop(" 找不到 ", name, call. = FALSE) } else if (rlang::env_has(env, name)) {
# 在当前的 env 环境中找到了,返回找到时的环境 
  env
} else {
# 利用递归向上层查找
Recall(name, rlang::env_parent(env))
} }

自变量 name 是要查找的名字,env 是从那个环境开始逐层向上查找,env 的缺省值是调用 where() 函数时的环 境。定义中分了三种情况:到顶层(空环境)都没有找到,出错停止;在向上逐层查找中在某个环境中找到了,返 回找到时的环境;否则就利用递归向上层查找。
这个例子可以用作环境逐层向上遍历的模板。

ps:用env_parents 不是也可以得到吗。

命名空间与环境运行

变量名和函数名的搜索路径中包含了已载入的扩展包的环境,这就造成一个问题:后载入的扩展包中的函数会遮盖 住先载入的扩展包中的同名函数,变量也是如此。所以,应该仅载入必要的扩展包,尽可能用 “扩展包名:: 函数 名” 的格式调用。

这些问题是用户可控的,还有一个本质性的问题:假设扩展包 A 中的函数 f1 要用到扩展包 B 中的函数 f11,先 载入了扩展包 B,然后载入了扩展包 A,这时调用 A 中的 f1() 没有问题。现在假设随后又调入了一个扩展 C, 扩展包 C 中也定义了一个 f11 函数,那么,现在调用 A 中的 f1 时,会调用 B 中的 f11 还是 C 中的 f11? 如 果调用 C 中的 f11 就是会程序出错或给出错误结果。
ps:这里先前曾老师还专门讲过对于用户的解决方案:https://mp.weixin.qq.com/s/l90spoS_YQ-6AFcLiqEp0g

为了避免这样的不可控的错误发生,R 语言的扩展包开发进行了严格的规定。R 的扩展包与两个环境有关,一个就 是扩展包的环境,这实际是用户能看到的 R 扩展包提供的变量和函数,在载入扩展包时会插入到搜索路径中。
另 一个环境是命名空间环境,这是扩展包私有的一个环境,其中的变量和函数有一些对包的用户不可见,扩展包环境 中那些用户可见的变量和函数也在命名空间环境中。R 扩展包在设计时都会利用命名空间严格限定包内部调用的其 它包中的函数,不至于引起歧义。

每个扩展包的命名空间环境都有如下的一套上层环境:
• imports环境,其中包含所有的用到的其它扩展包的函数,这是由扩展包的开发者确定的,所以不会错误调用 错误的包;
• imports 环境的父环境是基本 R 环境对应的命名空间环境,但其父环境与基本 R 环境的父环境不同;
• 基本 R 命名空间环境的父环境是全局环境。注意基本 R 环境的父环境是空环境。

所以,扩展包内调用其它扩展包的函数是需要开发者明确地加入到 imports 环境中的,不受用户调用时载入了那些 扩展包和载入次序影响。扩展包环境(针对用户的)和扩展包命名空间环境(包开发者自用)这 ‘两个环境不发生 直接的引用联系,可以通过函数环境逐层向上变量发生联系。

ps:说了这么多,感觉也没有提及用户解决包的函数冲突的方法呀。

函数在调用执行时自动生成一个运行环境,其父环境为函数定义时的环境,比如,设 f 是在命令行定义的函数,调用 f() 时自动生成一个 f 的运行环境,相当于 f 的局部变量和形参的环境,其父环境为f 定义时的环境,即全局环境。

设函数 f2 在函数工厂 f1 中定义并被 f1 输出为一个闭包 f2b,则调用 f2b 时自动生成一个 f2b 的运行环境, 相当于 f2b 的局部变量和形参组成的环境,此运行环境的父环境是定义时的环境,即 f2 函数内部的环境。函数执行结束则运行环境消失。

为了能够保留下来运行环境,一种办法是将运行环境在运行时用 rlang::current_env() 获取并作为函数的返回值保存到变量中,另一种办法是像函数工厂那样输出一个闭包,闭包的环境就是函数工厂的运行环境。

调用栈

函数在被调用时,还涉及到调用它的环境,可以用 rlang::caller_env() 获得。调用环境与调用时的实参计算 有关,需要了解调用栈 (call stack) 概念,调用栈由若干个分层的框架 (frames) 组成。R 运行出错时会显示一个 traceback() 结果,就是调用栈的各个框架。如:

> f1 <- function(x) { f2(x = 2)
+ }
> f2 <- function(x) {
+ f3(x = 3) }
> f3 <- function(x) { stop()
+ }
> f1()
Error in f3(x = 3) : 
> traceback()
4: stop() at #1
3: f3(x = 3) at #2
2: f2(x = 2) at #1
1: f1()

上面例子在用 stop() 产生出错信号时显示调用栈,下面的函数调用了上面的函数。

可以用 lobstr::cst() 显示函数的调用栈。
ps: lobstr 包还挺有意思的。

> f <- function(x) g(x)
> g <- function(x) h(x)
> h <- function(x) x
> f(cst())
    x
 1. +-global::f(cst())
 2. | \-global::g(x)
 3. |   \-global::h(x)
 4. \-lobstr::cst()

当调用时有懒惰求值时,调用栈就可能有多个分支。(这个分支还是不太明白)

调用栈中的每一次调用称为一个框架,或求值上下文 (evaluation context)。框架是支持 R 语言的重要成分,R 程序仅能对框架数据结构作很少的操作。每个框架有三个构成部分:
• 一个表示调用函数的表达式 expr。
• 一个环境,通常是调用的函数的运行环境。但是,全局框架的环境还是全局环境,使用eval() 会造出一个框架,其环境则是可以由用户干预的。
• 父框架,即调用它的框架。

R 采用句法作用域,即由定义决定变量作用域,有少数语言如 Lisp 采用动态作用域 (dynamic scoping),即在调 用栈上查找变量值。

将环境作为一般数据结构

环境中的变量都是引用,或者绑定,不需要制作环境副本,这使得环境可以当作一种高级的数据类型使用。
将环境作为一般数据结构使用,可以用在如下一些方面:
• 因为不需要制作副本,所以可以节省内存空间。但是直接使用环境不够友好,可以使用 R6 类型的数据,R6 类型是建立在环境的基础上的。
• 在自己建立的扩展包中,用环境保存包的状态。这样一个包中的函数多次调用时,可以在多次调用之间传递 一些状态信息。可以在包中用 get.xxx() 函数和 set.xxx() 函数提供包用户访问状态的接口函数。
• 环境可以当作一个杂凑表用,杂凑表可以在常数时间将名字对应到值。