其实之前读了李东风老师的内容,感觉收获颇丰;但因为自己的业务逻辑过于简单,渐渐又荒废掉了。
最近碰巧看到王诗翔的这篇文章,再次学习,顺便将笔记从yuque 发至公众号。共勉。
因为内容有些多,拆成两个部分介绍。
前文:[[113-R编程16-R的内部机制1]]
第二部分:
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 追踪变量指向的对象在内存中的位置:
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。
上面操作不难发现,这两个向量值相同,并共享内存地址,说明它们指向相同的数据,而赋值操作并没有自动复制数据。
但当我们对其中一个变量进行修改之后,其立刻制作了副本。
对于调用函数时内部变量的赋值,同样存在这样的“复制-修改机制”:
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)
如果修改了内部变量的元素的值,则会制作副本,并将修改的副本输出:
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指向同一个列表对象。但是, 列表对象的每个元素实际上也相当于一个绑定, 每个元素指向一个元素值对象。
> 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公用。
> 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, 返回对象及其内部元素的内存地址:
> 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()
函数创建一个新环境:
e1 <- new.env()
e2 <- rlang::env(
a = 3,
b = TRUE,
c = 5
)
或者用rlang::env()
生成新的环境同时,定义环境中的数据。
如果我们在环境中定义的名字相同,则会将之前的名称覆盖:
e3 <- rlang::env(
a = 4,
a = 2
)
> e3$a
[1] 2
我们打印环境,会输出十六进制数表示的内存地址:
> e2
<environment: 0x7fe51994e4b0>
> e3
<environment: 0x7fe509e9d668>
rlang包的env_print()
函数可以给出较多的信息:
> 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>
上面说过,环境的特点。这里主要展开介绍以下三点:
在先前的复制修改机制中,我们提到:
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]
而如果是环境。因为变量对应的环境指向的是同一个内存,修改任意其中一个环境中的变量,均发生修改:
e3 <- rlang::env(
a = 4,
a = 2
)
e4 <- e3
> e3$b
NULL
> e3$b <- 5
> e4$b
[1] 5
前面说过,环境的创建、访问、修改操作,都和list 很像,但是,环境没有索引,因此也不能构建和提取子集。
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
除了通过$
或[[
访问,如何知道环境中的变量呢?
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()
列出环境中的所有变量:
> 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就会到它的父环境中搜索。
我们可以在创建环境时指定它的父环境:
e2 <- new.env(parent = e1)
这里我们将e1设定为e2的父环境,那么e2的父环境的内存地址应该和e1一致:
> 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