2.8 函数
2.8.1 R中的函数
不像很多其他语言的函数(和方法)有value.func()
和func value
等格式,R中所有函数的通用格式是这样的:
function(argument1 = value1, argument2 = value2, ...)
比如
sample <- c(5.1, 5.2, 4.5, 5.3, 4.3, 5.5, 5.7)
# 根据传统,赋值变量时用`<-`号,赋值函数参数时才用`=`
t.test(x = sample, mu = 4.5)
#>
#> One Sample t-test
#>
#> data: sample
#> t = 3, df = 6, p-value = 0.02
#> alternative hypothesis: true mean is not equal to 4.5
#> 95 percent confidence interval:
#> 4.61 5.56
#> sample estimates:
#> mean of x
#> 5.09
二元运算符和[
(取子集符号)看起来一点都不像函数,而实际上它们也是函数,因此也可以用通用的格式使用他们,只是需要加上引号或转义符号:
"+"(2, 3)
`+`(2, 3)
## 5
"["(c("四川担担面", "武汉热干面", "兰州牛肉面", "北京炸酱面"), 2)
#> [1] "武汉热干面"
可自定义的二元运算符形式为%x%
, 其中x
为任何字符。(见第2.8.3.3节)
英语中,“parameter”或“formal argument”二词用于函数定义,“argument”或“actual argument”二词用于调用函数(Kernighan and Ritchie 1988),中文里分别是“形式参数”和“实际参数”,但是多数场合简称“参数”。
2.8.2 调用函数
根据通用格式(function(argument1 = value1, argument2 = value2, ...)
)调用函数。对于二元运算符,a %x% b
等价于"x"(a, b)
.
从“function(
”开始到此函数结尾的“)
”中间为(实际)参数,各参数用逗号隔开,空格和换行会被忽略,“#
”符号出现之处,那一行之后的内容都会被忽略。这意味着你可以(丧心病狂地)像这样调用一个函数。
sum (
# 4
4 # 我怕不是
, #疯了
6
)
#> [1] 10
它实际的好处是,当参数很长或是有嵌套的函数时,可以通过换行和空格使代码更易读,就像其它的编程语言一样。(后面会有很多例子)
函数的参数以seq
函数为例,通过查看帮助文档(在console执行?seq
),通常在Usage
一栏,可以查看它的所有的(形式)参数及其排序:
## Default S3 method:
seq(from = 1, to = 1, by = ((to - from)/(length.out - 1)),
length.out = NULL, along.with = NULL, ...)
可以看到第一个参数是from
,第二个是to
,第三个是by
,以此类推。我们执行seq(0, 50, 10)
的时候,R会理解成seq(from = 0, to = 50, by = 10)
。而想用指定长度的方法就必须要写清楚是length.out
等于几。
length.out
本身也可以简写:
seq(0, 25, l = 11)
#> [1] 0.0 2.5 5.0 7.5 10.0 12.5 15.0 17.5 20.0 22.5 25.0
因为参数中只有length.out
是以l
开头的,l
会被理解为length.out
. 但是这个习惯并不好;自己用用就算了,与别人分享自己的工作时请尽量使用参数名的全称。
对于seq(0, 50, 10)
,亦可写成seq(by = 10, 0, 50)
. 这是因为by
参数先赋值,0
和50
是未命名的参数,所以按照剩余的参数的排列顺序来,即from = 0, to = 50
. 同理,seq(to = 50, 0, 10)
也是等价的。
2.8.3 创建函数
2.8.3.1 普通函数
函数名 <- function(参数1, 参数2, ...){
对参数1和参数2
进行
一系列
一行或者多行
计算
return(计算结果)
}
在R中,函数是作为对象保存的,因此定义函数不需要一套另外的符号/语句,还是用赋值符号<-
,和function()
函数。
R自带了计算样本标准差 (standard deviation, \(s\))的函数, sd()
,我们可以根据它写一个计算均值标准差(即“标准误”, standard error)(\(SE=s_{\bar{x}}=\frac{s}{\sqrt{n}}\))
SE <- function(x) {
s <- sd(x)
n <- length(x)
result <- s/sqrt(n)
return(result)
}
# 随后,你就可以使用自定义的函数了
SE(c(5,6,5,5,4,5,6,6,5,4,5,3,8))
#> [1] 0.337
这里其实可以做一些省略。很多时候,最后一“句”的计算结果(不是赋值计算)就是我们想return
的结果。因此,这时return
可以省略:
SE <- function(x) {
s <- sd(x)
n <- length(x)
s/sqrt(n) # 注意不是`result <- s/sqrt(n)`
}
SE(c(5,6,5,5,4,5,6,6,5,4,5,3,8))
#> [1] 0.337
很多时候,函数内部有复杂流程控制,这时使用return()
可以很大地增强易读性:
# 这是随手写的一个没有意义的函数
myfunc <- function(i){
k <- 8
if (i>3) {
j <- -i
while(j < 20){
k <- k + i + j
j <- j+5
}
return(k)
} else {
if (i %% 2 == 0) {
return(5)
} else return(k*i)
}
}
myfunc(6)
#> [1] 83
本章剩余的内容,都是比较进阶的了。可以酌情从这里跳转至本章第2.9节。
2.8.3.2 匿名函数
函数不需要名字也可以执行。一般,会与apply
族函数联用(见第2.7.4节):
sapply(1:5, function(x) x^2)
#> [1] 1 4 9 16 25
或者用于
2.8.3.3 二元运算符
定义二元运算符的方式和定义普通函数的方法极其类似,只是参数必须要有且仅有两个(否则作为“二元”运算符就无意义了),且运算符名称需要用引号包围。
比如我们可以定义一个计算椭圆面积的函数
'%el%' <- function(x, y) pi*x*y
2 %el% 5
#> [1] 31.4
原则上,可自定义的二元运算符不一定要用%
包围;+
, -
, :
等符号的功能都可以被自定义,但是它们是R自带的,非常常用的函数,重定义它们只会带来麻烦。
2.8.3.4 闭包 (Closure)
函数里可以包含着另一个函数,这就形成了一个闭包:
myfunc <- function(){
a = 5
function(){
b = 10
return(a*b)
}
}
# 执行myfunc()的时候,默认结果为最后一句/一行,在这里应为内函数:
myfunc()
#> function(){
#> b = 10
#> return(a*b)
#> }
#> <environment: 0x7fae44fb9110>
# 既然`myfunc()`的结果是一个函数,那么在后面再加上一个括号就是执行内函数了;内函数可以使用外函数中所定义的变量(比如这里使用了外函数的`a = 5`)
myfunc()()
#> [1] 50
speak <- function(x){
x()$speak
}
speak(cat)
#> NULL
2.8.4 关于...
有时候,你想写的函数可能有数量不定的参数,或是有需要传递给另一个函数的“其他参数”(即本函数不需要的参数),这时候可以在函数定义时加入一个名为...
的参数,然后用list()
来读取它们。list是进阶内容,在第2.4节有说明。
比如我写一个很无聊的函数:
my_func <- function(arg1, arg2 = 100, ...){
other_args <- list(...)
print(arg1)
print(arg2)
print(other_args)
}
my_func("foo", cities = c("崇阳", "Αθήνα", "つがる"), nums = c(3,4,6))
#> [1] "foo"
#> [1] 100
#> $cities
#> [1] "崇阳" "Αθήνα" "つがる"
#>
#> $nums
#> [1] 3 4 6
arg1
指定了是"foo"
(通过简写),因此第一行印出"foo"
; arg2
未指定,因此使用默认值100
,印在第二行。cities
和nums
在形式参数中没有匹配,因此归为“…”,作为list印在第三行及之后。
下面是一个(没有意义的)利用...
做一个对于向量和列表通用的函数calc()
,使calc(data, pow = a, times = b, add = c)
返回与原数据data
的结构相同,但各元素\(x\)变为\(bx^a+c\)的向量/列表(这和OOP有相似之处):
calc_v <- function(v, pow = 1, times = 1, add = 0) {
v ^ pow * times + add
}
calc_l <- function(L, pow = 1, times = 1, add = 0) {
rapply(L, function(l) l ^ pow * times + add, how = "list")
}
calc <- function(data, ...) {
if(is.list(data)) {
calc_l(data, ...) # 即 calc_l(L = data, ...)
} else if(is.vector(data)) {
calc_v(data, ...) # 即 calc_v(v = data, ...)
}
}
calc(c(1, 2, 3), pow = 2, add = 1)
#> [1] 2 5 10
calc(list(1, 2, list(10, 20)), pow = 2, times = 2)
#> [[1]]
#> [1] 2
#>
#> [[2]]
#> [1] 8
#>
#> [[3]]
#> [[3]][[1]]
#> [1] 200
#>
#> [[3]][[2]]
#> [1] 800
pow
, times
和add
不是calc
的参数,它们以...
的形式被传递给calc_l()
和calc_v()
.
在第2.7.4.2节讲到,sapply()
的功能本质上和lapply()
一致,只是会化简结果。我们看一下sapply()
函数的结构:
sapply
#> function (X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
#> {
#> FUN <- match.fun(FUN)
#> answer <- lapply(X = X, FUN = FUN, ...)
#> if (USE.NAMES && is.character(X) && is.null(names(answer)))
#> names(answer) <- X
#> if (!isFALSE(simplify) && length(answer))
#> simplify2array(answer, higher = (simplify == "array"))
#> else answer
#> }
#> <bytecode: 0x7fae42df7d10>
#> <environment: namespace:base>
可以看到,answer <- lapply(X = X, FUN = FUN, ...)
这一行把sapply()
里...
中的参数传递到了lapply()
中,使用lapply()
得到未化解的结果answer
, 随后仅需要写用来化简结果的代码,而不需要把与lapply()
里的代码重写一遍。
2.8.5 赋值函数外的对象
函数内的赋值一般只在函数内有效,比如:
x <- 5
fun1 <- function() {
x <- 100
}
fun1()
x
#> [1] 5
使用assign()
函数可以在函数内赋值任意environment中的对象,其中最常见的是Global environment里的(即等价于在console中直接赋值)。
x <- 5
fun1 <- function() {
assign("x", 100, envir = .GlobalEnv)
}
fun1()
x
#> [1] 100
<<-
23可用于赋值“上一层”里的对象。当在“第一层”的函数里使用<<-
时, .GlobalEnv
里对应的对象就会受到影响,即和assign("x", value, envir = .GlobalEnv)
等效。
x <- 5
fun1 <- function() {
x <<- 100
}
fun1()
x
#> [1] 100
在下面的例子中,fun2()
赋值了fun1()
里的n
, 但.GlobalEnv
里的n
不受影响。
n <- 1 # `GlobalEnv`里的`n` = 1
fun1 <- function() {
n <- 10 # `fun1()`里的`n` = 10
fun2 <- function() {
n <- 50 # 赋值`fun2()`里的`n`
n <<- 100 # 重赋值`fun1()`里的`n`为100
}
fun2() # 运行`fun2()`
return(n) # 返回`fun1()`里的`n`
}
fun1() # 10是否变为100?
#> [1] 100
n # 是否仍然是1?
#> [1] 1
利用这个性质,我们可以使apply()
族函数进行递归计算,比如求累加和:
cum = 0
sapply(1:10, FUN = function(x){
cum <<- cum + x
cum
})
#> [1] 1 3 6 10 15 21 28 36 45 55
原则上,这已经不是一个向量化计算了,但是在这个例子中sapply()
仍然比for循环(见下)速度更快。
cum = 1
for (i in 2:10000) {
cum[i] <- cum[i-1] + i
}
cum
2.8.6 测速
当你开始处理复杂,大量的数据时,或是向别人分享自己的代码时,代码执行的速度变得重要。
一段代码/一个函数经常有很多种写法,哪种效率更高呢?实践是检验真理的唯一标准,R提供了一个测速函数:system.time()
函数。
x <- vector('numeric')
system.time(
for (i in 1:50){
for (j in 1:100) {
x <- append(x, i*j)
}
}
)
#> user system elapsed
#> 0.046 0.022 0.068
其中第三个数字 (elapsed
)是执行system.time()
括号内的语句实际消耗的时间。可以使用索引 ([3]
)抓取。
如果括号内的语句大于一句,像这样:
system.time(
1 + 1
2 + 1
)
R会报错。就像流程控制里学到的那样,需要用大括号包围多行/多句的语句,就像这样:
system.time({
1 + 1
2 + 1
})
References
Kernighan, Brain W., and Ednnis M. Ritchie. 1988. The C Programming Language. 2nd ed. Prentice Hall.