R 学习笔记(6) -- R 语言编程结构


R 是一种块状结构的语言,R 语言的块(block) 由大括号划分,不过当块只包含一条语句时大括号可以省略。程序语句由换行符或者分号隔开,不过还是尽量别用分号了,每行一条语句也方便理解。


控制语句

for, while, repeat 循环, 用 break 可以跳出循环。

> x <- c(5,12,13)
> for (n in x) print(n^2)
[1] 25
[1] 144
[1] 169
> # 更正式写法,for 后面用大括号将代码块括起来
> for (n in x) {
+     print(n^2)
+ }
[1] 25
[1] 144
[1] 169
> # while 循环,break 跳出循环
> i <- 1
> while(TRUE) {
+     i <- i+4
+     if (i>10) break
+ }
> i
[1] 13
> # repeat 循环,用 break 跳出循环
> i <- 1
> repeat {
+     i <- i+4
+     if (i>10) break
+ }
> i
[1] 13

还有一个重要的语句是 next ,它会告诉解释器直接跳过本次迭代的剩余部分,直接进入循环的下一次迭代。这避免了使用复杂的 if-then-else 嵌套结构,让代码更清晰。next 有点像 python 中的 pass。

> x <- 1:10
> x
 [1]  1  2  3  4  5  6  7  8  9 10
> for (n in x) {
+     if (n%%2==1) next
+     print(n)
+ }
[1] 2
[1] 4
[1] 6
[1] 8
[1] 10

一个很有用的函数,get(),这个函数接受一个代表对象名字的字符串参数,然后返回该对象的内容。

> m <- c(1,2,3)
> u <- c(7,8,9)
> for (i in c("m","u")) {
+     z <- get(i)
+     print(z)
+ }
[1] 1 2 3
[1] 7 8 9

if-else 结构,基本结构如下:

> if (r==4) {
+    x <- 1
+ } else {
+    x <- 3
+    y <- 4
+ }

虽然 if 的执行部分只有一条语句,但大括号不可省略。if-else 结构语句与函数调用的相似之处在于,会返回表达式的值:

> x <- 2
> y <- if(x==2) x else x+1
> y
[1] 2
> y <- if(x==3) x else x+1
> y
[1] 3

R 语言的基本运算

R 语言的基本运算符:

运算符 描述
x + y 加法
x - y 减法
x * y 乘法
x / y 除法
x ^ y 乘幂
x %% y 模运算(取余)
x %/% y 整数除法
x == y 判断是否相等
x <= y 判断是否小于等于
x >= y 判断是否大于等于
x && y 标量的逻辑“与”运算
x ll y 标量的逻辑“或”运算
x & y 向量的逻辑“与”运算(x,y以及运算结果都是向量)
x l y 向量的逻辑“或”运算(x,y以及运算结果都是向量)
!x 逻辑非

参数的默认值

函数的参数可以设置一个默认值,“具名实参”如下:

