前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >124-R编程18-R的内部机制2

124-R编程18-R的内部机制2

作者头像
北野茶缸子
发布2022-05-19 11:31:26
6150
发布2022-05-19 11:31:26
举报
文章被收录于专栏:北野茶缸子的专栏
  • 参考:
    • R的内部机制 - 王诗翔 (shixiangwang.github.io)[1]
    • 19 函数进阶 | R语言教程 (pku.edu.cn)[2]
    • 09. 工作空间和变量赋值 · 语雀 (yuque.com)[3]

前言

其实之前读了李东风老师的内容,感觉收获颇丰;但因为自己的业务逻辑过于简单,渐渐又荒废掉了。

最近碰巧看到王诗翔的这篇文章,再次学习,顺便将笔记从yuque 发至公众号。共勉。

因为内容有些多,拆成两个部分介绍。

前文:[[113-R编程16-R的内部机制1]]

第二部分:

  • 复制-修改机制 (Copy-on-modify mechanism)
  • 环境 (Environment)

复制-修改机制

介绍

R 的变量赋值类似python,并不是像C++、JAVA等语言那样, x代表某个存储位置, “x <- c(1,2,3)”代表将1到3这些值存储到x所指向的存储位置。

<-右边的c(1,2,3)是一个表达式, 其结果为一个R对象(object), 而x只是一个变量名, 并没有固定的类型、固定的存储位置, 赋值的结果是将x绑定到值为(1,2,3)的R对象上。R对象有值,但不必有对应的变量名;变量名必须经过绑定才有对应的值和存储位置。

我们可以通过变量获得对象所在的地址(存储位置),并获得对象的值。

类似py 中的id,我们也可以通过tracemem 追踪变量指向的对象在内存中的位置:

代码语言:javascript
复制
x <- c(1,2,3)
cat(tracemem(x), "\n")
## <0000000018288290>
y <- x    # 这时y和x绑定到同一R对象
cat(tracemem(y), "\n")
## <0000000018288290>
y[3] <- 0 # 这时y制作了副本
## tracemem[0x0000000018288290 -> 0x00000000183c5190]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous> do.call eval eval eval eval eval.parent local
x
## [1] 1 2 3
y
## [1] 1 2 0
untracemem(x); untracemem(y)

当使用tracemem 追踪变量后,如果变量绑定的对象发生了变化,会将变化结果输出到屏幕,如果不希望继续追踪,可以使用函数untracemem。

上面操作不难发现,这两个向量值相同,并共享内存地址,说明它们指向相同的数据,而赋值操作并没有自动复制数据。

但当我们对其中一个变量进行修改之后,其立刻制作了副本。

对于调用函数时内部变量的赋值,同样存在这样的“复制-修改机制”:

代码语言:javascript
复制
x <- c(1,2,3)
cat(tracemem(x), "\n")
## <0000000018DA05A0>
f <- function(v){ return(v) }
z <- f(x)
cat(tracemem(z), "\n")
## <0000000018DA05A0>
untracemem(x); untracemem(z)

如果修改了内部变量的元素的值,则会制作副本,并将修改的副本输出:

代码语言:javascript
复制
x <- c(1,2,3)
cat(tracemem(x), "\n")
## <000000001931EE70>
f2 <- function(v){ v[1] <- -999; return(v) }
z <- f2(x)
## tracemem[0x000000001931ee70 -> 0x00000000193d8880]

其他副本创建类型

列表

如果x是一个有5个元素的列表, 则y <- x使得y和x指向同一个列表对象。但是, 列表对象的每个元素实际上也相当于一个绑定, 每个元素指向一个元素值对象。

代码语言:javascript
复制
> a1 <- list(1,2,3,4,5)
> cat(tracemem(a1), "\n")
<0x7fe4e05068e8> 
> a2 <- a1
> a2[[1]] <- 3
tracemem[0x7fe4e05068e8 -> 0x7fe4e03180a8]:

所以如果修改y:y[[3]] <- 0, 这时列表y首先被制作了副本, 但是每个元素指向的元素值对象不变, 仍与x的各个元素指向的对象相同;然后, y[[3]]指向的元素值进行了重新绑定, 不再指向x[[3]], 而是指向新的保存了值0的对象, 但y的其它元素指向的对象仍与x公用。

