本文對應《R語言編程藝術》
第7章:R語言編程結構;
第9章:面向對象的編程;
第13章:調試
=========================================================================
R語言編程結構
控制語句:
循環:
for (n in x) { } while (condition) { } repeat { }
另外break也可以用在另兩種形式的循環語句中。注意repeat沒有跳出循環的判斷條件,因此使用break(或者類似return())的語句。
除此之外,next語句可以用來跳過本次迭代的剩余部分。具體應用情景,是可以替代循環中的條件嵌套語句,不致代碼看起來混亂。
需要注意的是,for結構可以用在任何向量上,無論向量是什么模式,注意靈活運用。
對非向量集合的循環:R並不支持直接對非向量集合的循環,但是有一些間接但簡單的方式可以做到這點:
使用lapply()。如果循環的每次迭代之間相互獨立,就是用lapply(),可以允許以任意順序執行;
使用get()。這個函數接受一個代表對象名字的字符串參數,然后返回該對象的內容,看起來簡單,但是十分有用。
#假設u, v都是兩列的矩陣,要分別在這兩個矩陣上應用lm函數 for (m in c(“u”, “v”)) { z <- get(m) print(lm(z[, 2] ~ z[, 1])) }
if-else結構:
#注意以下代碼 if (r == 4) { x <- 1 } else { x <- 3 y <- 4 }
雖然if后面只有一條語句,但是它對應的大括號是不能省略的,原因在於R的解釋器是根據else前面的右括號判斷這是一個if-else結構,如果沒有大括號,R將認為這是一個if結構。
#if-else結構的簡化 v <- if (cond) expression1 else expression2
這是一個簡化代碼的技巧,因為if-else結構最終會返回的值取決於cond是否為真。如果expression是函數調用語句,可以讓代碼更為清晰,但是如果語句復雜,就要優先考慮代碼是否清晰易懂。
處理向量時,使用向量化的ifelse()函數有可能提高運行速度。
算術和邏輯運算符及數值:
之前提到,R中只有向量,沒有標量,標量可以看作是僅有一個值的向量。但是在邏輯運算時,特別是if語句的條件控制,其邏輯值不能為向量,只能有一個值,也就是可以看作標量。這時,就要區分邏輯與和邏輯或的向量標量運算了。
x && y |
標量的邏輯與運算 |
x || y |
標量的邏輯或運算 |
x & y |
向量的邏輯與運算 |
x | y |
向量的邏輯或運算 |
如果if語句里的控制條件里出現向量並且長度大於1,R的處理方式是將向量的第一個元素作為控制條件返回結果,然后提示warning message
參數的默認值:
具名實參(named argument):如果不使用默認值,那么在調用函數時必須給出參數的具體名稱;
惰性求值(lazy evaluation)原則:除非有需要,否則不會計算一個表達式的值,也就是說具名實參不一定會被使用。
返回值:
函數的返回值可以是任何R對象。通常為列表形式,但是如果有需要,甚至可以返回另一個函數。
一般來說,如果只在函數結尾返回值,要避免顯式地調用return()函數。如果想要將程序表達得更加清晰,在函數中間部分返回值的時候可以顯式地調用return()函數。
返回復雜對象:函數的返回值可以是另一個函數。如果函數需要返回多個返回值,可以把它們儲存在一個列表或其他容器變量中。
函數都是對象:
注意到函數是對象,也就是說對於對象的操作對函數也適用。包括創建、編輯,甚至在以函數組成的列表上做循環。
g <- function(h, a, b) h(a, b) body(g) <- quote(2 * x + 3) g(3) # 9
首先,function函數創建了一個函數,並賦值給g;然后body函數修改了g的函數體。因為函數主體部分屬於”call”類,而這種類是由quote()函數生成。
環境和變量作用域的問題:
頂層環境:<environment: R_GlobalEnv>
變量作用域的層次:里層的函數可以使用更加頂層的變量,如果層次之間變量命名發生沖突,優先使用最里層的變量。一般來說,里層對於更加頂層的變量只讀不可寫。可寫的情況將單獨討論。
ls()函數:調用不帶參數的ls()可以返回當前的局部變量;使用envir參數可以返回函數調用鏈中任何一個框架的局部變量名。
函數代碼對於其非局部變量一般只讀不寫,即使重新賦值也是影響它們的備份而不是變量本身。事實上,直到局部變量發生變化前,與其對應的全局變量是共享一個內存地址的。
R語言中沒有指針:
不能直接改變一個函數的參數,只能重新賦值。
其他因為沒有指針造成的問題將在下文討論。
向上級層次進行寫操作:
利用超賦值運算符對非局部變量進行寫操作:<<-
注意,超賦值運算符進行寫操作的時候,首先逐層向上級環境層次查找,遇到該變量的第一個層次即進行操作,如果沒有遇到,將在全局層次上新建變量。
用assign()函數對非局部變量進行寫操作:
可以通過設置函數參數指定向上層的哪一層次的變量進行寫操作。注意引用變量是使用字符串實現的。
全局變量的使用:一般來說不推薦在函數中寫全局變量,但是有時為了簡化代碼,這么做也是可以的,靈活運用即可。對於大型程序不推薦這樣做,因為存在着重寫不相關卻同名變量的風險。一種折中的方案是,在頂層環境中新建一個封裝全局變量的包,再使用assign()和get()函數通過索引到這個包來訪問全局變量。
閉包:這里的閉包指一種編程方法,具體做法是,先定義一個可創建局部變量的函數,再創建另一個函數可以訪問該變量。這樣,后面的一些函數創建的局部變量之間互不影響,各自獨立。
遞歸:
用於回歸和分類的遞歸分塊方法庫:rpart
置換函數:
任何左邊不是標識符(意味變量名)的賦值語句都可看作是“置換函數”。當遇到以下形式:
g(u) <- v
R語言會嘗試用以下方式執行:
u <- “g<-”(u, value = v)
直觀感受下,就是將值賦值給函數調用的結果。事實上,只要左邊不是變量名,就是一個置換語句,比如u[2] <- 8
寫函數代碼的工具:
快速載入環境:
source(“filename.R”)
編輯函數:
f1 <- edit(f1) f2 <- edit(f1)
創建自己的二元運算符:
> “%a2b%” <- function(a, b) return(a + 2 * b) > 3 %a2b% 5 [1] 13
匿名函數:
如果函數不是很復雜,可以省去函數定義的過程,在使用function()命令建立函數后直接調用,因為沒有具體的函數名,因此稱為匿名函數。應用情況取決於具體情況,可以使代碼更加易讀緊湊。
=========================================================================
面向對象的編程
- 能接觸到的R中所有東西(從數字到字符串到矩陣)都是對象
- R支持“封裝”(encapsulation),即把獨立但相關的數據項目打包為一個類的實例。封裝可以幫助你跟蹤相關的變量,提高清晰度
- R類是“多態”(polymorphic)的,這意味着相同的函數使用不同類的對象時可以調用不同的操作。多態可以促進代碼可重用性
- R允許“繼承”,即允許把一個給定的類的性質自動賦予為其下屬的更特殊化的類
S3類:
一個S3類包含一個列表,再附加上一個類名屬性和調度(dispatch)的功能。
S3泛型函數:即同一個函數可以針對不同的類調用不同的操作。可以通過method()函數找到給定泛型函數的所有實現方法,返回值中,星號標注的是不可見函數(nonvisible function),即不在默認命名空間中的函數,可以通過getAnywhere()函數找到這些函數。另外可以查看一個類能應用的所有泛型函數,代碼method(, class = “classname”)
編寫S3類:“類”屬性可以通過attr()或class()函數手動設置。然后可以針對自己編寫的類,設定獨有的泛型函數,下面是一個例子:
#創建一個名為employee的類的對象 j <- list(name = “Joe”, salary = 55000, union = T) class(j) <- “employee” #編寫類employee的打印方法 print.employee <- function(wrkr) { cat(wrkr$name, “\n”) cat(“salary”, wrkr$salary, “\n”) cat(“union member”, wrkr$union, “\n”) }
使用繼承:繼承的思想是在已有類的基礎上創建新的類:
#創建類employee的子類hrlyemployee k <- list(name = “Kate”, salary = 68000, union = F, hrsthismonth = 2) class(k) <- c(“hrlyemployee”, “employee”)
新類包含了兩個字符串,分別代表新類和原有的類,新類繼承了原有類的方法,可以使用對應的泛型函數。泛型函數在調度時的工作原理是首先查找兩個類名稱的第一個,如果沒查到,則再去查找第二個類,實現了繼承。
S4類:
操作 |
S3類 |
S4類 |
定義類 |
在構造函數的代碼中隱式定義 |
setClass() |
創建對象 |
創建列表,設置類屬性 |
new() |
引用成員變量 |
$ |
@ |
實現泛型函數f() |
定義f.classname() |
setMethod() |
聲明泛型函數 |
UseMethod() |
setGeneric() |
編寫S4類:
> #定義類employee > setClass(“employee”, + representation( + name = “character”, + salary = “numeric”, + union = “logical”) + ) [1] “employee” > #創建一個類的實例 > joe <- new(“employee”, name = “Joe”, salary = 55000, union = TRUE) > joe An object of class “employee” Slot “name”: [1] “Joe” Slot “salary”: [1] 55000 Slot “union”: [1] TRUE > #引用成員變量 > joe@salary [1] 55000 > slot(joe, “salary”) [1] 55000 > #賦值也可以使用這兩種方法 > joe@salary <- 88000 > slot(joe, “salary”) <- 88000
S4類的安全性在於,如果給不存在的成員變量賦值,會有報錯消息,而S3類將會新建一個成員變量。
> joe@salry <- 48000 Error in checkSlotAssignment(object, name, value) : “salry” is not a slot in class “employee”
在S4類上實現泛型函數:
#show()函數指在交互模式下,輸入變量名直接打印 setMethod(“show”, “employee”, function(object) { inorout <- ifelse(object@union, “is”, “is not”) cat(object@name, “has a salary of”, object@salary, "and", inorout, "in the union", "\n") } )
S3類和S4類的對比:
S3具有較強的靈活性,而S4具有很好的安全性。一般來說推薦使用S3使代碼更加靈活,而當安全性更加重要時選擇S4(比如混合編程的情況下)。
對象的管理:
ls()函數:列出所有對象;
rm()函數:刪除特定對象;
save()函數:保存對象集合,可以用load()載入;
查看對象內部結構的函數:class(), mode(), names(), attributes(), unclass(), str(), edit();
exists()函數:判斷對象是否存在。
=========================================================================
調試
調試的基本原則:
調試的本質:確認原則;
從小處着手;
模塊化的、自頂向下的調試風格;
反漏洞。