
这个系列将系统性介绍R语言的理论与实践,R语言是专注应用统计与数据分析领域的最热门的开源语言,兼具函数编程与面向对象编程的特点。R语言的使用门槛非常低,如果只是用来估计特定模型,那么只需要输入输出会调包就可以了,但总要有人去写以及优化这些包,所以我们在使用R语言之前,有必要系统性学习一下R语言编程。
这个系列会先介绍一些R语言编程的基础,比如变量、向量、向量切片、分支与循环、函数、可视化等,然后再分别介绍R语言的函数编程与面向对象编程的内容,最后介绍元编程与其他编程技巧。希望接下来的这个学年能够完成这个系列!
R语言的变量名例1 奇怪但正确的变量名
> .c = 3 > `_fff` = 5 > `for` = 3 > for = 3 Error: unexpected '=' in "for =" > _fff = 5 Error: unexpected input in "_"赋值符号 <- 与 = 的区别
R语言中 <- 与 = 都是非常常用的赋值符号,但它们是有很大的区别的:
例2 <- 与 = 的区别
library(lobstr) # 使用lobstr包可以分析R语言运行的细节 x <- 1 y <- x obj_addr(x) obj_addr(y) a = 1 b = 1 obj_addr(a) obj_addr(b)
第一段代码创造了一个取值为1的对象,然后让变量x指向这个对象;并且让变量y指向变量x指向的地址,于是变量y也指向了这个对象,函数obj_addr()可以返回一个对象的地址,输出为
obj_addr(x)
[1] “0x2e164f19ae0”
obj_addr(y)
[1] “0x2e164f19ae0”
这说明变量x与变量y指向同一个地址,也就是取值为1的这个对象的地址。
第二段代码将1赋值给变量a,然后将1赋值给变量b,这两个1是分别创造的两个不同的对象,因此函数obj_addr()的输出为
obj_addr(a)
[1] “0x2e1663493b8”
obj_addr(b)
[1] “0x2e166349348”
这说明变量a与变量b并没有指向同一个地址。
大家可以自行尝试,把第一段代码中的y <- x改为y = x,效果和第二段代码就是一样的。
赋值符号 <- 的更多细节 Copy-on-Modify与Modify-in-Place例3 Copy-on-Modify vs Modify-in-Place
x <- c(1,2,3,4) y <- x y[[2]] <- 1 x obj_addr(x) y obj_addr(y)
我们先来分析这段代码,首先用c()创造了一个向量对象,并让变量x指向这个对象,然后让变量y也指向这个对象;接下来把变量y指向的对象中的第二个位置的值改为1,神奇的事情就发生了!我们来看一下输出
> x [1] 1 2 3 4 > obj_addr(x) [1] "0x2e16fcc0558"
这两行的输出说明x指向的对象取值没有变;
> y [1] 1 1 3 4 > obj_addr(y) [1] "0x2e16fcc0648"
这两行的输出说明y指向了一个新的对象,并且新的对象取值变了。这个现象的学名叫做Copy-on-Modify,它其实是由R语言的一种保护机制所导致的,因为变量x和y指向同一个对象,为了避免对变量y做的修改引起变量x的变化,R语言实质上复制了一个取值相同的对象,修改这个对象的取值,然后让变量y指向这个新的对象,原来的对象会被Garbage Collector收集起来删除释放内存。这种保护机制的好处在于它可以避免同时修改指向同一个对象的变量,缺点在于增加了运算需要的时间(复制+修改+重新指向)。大家可以自行尝试,把 y[[2]] <- 1 改为 y[[2]] = 1 也会出现Copy-on-Modify的现象。
x <- c(1,2,3,4) cat(tracemem(x), "n") y <- x y[[2]] <- 1 untracemem(x)
这一段代码可以帮助我们更清楚地理解地址的变化,tracemem()的作用是追踪内存地址的变换,untracemem()的作用是停止追踪。cat()的作用就是输出一个对象,"n"表示换行输出。我们先分析第一个输出:
> x <- c(1,2,3,4) > cat(tracemem(x), "n") <000002E16F72B890>
这个是第一条语句创造的向量对象的内存地址;下面看第二条输出,
> y <- x y[[2]] <- 1 tracemem[0x000002e16f72b890 -> 0x000002e16f728310]:
这个输出表示在执行y[[2]] <- 1,R语言在0x000002e16f728310这个位置复制了一个位于0x000002e16f72b890的对象,然后把位于0x000002e16f728310的对象第二个元素改为1,最后让y指向0x000002e16f728310。
x <- c(1,2,3,4) y <- x obj_addr(y) y[[2]] <- 1 obj_addr(y) y[[3]] <- 1 obj_addr(y)
下面分析这一段代码,如果在修改了一次之后再修改一次变量y的取值,会发现第二次修改后变量y的例子就不会再发生改变了:
> x <- c(1,2,3,4) > y <- x > obj_addr(y) [1] "0x2e16f724ca0" > y[[2]] <- 1 > obj_addr(y) [1] "0x2e16f724e30" > y[[3]] <- 1 > obj_addr(y) [1] "0x2e16f724e30"
原因很简单,现在指向对象0x2e16f724e30的变量只有y一个,因此对变量y的取值的修改是直接在内存位置0x2e16f724e30进行的,这种现象叫做Modify-in-Place。
Copy-on-Modify比Modify-in-Place需要更多的时间,在优化代码的时候可以尝试规避用不同的变量指向同一个对象。
函数调用当函数调用某一个变量的时候,传递到函数中的是指针而不是值。
例4 R语言函数调用时进行指针传递
f <- function(a){
a
}
x <- c(1,2,3,4)
cat(tracemem(x), "n")
z1 <- f(x)
z2 = f(x)
untracemem(x)
y = c(1,2,3,4)
cat(tracemem(y), "n")
w <- f(y)
untracemem(y)
执行第一段代码,tracemem()没有监测到内存地址的变化,因此函数f引用的是变量x的指针,而不是赋值的变量x的值,并且这个性质与把函数值赋值给另一个变量时用的方式无关;执行第二段代码会发现同样的结论,说明这个性质与被引用对象的赋值方式无关。
列表R语言的列表比上面的数值和向量更为复杂,比如
L1 <- list(1,2,3,4)
这个语句实际上执行了三步操作:
也就是说列表中装的是地址而不是值!因此变量L1指向一个地址,它的四个元素分别指向四个不同的地址:
> obj_addr(L1) [1] "0x143ec3855a0" > obj_addr(L1[[1]]) [1] "0x143ed747f08" > obj_addr(L1[[2]]) [1] "0x143ed747ed0" > obj_addr(L1[[3]]) [1] "0x143ed747e98" > obj_addr(L1[[4]]) [1] "0x143ed747e60"
修改列表中某个元素的值时实质上是创建一个新对象,然后更改列表对应位置的地址。实际上R语言中用c()定义的向量也具有类似的性质,不同元素是存在不同的地址的:
> x <- c(1,2,3,4) > obj_addr(x) [1] "0x143ec843630" > obj_addr(x[[1]]) [1] "0x143ed8be8d0" > obj_addr(x[[2]]) [1] "0x143ed8be6a0" > obj_addr(x[[3]]) [1] "0x143ed8be470" > obj_addr(x[[4]]) [1] "0x143ed8be240"数据框(Dataframe)
R语言中的数据框结构比列表更复杂,但我们可以简单理解为它是列表的列表,并且每一个位置存的都是对应元素的地址。
> D1 <- data.frame(x=c(1,2,3,4),y=c(1,2,3,4)) > obj_addr(D1) [1] "0x143ec2e3950" > obj_addr(D1$x) [1] "0x143ecc4e148" > obj_addr(D1$y) [1] "0x143ecc4de28" > obj_addr(D1$x[[1]]) [1] "0x143ed911550" > obj_addr(D1$y[[1]]) [1] "0x143ed9112b0"
创建数据框对象时,执行了下面的操作:
R语言有一个global string pool,所有的英文字符和简单字符串保存在其中,因此字符型的变量或列表的指针是指向global string pool中某一个字符的。
> x <- c("a", "a", "abc", "d")
> ref(x, character = TRUE)
o [1:0x143ed294f28]
+-[2:0x143e34cd7a0]
+-[2:0x143e34cd7a0]
+-[3:0x143e62f5830]
-[4:0x143e369d0a0]
ref()函数的作用是返回字符型变量引用的global string pool中的字符的地址,从上面的结果来看,相同的字符"a"在global string pool中其实是同一个对象,不同的字符在global string pool是单独存放的。
存储空间使用obj-size()可以查看一个对象的大小。得益于R语言赋值采用指针的形式,重复引用一个对象多次并不会显著增加内存使用:
> x <- runif(1e6) > obj_size(x) 8,000,048 B > y <- list(x, x, x) > obj_size(y) 8,000,128 B > obj_size(list(NULL, NULL, NULL)) 80 B
在创建y时,实质上只是把指向x的指针重复了三次,因此增加的内存消耗就是一个空列表的内存。并且内存计算有一个神奇的规律:
> obj_size(x,y) 8,000,128 B
R语言存储还有一个很有趣的地方,当存一个步长为1的数列时,只用存第一项和最后一项:
> obj_size(1:3) 680 B > obj_size(1:300) 680 B > obj_size(1:30000) 680 B > obj_size(1:3000000) 680 B