代码语言:javascript
复制
> do.call("cat", lapply(a1, tracemem))
<0x7fe4bec9c760> <0x7fe4bec9c798> <0x7fe4bec9c7d0> <0x7fe4bec9c808> <0x7fe4bec9c840>
> do.call("cat", lapply(a2, tracemem))
<0x7fe4c07f75c0> <0x7fe4bec9c798> <0x7fe4bec9c7d0> <0x7fe4bec9c808> <0x7fe4bec9c840>

列表的这种复制方法称为浅拷贝, 表格对象及各个元素绑定被复制, 但各个元素指向(保存)的对象不变。这种做法节省空间也节省运行时间。

在R的3.1.0之前则用的深拷贝方法, 即复制列表时连各个元素保存的值也制作副本。

其实这里还可以使用函数lobstr::ref, 返回对象及其内部元素的内存地址:

代码语言:javascript
复制
> lobstr::ref(y,y1)
o [1:0x7fd896f93558] <list> 
+-[2:0x7fd89809f840] <dbl> 
+-[3:0x7fd89809f878] <dbl> 
\-[4:0x7fd89809f8b0] <dbl> 
 
o [5:0x7fd896eec6e8] <list> 
+-[6:0x7fd897d9f088] <dbl> 
+-[7:0x7fd897d9f0c0] <dbl> 
\-[8:0x7fd897d9f0f8] <dbl> 

数据框

其实在R 的内部机制中,数据框和列表并没有什么明显的区别:

只不过从操作上,我们可以对不同列表的相同位置的数据进行同时处理(行操作)。

数据框的每一列实际绑定到一个对象上。如果y <- x, 则修改y的某一列会对y进行浅拷贝, 然后仅该列被制作了副本并被修改, 其它未修改的列仍与x共用值对象。

但是如果修改数据框y的一行, 因为这涉及到所有列, 所以整个数据框的所有列都会制作副本。

环境

环境是一组名称组成的对象。对于R 来说,环境作为一个数据结构与有名的列表相似。

★当我们查找一个符号(变量)时,如果它在当前环境中,R就会在当前环境中搜索并返回该符号指向的对象。如果这个符号在当前环境中没有找到,R就会到它的父环境中搜索。 ”

环境有以下特点:

  • 环境中的数据名称必须互不相同;
  • 环境中的变量没有次序;
  • 环境(除了空环境)都有一个父环境;
  • 修改环境内容时,不会制作副本。

创建环境

环境的创建和打印,操作也和列表对象非常相似。

我们使用new.env()函数创建一个新环境:

代码语言:javascript
复制
e1 <- new.env()

e2 <- rlang::env(
  a = 3,
  b = TRUE,
  c = 5
)

或者用rlang::env()生成新的环境同时,定义环境中的数据。

如果我们在环境中定义的名字相同,则会将之前的名称覆盖:

代码语言:javascript
复制
e3 <- rlang::env(
  a = 4,
  a = 2
)

> e3$a
[1] 2

我们打印环境,会输出十六进制数表示的内存地址:

代码语言:javascript
复制
> e2
<environment: 0x7fe51994e4b0>
> e3
<environment: 0x7fe509e9d668>

rlang包的env_print()函数可以给出较多的信息:

代码语言:javascript
复制
> rlang::env_print(e2)
<environment: 0x7fe51994e4b0>
parent: <environment: global>
bindings:
 * a: <dbl>
 * b: <lgl>
 * c: <dbl>
> rlang::env_print(e3)
<environment: 0x7fe509e9d668>
parent: <environment: global>
bindings:
 * a: <dbl>

环境的特点

上面说过,环境的特点。这里主要展开介绍以下三点:

  • 环境被修改时,并不会制作副本;
  • 环境不存在索引;
  • 除空环境外,环境都具有父环境;

修改不复制

在先前的复制修改机制中,我们提到:

