R语言入门学习笔记(四)

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"

lsls.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对象处于的环境,其实作为一个使用者,常用的环境就是全局环境和函数运行时创建的环境。随着进一步的学习深入,日后再来看这篇文章,可能会豁然开朗。

  • 40
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值