R语言编程 第一讲 变量与赋值
R語言編程 第一講 變量與賦值
- R語言的變量名
- 賦值符號 <- 與 = 的區別
- 賦值符號 <- 的更多細節
- Copy-on-Modify與Modify-in-Place
- 函數調用
- 列表
- 數據框(Dataframe)
- 字符
- 存儲空間
這個系列將系統性介紹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] <chr> +-[2:0x143e34cd7a0] <string: "a"> +-[2:0x143e34cd7a0] +-[3:0x143e62f5830] <string: "abc"> \-[4:0x143e369d0a0] <string: "d">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的指針重復了三次,因此增加的內存消耗就是一個空列表的內存。并且內存計算有一個神奇的規律:
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總結
以上是生活随笔為你收集整理的R语言编程 第一讲 变量与赋值的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 组合恒等式7 组合变换的互逆公式 简介与
- 下一篇: UA MATH563 概率论的数学基础I