代码语言:javascript
复制
x <- c(1,2,3)
cat(tracemem(x), "\n")
## <0000000018288290>
y <- x    # 这时y和x绑定到同一R对象
cat(tracemem(y), "\n")
## <0000000018288290>
y[3] <- 0 # 这时y制作了副本
## tracemem[0x0000000018288290 -> 0x00000000183c5190]

而如果是环境。因为变量对应的环境指向的是同一个内存,修改任意其中一个环境中的变量,均发生修改:

代码语言:javascript
复制
e3 <- rlang::env(
  a = 4,
  a = 2
)

e4 <- e3

> e3$b
NULL
> e3$b <- 5
> e4$b
[1] 5

环境不存在索引

前面说过,环境的创建、访问、修改操作,都和list 很像,但是,环境没有索引,因此也不能构建和提取子集。

代码语言:javascript
复制
e1[1:3] #索引 
#> Error in e1[1:3]: object of type 'environment' is not subsettable 

e1[[1]] #构建子集 
#> Error in e1[[1]]: wrong arguments for subsetting an environment

除了通过$[[访问,如何知道环境中的变量呢?

代码语言:javascript
复制
e2 <- rlang::env(
  a = 3,
  b = TRUE,
  c = 5
)

> exists("a", e2)
[1] TRUE
> get("a", e2)
[1] 3
> exists("x", e2)
[1] FALSE
> get("x", e2)
Error in get("x", e2) : 找不到对象'x'

还可以调用ls()列出环境中的所有变量:

代码语言:javascript
复制
> ls(e2)
[1] "a" "b" "c"
> ls()
 [1] "args"           "dataset"        "e1"            
 [4] "e2"             "e3"             "e4"            
 [7] "params"         "parser"         "sce"           
[10] "sim"            "sim_discrete"   "sim_trajectory"
[13] "step1"          "tmp"            "tmp1"          
[16] "tmp11"          "tmp2"     

ps:这个ls()也就是输出当前所在环境中的全部变量了。

父环境

当我们查找一个符号(变量)时,如果它在当前环境中,R就会在当前环境中搜索并返回该符号指向的对象。如果这个符号在当前环境中没有找到,R就会到它的父环境中搜索。

我们可以在创建环境时指定它的父环境:

代码语言:javascript
复制
e2 <- new.env(parent = e1)

这里我们将e1设定为e2的父环境,那么e2的父环境的内存地址应该和e1一致:

代码语言:javascript
复制
> e1;parent.env(e2)
<environment: 0x7fe5036d6ef0>
<environment: 0x7fe5036d6ef0>

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

同样,一个变量如果在环境中没有找到,就会在其对应的父环境中查找,如果不想让函数搜索父环境,可以设定inherits = FALSE

小小的总结

个人觉得,大部分生信相关的工作,或者说是数据科学从业者的工作,应该都使用不到这些更深入的编程思想。但是,如果你是一个对自己编程能力有更高追求的人,亦或是想进军面向对象的海洋,通过R 这门傻瓜式语言探一探编程海洋的大门也是不错的。

ps:关于面向对象更深的学习,个人还是不建议以R来学习它们,包括S4或S6 这些高级对象。可以先通过py 或java 这些较为成熟且自成体系的编程语言,再按照自己的业务需求写R 的面向对象,或是R 包开发,会好一些。

当然,我也只是一个小小的菜鸟,听听就好。

参考资料

[1]

R的内部机制 - 王诗翔 (shixiangwang.github.io): https://shixiangwang.github.io/home/cn/post/2019-11-20-r-mechanism/

[2]

19 函数进阶 | R语言教程 (pku.edu.cn): https://www.math.pku.edu.cn/teachers/lidf/docs/Rbook/html/_Rbook/p-advfunc.html#p-advfunc-lazy

[3]

09. 工作空间和变量赋值 · 语雀 (yuque.com): https://www.yuque.com/mugpeng/rr/ebhayr#E2tnY

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 北野茶缸子 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 复制-修改机制
    • 介绍
      • 其他副本创建类型
        • 列表
        • 数据框
    • 环境
      • 创建环境
        • 环境的特点
          • 修改不复制
          • 环境不存在索引
          • 父环境
          • 参考资料
      • 小小的总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档