> read.table
function (file, header = FALSE, sep = "", quote = "\"'", dec = ".", 
    numerals = c("allow.loss", "warn.loss", "no.loss"), row.names, 
    col.names, as.is = !stringsAsFactors, na.strings = "NA", 
    colClasses = NA, nrows = -1, skip = 0, check.names = TRUE, 
    fill = !blank.lines.skip, strip.white = FALSE, blank.lines.skip = TRUE, 
    comment.char = "#", allowEscapes = FALSE, flush = FALSE, 
    stringsAsFactors = default.stringsAsFactors(), fileEncoding = "", 
    encoding = "unknown", text, skipNul = FALSE) 
{
    if (missing(file) && !missing(text)) {
        file <- textConnection(text, encoding = "UTF-8")
        encoding <- "UTF-8"
        on.exit(close(file))
....

其中 header = FALSE意味着如果不重新指定 header 的值,则默认为 FALSE。


返回值

在函数中可以显示地调用 return 来返回值,如果不用 return 则函数默认将最后一条语句的值作为返回值。虽然调用 return 会延长执行时间,但这点时间是微不足道的,长远来看显示地调用 return 可使代码更易阅读,利大于弊。

当然也可以返回复杂对象,比如返回值是一个函数。

> x <- c(1,2,3,4,5,6,7,8,9)
> oddcount <- function(x){
+   k <- 0
+   for (n in x){
+     if (n %% 2 ==1) k <- k + 1
+   }
+   k
+ }
> oddcount(x)
[1] 5
> # 下面这种写法效果是一样的,但是显示地调用 return 更便于理解
> oddcount_new <- function(x){
+   k <- 0
+   for (n in x){
+     if (n %% 2 ==1) k <- k + 1
+   }
+   return(k)
+ }
> oddcount_new(x)
[1] 5

函数都是对象

R 中的函数是第一类对象(属于“funtction”类,函数类),函数可以作为对象来操作,创建函数:

> g <- function(x) {
+   return(x+1)
+ }
> # 查看函数 g 的内容
> g
function(x) {
  return(x+1)
}

function() 是 R 的一个内置函数,功能就是创建函数,在 function 的右边其实有两个参数,其一是创建的函数的形式参数列表,这个例子中只有一个 x,其二是函数的主体部分,函数体,本例中只有一句表达式return(x+1),第二个参数必须是表达式类。


环境和变量作用域的问题

简单来说变量被定义的环境决定了变量的作用域,顶层变量(全局变量)和局部变量,用 ls()函数可以查看当前环境下面的变量。

> w <- 12
> f <- function(y) {
+   d <- 8
+   h <- function() {
+     return(d*(w+y))
+   }
+   #查看函数 f 中的变量,局部变量
+   print(ls())
+   return(h())
+ }
> environment(f)
<environment: R_GlobalEnv>
> f(2)
[1] "d" "h" "y"
[1] 112
> # 查看全局变量
> ls()
[1] "f" "w"

在函数中对顶层变量进行更改只是临时性的,改变只发生在函数的命名空间中。

R 语言没有指针,因此有些不方便,举个例子,在Python中:

>>> x = [5,1,2,4,3]
>>> x.sort()
>>> x
[1, 2, 3, 4, 5]

Python 中对变量 x 进行排序 list.sort(x) 结果导致 x 本身发生了变化,但是在 R 中 x 本身并不会变:

> x <- c(5,1,2,4,3)
> x
[1] 5 1 2 4 3
> # 对 x 排序后 sort() 函数返回排序后的结果
> # 但 x 本身的值仍是排序之前的
> sort(x)
[1] 1 2 3 4 5
> x
[1] 5 1 2 4 3
> # 改变 x 就需要对 x 重新进行赋值
> x <- sort(x)
> x
[1] 1 2 3 4 5

超赋值运算符

R 中环境层次中某一层次的代码对它上级层次中的所有变量只有读的权限,但是利用超赋值运算符 <<- 或 assign() 函数可以对上一层级中的变量进行修改。

> f <- function(u) {
+   u <<- 2*u
+   z <- 2*z
+   print(u) # 此时输出的是作为局部变量的函数参数 u,并不是超赋值的变量 u
+   print(z)
+ }
> f(2)
Error in f(2) : 找不到对象'z'
> z <- 3
> f(2)
[1] 2
[1] 6
> # 超赋值操作在函数的上一层级创建了一个变量 u 
> u
[1] 4
> # 函数内部的 z 和此处的 z 是两个变量,前者是局部变量,后者是顶层变量
> z
[1] 3

跨级操作变量的 assign() 函数

assign() 函数也可以对上一层级的变量进行写操作,类似于超赋值运算符的作用:

> # 当前顶层变量只有 x 和 z
> ls()
[1] "x" "z"
> two <- function(u) {
+   assign("u",2*u,pos=.GlobalEnv)
+   z <- 2*z
+ }
> x
[1] 1
> z
[1] 3
> two(x)
> # 定义 two 函数,调用 assign 函数后,顶层变量多了 two 和 u
> ls()
[1] "two" "u"   "x"   "z"  
> u
[1] 2
> z
[1] 3

例子中 assign 函数中 pos=.GlobalEnv 指定了新变量 u 的命名空间是全局环境。


闭包

闭包包含一个可创建局部变量的函数,并创建另一个函数可以访问该变量。举例说明:

> counter <- function() {
+   ctr <- 0
+   f <- function() {
+     ctr <<- ctr + 1
+     cat("this count currently has value",ctr,"\n")
+     }
+   return(f)
+ }
> c1 <- counter()
> c2 <- counter()
> # c1 和 c2 是两个独立的闭包,各自创建的变量在内存中存储在不同的位置
> c1
function() {
    ctr <<- ctr + 1
    cat("this count currently has value",ctr,"\n")
    }
<environment: 0x24ce6d0>
> c2
function() {
    ctr <<- ctr + 1
    cat("this count currently has value",ctr,"\n")
    }
<environment: 0x24cf1a0>
> c1()
this count currently has value 1 
> c1()
this count currently has value 2 
> c2()
this count currently has value 1 
> c2()
this count currently has value 2 
> c2()
this count currently has value 3 
> c1()
this count currently has value 3 
> # c1 和 c2 是两个独立的计数器

每次调用 counter() 变量 ctr 都会被创建,但是每次都创建在不同的环境中,例如 c1 的 <environment: 0x24ce6d0> 和 c2 的 <environment: 0x24cf1a0>,结果使得 c1 和 c2 相互独立。


递归魔法

递归是一种解决问题的思路,递归函数会调用自己本身,大致来讲,通过写一个递归函数 f() 来解决 X 类型的问题:

  1. 将 X 类型的原始问题划分为更小的 x 类型问题。
  2. 在 f() 中对每个较小问题调用 f() 函数。
  3. 然后在 f() 中,将 2 步骤的所有结果整合起来解决 X 问题。

例如在数学上,斐波那契数列是以递归的方法来定义:

F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2) (n≧2)

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233......

如果要得到第 31 个斐波那契数:

> fib <- function(n){
+   if (n==0) {
+     return(0)
+   }else if(n==1){
+       return(1)
+   }else{
+       return(fib(n-1)+fib(n-2))
+   }
+ }
> # fib(30) 返回值即第 31 个斐波那契数
> fib(30)
[1] 832040
> # 如果要得到第 100 个斐波那契数
> fib(99)
> # 机器算了几十分钟还没出结果,被我 ctrl + C 了
> # 这个算法只是演示递归思想,由于时间复杂度太高并不实用,斐波那契这类问题用动态规划算法可以快速得到结果


置换函数

R 中有一类不同寻常的置换函数,看这个例子:

> x <- c(1,2,3)
> x
[1] 1 2 3
> names(x)
NULL
> # 重点就是这个语句,把一个向量赋值给一个函数调用返回值?
> names(x) <- c('a','b','c')
> names(x)
[1] "a" "b" "c"
> x
a b c 
1 2 3 

例子中把一个向量赋值给了一个函数调用的结果,好像不合理,但实际上这可以用 R 中的置换函数的概念来解释:
names(x) <- c('a','b','c') 这句实际发生的操作是:

 > x <- "names<-"(x,value=c('a','b','c'))

这里调用了一个名为 names<-() 的函数,由于函数名中有特殊符号,用引号括起来了。

在 R 中任何左边不是标识符(即变量名)的赋值语句都可以看作是“置换函数”:

> # 例如这样一个语句
> g(u) <- v
> # R 语言会尝试用下面这种方法执行
> u <- "g<-"(u,value=v)
> # 如果事先已经定义好“g<-”函数,就可以正常执行置换函数

匿名函数

简单的说,匿名函数就是不将定义的函数赋值给一个变量,直接调用 function() 定义一个临时函数,看例子:

> z
     [,1] [,2]
[1,]    1    4
[2,]    2    5
[3,]    3    6
> y <- apply(z,1,function(x) x/c(2,8))
> y
     [,1]  [,2] [,3]
[1,]  0.5 1.000 1.50
[2,]  0.5 0.625 0.75

匿名函数对付一些简单的问题很方便,也使得代码更容易阅读。


肚子大不可怕,可怕的是肚子里没有好东西。 --加菲猫

推荐阅读更多精彩内容