引言:所谓编程,就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到相应结果的过程。构成程序代码的基本元素包括常量、变量、表达式等之前讲述的内容,然而现实的问题往往复杂到用表达式是无法解决的。所以程序必然包括能将常变量、表达式等程序设计语言中的基本元素组织起来的方法。
进入 R 作为程序语言学习的第二个层次——控制结构,
以及第三个层次——函数与类。
1.控制结构
基本上所有的计算机程序语言都有三种基本控制结构:顺序、分支、循环。
1.1顺序结构:
顺序结构由表达式序列组成,程序执行时,按表达式的顺序从上而下逐条执行。
x <- 2
a <- 0.5
y <- a * x
y
## [1] 1.0
R 会按照先后顺序完成所有操作。这种顺序是严格的,如果变量或符号常量在未赋值前被调用,系统会报出错误。
1.2分支结构
分支结构则是根据一定的条件判断决定执行哪一部分表达式。
条件判断语句的一般形式是
if (condition) cons_expression else alt_expression
或者:
if (condition) cons_expression
简单实例:
x <- -1
if (x > 0) print("x > 0") else print("x <= 0")
## [1] "x <= 0"
多数时候我们用到的分支结构都是为了解决上述一分为二的情形,但难
免也会碰到多重分支的情况:
x <- 4
if (x == 1) {
print("No. 1")
}else if (x == 2) {
print("No. 2")
}else if (x == 3) {
print("No. 3")
}else{
print("No medal")
}
[1] "No medal"
注意:判断表达式返回了一个有多个逻辑值组成的向量,当这个向量交给
条件语句命令 if 时,只会采用第一个元素 TRUE 值。
可用使用 ifelse() 函数进行向量化操作
x <- c(6:-4)
x
## [1] 6 5 4 3 2 1 0 -1 -2 -3 -4
ifelse(x >= 0, x, x * -1)
## [1] 6 5 4 3 2 1 0 1 2 3 4
分析ifelse(x >= 0, x, x * -1)语句,
ifelse()函数的第一参数为判断表达式,第二个参数是条件为真时执行的操作,第三个参数是条件为假时执行的操作,最终的返回值是一个向量。
分支结构展现了程序设计的灵活智能性,使得计算机对各种情况具有分析能力。
1.3 循环结构
循环结构使同一个表达式(组)根据一定的条件执行若干次。“若干”
循环结构体现了程序设计的计算能力和高效性。循环必须有终止循环的条件,且
终止条件必须在某一时刻为真。如果没有终止条件,那么就需要人为设置循
环终止。
R 语言中与循环终止相关的命令有 break 和 next。
break 命令将使循环体终止,然后进入循环体后的顺序结构继续执行后续的表达式操作;
next 只是停止执行循环体内 next 之后的表达式代码,然后进入下一次
循环,整个循环体并不会终止。
—— repeat 命令
repeat 是 R 语言中最简单的循环结构,它只重复同一个表达式:
x <- 2
repeat {if (x > 10) break else {print(x); x <- x + 2}}
## [1] 2
## [1] 4
## [1] 6
## [1] 8
## [1] 10
判断表达式 x > 10 的返回值在循环开始时为逻辑假 FALSE,因此不会
进入其后的表达式,即 break,而终止循环。只会进入 else 后面的部分,
因为这里的代码有两个表达式,所以我们用大括号包裹,以形成一个相对独
立的代码体。这里最关键的是 x 被从新赋值为当前值与 2 的和,该操作改
变了 x 值,并且每次循环完成时都增加 2。这使得终将在某时刻判断表达式
x > 10 的返回值为逻辑真 TRUE,从而执行 break,终止循环。如果此处
没有让 x 值增加的表达式,那么循环将无限进行下去。
repeat 循环简单,主要因为由于条件判断语句不是必须,也因此终止循环需要 break 命令。而 while 和 for 循环是需要提供条件判断语句的。
—— while 循环
while 循环的一般形式为:
while (condition) expression
可以理解为:当 condition 条件为真时,执行 expression。具体例子:
x <- 2
while (x <= 10) {print(x); x <- x +2}
## [1] 2
## [1] 4
## [1] 6
## [1] 8
## [1] 10
得到的结果 repeat 循环一样。分析它们执行过程的异同。
while 循环中的判断条件与 repeat 循环恰恰相反。
while 循环中只有 x ≤10 时才会进入循环体,开始时条件判断始终返回逻辑真值。同样的,while循环体必须有使条件判断为 FALSE 的操作,这就是表达式 x <- x + 2 的作用。
while 循环中同样可以使用 break 和 next。next理解为跳过
x <- 2
while (x <= 10) {
x <- x + 2
if (x == 6){
next
}
print(x)
}
## [1] 4
## [1] 8
## [1] 10
## [1] 12
当 x = 6 时,next 终止了当前迭代中后续的操作,即打印输出 x 的值,而进入了下一次迭代。
—— for 循环
for 循环允许我们遍历向量(或其它数据结构)中的每个元素(或项目)。
一般形式:
for (var in seq) expression
其中的 seq 表示可索引的集合, 我们之前学习过的向量、矩阵、数组、数据框、列表都可作为 seq。例如:
for (i in c(1,2,3,4,5)) print(i*2)
## [1] 2
## [1] 4
## [1] 6
## [1] 8
## [1] 10
for (i in c(1,2,3,4,5)) {
if (i == 3){
next
}
print(i*2)
}
## [1] 2
## [1] 4
## [1] 8
## [1] 10
for (i in c(1,2,3,4,5)) {
if (i == 3){
break
}
print(i*2)
}
## [1] 2
## [1] 4
##上例再次演示了 next 和break的作用。这些例子中 seq 都为向量。
##数据框的例子:
letters.df <- as.data.frame(letters.m)
for (i in letters.df) print(i)
[1] "a" "b" "c"
[1] "d" "e" "f"
[1] "g" "h" "i"
[1] "j" "k" "l"
[1] "m" "n" "o"
[1] "p" "q" "r"
[1] "s" "t" "u"
[1] "v" "w" "x"
[1] "y" "z" "a"
2 函数
编程语言中函数概念来源于数学中的函数概念。数学中我们会这样表达函数:
y = f(x)
自变量 x 与因变量 y 的关系由 f() 表达,当有一个 x 值作为输入值进入 f()
就会得到一个 y 值,其中由 f() 决定的计算过程,在 f() 定义完成之后就
不会再改变。这种模型完全被移植到了计算机当中,把需要完成的操作封装到一个 “盒子” 里,每次使用这个 “盒子” 时只需要向其提供参数(当然也
可不提供参数),就可以坐等结果了。
这个 “盒子” 就是函数 function。R 语言中定义一个函数需要用到 function() 命令,一般形式:
NewFuncName <- function(arguments) { body }
新的函数定义完成后,后续的表达式即可通过 NewFuncName() 来调用
新函数了。新函数名字 NewFuncName 对与函数来说并不是必须的(见下
文的匿名函数)。arguments 是一个符号名字的集合,这些符号将会被贯穿
使用于函数主体 body,为了标明函数主体与函数体外表示的界限,主体会
用大括号包裹。当主体只有一个表达式时,大括号可省略。
2.1 参数
参数是一个符号常量,这里它起到了 “代词”的作用,代指将来在函数调用是传入的实际参数,而函数主体中出现的参数名称是一种 “引用”,在函数体中调用未在参数位置定义的符号,系统会提示未知对象的错误。
fnc1 <- function(x,y) {z <- x + y + x * y ; z}
函数 fnc 的数学形式为:
z = f(x, y) = x + y + x × y
当调用 fnc 时,必须提供两个参数输入 x 和 y,否则报错:
fnc1(x=3)
## Error in fnc(1) : argument "y" is missing, with no default
也就是说在未指定参数默认值的情况下,必须传入相应的实参,需在定义参数时让其等于默认值即可:
fnc2 <- function(x,y=2) {z <- x + y + x * y ; z}
fnc2(x=1)
## [1] 5
当调用函数时,也可以覆盖参数的默认值,用实际参数来计算:
fnc2(1,4)
## [1] 9
除了上述显式的定义参数列表外,R 还允许给函数以任意长度的参数列表,这意味着该函数的参数是可变的。实现的方式是在参数列表最后加入省略号(…):
fnc3 <- function(x,y=2,...) {
z <- x + y + sum(...)
z
}
fnc3(1,2,3,4,5)
## [1] 15
如果我们想单独处理可变数目的参数列表中的每个参数,可以通过
**将…转换为一个列表:**列表list
fnc4 <- function(x,...){
args <- list(...)
for (a in args){
if (a > 0){
x <- x + a
}
}
x
}
fnc4(10,1,2,-1,-2)
## [1] 1
上例中我们将可变参数列表转换为列表,然后对每个参数进行了与 0 的
比较,大于 0 的值将加到唯一的固定参数 x 上。
R 还为我们提供了延伸…的方法,即从…1、…2 到…9 等直接引用列表…中的内容,…1 表示列表中的第一项,…2 表示第二项,以此类推。
以上内容重点放在了函数的定义上,而在函数调用方面我们需要注意的是传入参数的方式。
R 为我们提供了三种指定参数值的方式(按优先级别):
- 确切匹配参数名字
- 部分匹配参数名字
- 按参数的顺序
定义一个函数来说明以上三种方式:
fnc5 <- function(firstArg,secondArg) {firstArg ^ secondArg}
fnc5(firstArg=2, secondArg=3) #全名匹配,确切匹配参数名字
## [1] 8
fnc5(f=2, se=3) #部分匹配,部分匹配参数名字
## [1] 8
fnc5(2,3) #按参数列表中顺序
## [1] 8
三种方式中推荐使用第一种,虽然你需要输入更多的字符,使代码可读性更强;部分匹配的方式容易引起歧义,使 R 产生混淆。
2.2 返回值
输出。从函数 fnc1 到 fnc5,它们都有输出(返回值),即函数体中最后一个表达式的所指的对象。R 为我们提供了 return() 函数:使代码更加清晰易读
fnc5 <- function(x) {return(x^2 + 2*x + 1)}
fnc5(2)
##[1] 9
2.3 作为参数的函数
通常情况下,一般类型的对象都可以作为函数的参数,此外 R 还允许函数作为参数传递给其它函数。
2.4 函数的属性
R 中一切皆对象,对象具备多重特性,这些特性统称为属性 attribute。
函数作为 R 语言中一种特殊的对象,它也是有属性的,且有一些好用的函数可以帮我提取函数的这些属性:
• args() 函数用于查看函数包含了哪些参数;
• formals() 函数用于生成一个关于函数参数的列表;
• alist() 函数用于生成一个参数的列表,并有生成的列表对象结合
• formals() 函数修改某函数的参数列表;
• body() 函数用于返回函数的函数体,函数体的类型为表达式 expression,因此我们可以利用 expression() 函数来修改函数的函数体;
3 环境
下面我们要讨论一个对于计算机编程初学者来说,不太那么容易理解的
一个概念——环境 environment。
前文讨论工作空间时,我们知道当打开一个 R 会话时,即进入了一个
R 工作空间,它储存着所有用户定义的对象 (向量、矩阵、函数、数据框、列
表)。其实这个工作空间也就是一种环境(也就是下文中的全局环境 global
environment)。
环境是 R 语言系统内部设计的框架结构,它的主要作用是帮助我们组
织和管理 R 中的所有对象的。我们知道 R 中一切皆为对象,例如在工作空
间中定义一个新的变量 v,并赋值为 0,当我们向 R 调用变量 v 时,R 会
告诉我们 v 的值。在内部,R 会先在当前工作空间中查找符号 v,如果存在
v,那么 R 就返回符号 v 所指代的数值对象,如果当前工作空间中没有找
到匹配的符号 v,R 就会逐级向上查找父环境中是否有匹配的符号。如果在
所有父环境中都能找到匹配的符号,那么 R 会返回一个系统错误:“Error:
object ‘v’ not found”。
所以 R 中每一层环境包含了:上下文中的符号集合、与这些符号相关
的对象,以及一个指向父环境的指针。我们可以把环境理解为一种链结构,
或者树结构。
??????????????????????????????????下
3.1 全局环境
数据分析实践中,我们最常接触的 R 环境就是全局环境。每次启动 R时,上次运行 R 时保存下来的 R 对象将会自动重新加载。
全局环境并不是环境链的顶部,而环境链的底部则取决于全局环境中是否定义了子环境。
全局环境实际上是搜索路径中的最有一个环境。下面我们通过一个实
际操作的例子来显示 R 中的环境链:
cur.env <- environment()
while (environmentName(cur.env) != environmentName(emptyenv())){
print(environmentName(parent.env(cur.env)));
cur.env <- parent.env(cur.env)}
## [1] "package:stats"
## [1] "package:graphics"
## [1] "package:grDevices"
## [1] "package:utils"
## [1] "package:datasets"
## [1] "package:methods"
## [1] "Autoloads"
## [1] "base"
## [1] "R_EmptyEnv"
由于我在 Rstudio 中运行上述表达式,所以当前(全局)环境的父环境
是 “tools:rstudio”,
如果我们在 RGUI 中运行,则其父环境为 “tools:RGUI”。
这里有几个知识点可以通过上述例子反应出来:
- 首先,R 中环境链的根为空环境(enmpty environment),环境名字为 R_EmptyEnv;
- 其次,R 在启动的过程中按照环境链的顺序从空环境开始逐个启动加载了相应的环境,直到全局环境;
- 函数 environmentName() 可返回作为参数输入的环境的名称;
- 函数 parent.env() 可返回作为参数输入的环境的父环境;
- 函数environment() 可新建或返回当前环境(参数为空)。
3.2 函数子环境
本小节中前面为讨论函数子环境做的铺垫。
函数是 R 基本数据类型(第一层次)、控制结构(第二层次)的组合与封装。封装起来的除了当中的数据对象和表达式控制结构外,
封装的还有——环境。
当我们新定义一个函数时,在函数内部就形成了一个新的环境,
它是当前(一般为全局环境)环境的子环境。这也就解释了为什么函数内
定义的局部变量,如参数,在函数外是不能调用的,而只能在函数内调用。
一个函数的父环境是创建该函数所在的环境。有时候,函数会定义在一
个环境中,如一个软件包,而调用它的是另一个环境,如全局环境,那么父
环境与调用环境就不一样了。
3.3 调用堆栈
3.4 向环境中添加/删除对象
R 可以通过 ls() 或者 objects() 函数来显示当前环境中的对象(输
入参数为空),如果以某个环境为参数,则返回该环境下的对象信息。
x <- environment()
while (environmentName(x) != environmentName(emptyenv())){
cat(environmentName(x), length(ls(x)), "\n");
x <- parent.env(x);
}
## R_GlobalEnv 12
## package:stats 447
## package:graphics 87
## package:grDevices 107
## package:utils 214
## package:datasets 104
## package:methods 218
## Autoloads 0
## base 1221
通过上例我们计算了从 base 到当前全局环境每一层环境中 R 对象的个数。
R 提供了向当前环境添加对象的方法:attach() 函数,需要注意的是 attach() 函数并不会复制。
去除某个对象的方法:detach() 函数。
一个对象到当前环境,它只是将相关数据(库)作为 R 的一个搜索的对象,
这样我们就可以方便的接触到数据(库)中的内容。
??????????????????????????????????上
women
## height weight
## 1 58 115
## 2 59 117
## 3 60 120
## 4 61 123
## 5 62 126
## 6 63 129
## 7 64 132
## 8 65 135
## 9 66 139
## 10 67 142
## 11 68 146
## 12 69 150
## 13 70 154
## 14 71 159
## 15 72 164
当如果我们想直接调用 women 中 height 数据,在未将 women 进行
attach 操作之前,系统会返回:“Error: object ‘height’ not found” 错误。而
attach 后:
height
## Error: object 'height' not found
attach(women)
height
## [1] 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
ls()
## [1] "a" "cur.env" "fnc1" "fnc2" "fnc3"
## [6] "fnc4" "fnc5" "i" "letters.df" "letters.m"
## [11] "x" "y"
显然 attach 操作并不会将 women 对象添加到全局环境中。
4 异常
到目前为止,我们已经先后接触到了 R 系统的异常提示,最常见的是
对象在未定义之前调用而报出的错误 “Error: object not found”。我们再来
看一类错误类型:
1 / "hello"
## Error in 1/"hello" : non-numeric argument to binary operator
和其它编程语言一样,R 也可以在发生异常事件时抛出异常信号,也可以捕获异常信号。在编写 R 程序时遇到了错误时,最好停止程序执行,警告用户加以处理,同时还需要注意在程序中调用其它函数或程序时,由它们发出的异常信息。
4.1 提示错误
stop() 函数可以终止当前表达式的执行并输出错误提示。
openfile <- function(filename) {
if (file.exists(filename)) {
read.table(filename)
} else {
stop("Cannot open the file: ", filename)
}
}
openfile("aaa.txt")
## Error in openfile("aaa.txt") : Cannot open the file:aaa.txt
## warning() 函数可以只发出错误信息,而不终止程序执行。
checkfile <- function(filename) {
if (file.exists(filename)) {
message("File: ",filename, " exists!")
} else { ## massage() 函数只是向用户输出某些信息。
warning("File does not exist:", filename)
}
}
checkfile("aaa.txt")
## File: aaa.txt exists!
## File does not exist:aaa.txt
write.table(x = women, file="aaa.txt")
checkfile("aaa.txt")
## File: aaa.txt exists!
## File: aaa.txt exists!
!!!!!!!!!!!!!!!上码,文件了还不会,不过代码很好理解
4.2 捕获错误
try() 函数
doit <- function(x) {
x <- sample(x, replace = TRUE);
if(length(unique(x)) > 10) {
mean(x);
} else {
stop("too few unique points");
}
}
x <- rnorm(20) #生成满足正态分布的20个数据点
res <- lapply(1:10, function(i) try(doit(x), TRUE))
unlist(res[sapply(res, function(x) inherits(x, "try-error"))])
## [1] "Error in doit(x) : too few unique points\n"
unlist(res[sapply(res, function(x) !inherits(x, "try-error"))])
## [1] 0.40770673 0.21614815 0.03386882 -0.10077969 -0.00662828 0.04086354
## [7] 0.53093542 0.34868516 0.08520131
tryCatch() 函数
?????????????????上码??????????????