R语言入门学习笔记(四)
前言
这一节,我们将来学习R的环境系统。R的环境系统在使用中是比较无感的,但是学习R的环境相关知识,可以更加清楚的了解R对对象的存储、查找、操作等逻辑。
一、环境
1.环境简介
R环境(environment)也可以看作是文件夹的概念(与python编程语言中的环境类似),与计算机存储文件的逻辑类似。计算机中文件夹层层嵌套,形成了一个分层的文件系统,如果想要找到某个文件,就必须在这个文件系统中逐层进行寻找。
R存储对象也是类似,每个对象存储在一个环境中,每个环境都与予个父环境相连,父环境是高一层级的环境,就这样父子环境就构成了一个分层的环境系统。
R中pryr
包的parenvs
函数可以查看R的环境系统。`parenvs(all = TRUE)会返回当前会话的环境列表,返回结果取决于加载的R包。下面是当前我的会话返回的输出结果
library(pryr)
parenvs(all = TRUE)
#> label name
#> 1 <environment: R_GlobalEnv> ""
#> 2 <environment: package:pryr> "package:pryr"
#> 3 <environment: 0x000001845f08c278> "tools:rstudio"
#> 4 <environment: package:stats> "package:stats"
#> 5 <environment: package:graphics> "package:graphics"
#> 6 <environment: package:grDevices> "package:grDevices"
#> 7 <environment: package:utils> "package:utils"
#> 8 <environment: package:datasets> "package:datasets"
#> 9 <environment: package:methods> "package:methods"
#> 10 <environment: 0x000001845c9858f0> "Autoloads"
#> 11 <environment: base> ""
#> 12 <environment: R_EmptyEnv> ""
这一结果中,最底层的环境是R_GlobalEnv,它存储在名为package:pryr的环境中,可以将其想象成文件系统的结构。而package:pryr的父环境名为0x000001845f08c278,以此类推,最高层级环境是R_EmptyEnv,这是R中唯一没有父环境的环境。
使用文件系统类比只是为了更好的理解,R环境实际是存储在内存中的。并且,R环境之间的关系也并不是嵌套关系,每个环境都与一个父环境相连,这样形成的树形结构非常易于检索对象。但这样的连接是单向的,我们可以轻而易举地知道某个环境的父环境是什么,但却无法直接找到一个环境的子环境。R地树形结构不支持自上而下搜索。
2.调用R环境的函数
R中提供了一些函数来对R环境进行操作。首先,as.environment
函数接受一i个环境名称(字符串)作为输入,返回其对应的环境。注意这个返回值是环境。
as.environment("package:stats")
#> <environment: package:stats>
#> attr(,"name")
#> [1] "package:stats"
#> attr(,"path")
#> [1] "C:/Program Files/R/R-4.3.2/library/stats"
环境树种存在三个环境有自己的调用函数。它们是:全局环境(R_GlobalEnv)、基础环境(base)和空环境(R_EmptyEnv)。可以使用下列命令进行调用这些环境。
globalenv()
#> <environment: R_GlobalEnv>
baseenv()
#> <environment: base>
emptyenv()
#> <environment: R_EmptyEnv>
可以使用parent.env()
调用某个环境的父环境。(注意空环境没有父环境)
parent.env(globalenv())
#> <environment: package:pryr>
#> attr(,"name")
#> [1] "package:pryr"
#> attr(,"path")
#> [1] "C:/Users/lmy/AppData/Local/R/win-library/4.3/pryr"
ls
或ls.str
函数可以查看存储在某个环境中的R对象。ls只返回对象名,ls.str会返回对象的结构。查看到某个对象后,可以使用环境$对象名
来提取环境中的这个对象。
如果想要将某个对象存储至某个环境中,可以使用assign()
函数,它类似于赋值函数<-
:
# assign(对象名, 对象取值, envir = 指定环境)
assign("new", "CSDN_Rrumen", envir = globalenv())
globalenv()$new
#> [1] "CSDN_Rrumen"
3.活动环境
任何时候,R的活动环境只有一个。所有的新对象都会被存储在该环境中,并且搜索对象时,R也会优先搜索该环境,这个环境被称为活动环境(active environment)。通常来说活动环境是全局环境,但是当运行函数时,活动函数可能会改变。
environment()
函数可以查看当前的活动环境。
environment()
#> <environment: R_GlobalEnv>
R中,命令行中运行的所有命令都是在全局环境中进行的,因此创建的对象会存储在该环境中。
二、环境中值的操作
1.作用域规则
在搜索对象时,R会遵循一系列的规则,这些规则被称为R的作用域规则(scoping rules)。即:
(1)R首先在当前活动环境搜索对象
(2)当前活动环境不存在该对象,R会在该环境的父环境中搜索该对象
(3)活动环境的父环境还没有该对象,R会在该父环境的父环境进行搜索,依次类推,直到空环境(顶层)搜索才会停止。如果都找不到,会返回一条错误信息。
注意:函数也是一种R对象。比如parenvs函数就存储在"package:pryr"环境中。
2.赋值
经过前面的学习我们知道,赋值操作会发生在当前的活动环境。多次对同一个对象进行赋值操作时,R对象的值就会发生覆盖。
那么来看R函数,如果我们创建的R函数,需要创建一些临时对象来承担一些任务,那么该对象是否会替换掉原来活动环境中存在的同名对象呢?R为了避免这种事情的发生,在每次运行函数(注意是运行,也就是调用函数时,而不是存储该函数时)时都会创建一个新的活动环境,函数的运行都是在这个新环境下进行的。
3.函数调用
R的每一次函数调用都会创建一个新环境。每当R调用该函数时就会在这个新环境中进行,然后带着函数运行的结果回到调用该函数时的环境。我们将这个R调用函数时创建的环境称为运行时环境(runtime environment,运行函数时的活动环境)。
接下来让我们用下面的函数来探索一个R的运行时环境,它将向我们展示这个环境,以及它的父环境的相关信息。
show_env <- function(){
list(run = environment(),
parent = parent.env(environment()),
objects = ls.str(environment())
)
}
show_env本身也是个函数,调用它时,R会创建一个运行时环境来运行函数。它的输出结果会告诉我们相关信息。
show_env()
#> $run
#> <environment: 0x000001845d2947a8>
#>
#> $parent
#> <environment: R_GlobalEnv>
#>
#> $objects
如果再次运行该函数,会发现运行时环境就不一样了。这时因为每运行一次函数,R都会创建一个新环境。
我们来看函数的父环境是什么呢?show_env
函数是全局环境。R会将一个函数的运行时环境与*第一次创建该函数(写下这个函数的时候)*时所在的环境相连接。以后每次调用运行该函数,它们的父环境都是这个函数创建时所在的环境,我们将它称为原环境(origin environment)。可以通过envirnoment
函数来查看一个函数的原环境(其实也就是看函数这个R对象所在的环境,即创建它的环境)。
environment(show_env)
#> <environment: R_GlobalEnv>
show_env的原环境是全局环境。但是并不一定所有的函数的原环境都是全局环境。像平时自己在命令行自定义的函数,是在全局环境中创建的,它们的原环境就是全局环境。像我们提到的parenvs
函数的原环境就是pryr包了。
接下来看看show_env函数运行时环境中包含的对象,由于没有在该函数中添加对象,所以它的运行时环境的对象是空的。如果函数中有对象存在,他就会被存储在运行时环境中,所以并不会和其他环境中的R对象发生冲突。如果函数带有参数,R会在运行时环境中为每个参数制作一个副本。参数会在运行时环境中以对象的形式村子啊,并且对象的名称就是参数名,但取值是用户提供的值。
arg <- "我是一个参数"
show_env <- function(x = arg){
list(run = environment(),
parent = parent.env(environment()),
objects = ls.str(environment())
)
}
show_env()
#> $run
#> <environment: 0x0000018461bd4598>
#>
#> $parent
#> <environment: R_GlobalEnv>
#>
#> $objects
#> x : chr "我是一个参数"
现在,总结一下R是怎么调用函数的:首先一个函数是在某个环境中被定义的,在调用函数前,R是在活动环境中工作的,我们暂且叫它调用环境。R在该环境中调用这个函数时,R会创建一个新的运行时环境,这个环境是函数原环境的子环境。R会将函数的参数复制到运行时环境,这时这个运行时环境就是当前的活动环境。当函数运行结束时,R会将活动环境切回到调用环境。如果将函数运行结果用赋值符<-
赋给某个对象,那么这个新对象就会存储在调用环境中了。
4.闭包
全局环境作为用户操作大多数情况下的活动环境是非常活跃的,里面可能会发生许多读写操作,有许多元素都可能被不小心修改或者删除。那么如果能够将某些重要信息存储在一个安全,不易触碰的地方,如R运行函数时创建的运行时环境,就不会这样了。
比如:
setup <- function(x){
X <- x
show_env <- function(){
x <- X
list(run = environment(),
parent = parent.env(environment()),
objects = ls.str(environment())
}
}
这样当运行setup函数时,R会创建一个运行时环境,用于存储该函数产生的所有对象【x的值以及show_env函数(随时记得函数也是R对象的一种)】。
现在所有对象都被安全地放在了全局环境的一个子环境中了。这样做保证安全但不利于利用,最佳的办法就是使用列表修改代码,让函数返回一个列表用于调用内部对象。
setup <- function(x){
X <- x
Show_env <- function(){
x <- X
list(run = environment(),
parent = parent.env(environment()),
objects = ls.str(environment()))
}
list(x = X, show_env = Show_env)
}
x <- "HELLO"
safe <- setup(x)
然后可以将所需使用的对象保存在全局环境下的专用对象中:
show_env <- safe$show_env
show_env
#> function(){
#> x <- X
#> list(run = environment(),
#> parent = parent.env(environment()),
#> objects = ls.str(environment()))
#> }
#> <bytecode: 0x000001845d22db58>
#> <environment: 0x000001845f719900>
environment(show_env)
#> <environment: 0x000001845f719900>
可以看到它与原来的函数有一个差别,就是它的原环境不再是全局环境(虽然show_env对象存储在全局环境中),而是R运行setup函数时创建的运行时环境。就是创建Show_env(原本show_env副本)的地方。
这样的处理当时称为闭包(closure)。setup的运行时环境将show_env
函数包了起来。这个环境不在任何R函数或环境的搜索路径上。如果在闭包内对x进行操作,但不想影响到全局变量中的x,那么可以在使用assign
函数进行赋值时,将环境选项设定为envir = parent.env(environment())
即可。
总结
学习了该节的内容,我们对R环境有了一个基本的了解。环境类似于计算机的文件系统,发挥想象取理解其内部的环境结构。为了操作环境,我们学习了一些调用环境的函数。在环境中对数据进行操作时,R遵守搜索的作用域规则,这将我们之前对于数据存储与检索的认知拔高到了环境的层面,我们可以更清晰地认识到数据的调用与存储的形式。另外,了解了环境,我们也更加清楚了函数的作用方式,利用函数调用时所创建的运行时环境,我们也可以将部分R对象封装到这个环境中来保护这些对象不被破坏。最后,需要明白的是R环境是在内存中存储的,要随时注意环境的变化,在对R对象进行赋值时要理清楚所在的环境。
如果是第一次接触环境的概念,初学本节内容可能会有些吃力。我的建议是,对于其中的概念要自己梳理清楚,可以对照着打开RStudio操作试试,然后在平时使用R的时候去分辨一下R对象处于的环境,其实作为一个使用者,常用的环境就是全局环境和函数运行时创建的环境。随着进一步的学习深入,日后再来看这篇文章,可能会豁然开朗。