13.1 什么时候该用函数
先看一个例子:
df <- tibble::tibble( a = rnorm(10),#产生10个服从正态分布的随机数 b = rnorm(10), c = rnorm(10), d = rnorm(10) ) df$a <- (df$a - min(df$a, na.rm = TRUE)) / (max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE)) df$b <- (df$b - min(df$b, na.rm = TRUE)) / (max(df$b, na.rm = TRUE) - min(df$b, na.rm = TRUE)) df$c <- (df$c - min(df$c, na.rm = TRUE)) / (max(df$c, na.rm = TRUE) - min(df$c, na.rm = TRUE)) df$d <- (df$d - min(df$d, na.rm = TRUE)) / (max(df$d, na.rm = TRUE) - min(df$d, na.rm = TRUE))
显然,上面这一大段代码是数据标准化(将每列的值调整到 0 到 1 之间)常用的一个方法Max-Min。
先分析一下代码。
df$a - min(df$a, na.rm = TRUE)) / (max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE))
这段代码只有一个输入:df$a。使用具有通用名称的临时变量来重写代码。 以上代码只需要一个数值向量,我们可以称其为 x:
x <- df$a (x - min(x, na.rm = TRUE)) / (max(x, na.rm = TRUE) - min(x, na.rm = TRUE))
这段代码中还有一些重复,计算了 3 次数据最大值和最小值,可以简化:
rng <- range(x, na.rm = TRUE) #该向量包含给定参数的最大值和最小值。 (x - rng[1]) / (rng[2] - rng[1])
接下来就可以将其转换为函数了:
rescale01 <- function(x) { rng <- range(x, na.rm = TRUE) (x - rng[1]) / (rng[2] - rng[1]) } rescale01(c(0, 5, 10)) #测试 #> [1] 0.0 0.5 1.0
要想创建一个新函数,需要 3 个关键步骤。
为函数选择一个名称。在以上示例中,我们使用 rescale01 作为函数名称,因为这个函数的功能是将一个向量调整到 0 到 1 之间。
列举出 function 中所用的输入,即参数。这个示例中只有一个参数,如果有更多参数, 那么函数调用形式就类似于 function(x, y, z)。
将已经编写好的代码放在函数体中。在 function(...) 后面要紧跟一个用 {} 括起来的 代码块。
此时我们应该使用其他输入来测试函数是否正确:
rescale01(c(-10, 0, 10)) #> [1] 0.0 0.5 1.0 rescale01(c(1, 2, 3, NA, 5)) #> [1] 0.00 0.25 0.50 NA 1.00
既然已经有了函数,那么我们就可以利用它来简化原来的示例了:
df$a <- rescale01(df$a) df$b <- rescale01(df$b) df$c <- rescale01(df$c) df$d <- rescale01(df$d)
相对于原来的代码,这段代码更清楚易懂,而且还消除了复制粘贴可能带来的错误。但这段代码中仍然有一些重复,因为我们对多个数据列进行了同样的操作。(如何消除这种重复后面的章节会有)
函数的另一个优点是,如果需求发生变化,我们只需要在一处进行修改。
13.2 人与计算机的函数
简单来说,不止得让计算机运行你的函数,还得让别人能读懂。
函数名是非常重要的。理想的函数名应该既简短,又能清楚地说明函数的作用。
# 名称太短 f() # 名称不是动词,或者没有描述力 my_awesome_function() # 名称虽然长,但是表达得很清楚 impute_missing() collapse_years() 如果你的函数名由多个
如果你的函数名由多个单词组成,建议使用“snake_case”命名法,即使用小写单词,单词之间用下划线隔开。
# 千万别这样! col_mins <- function(x, y) {} rowMaxes <- function(y, x) {} # 良好的命名方式 input_select() input_checkbox() input_text() # 不太好的命名方式 select_input() checkbox_input() text_input()
尽可能避免覆盖现有的函数和变量。总体来说,完全不覆盖是不可能的,因为太多好名称 已经被其他 R 包占用了,但完全可以不覆盖 R 基础包中最常用的名称,这样可以避免混淆。
13.3 条件执行
if 语句可以使得你有条件地执行代码。其形式如下所示:
if (condition) { # 条件为真时执行的代码 } else { # 条件为假时执行的代码 }
13.3.1 条件
condition 的值要么是 TRUE,要么是 FALSE。如果它是一个向量,那么你会收到一条警告; 如果它是 NA,那么程序就会出错。
可以使用 ||(或)和 &&(与)操作符来组合多个逻辑表达式。
不能在 if 语句中使用 | 或 &,它们是向量化的操作符,只可以用于多个值(这就是我们在 filter() 函数中使用它们的原因)。
你还需要提防浮点数的问题:
x <- sqrt(2) ^ 2 x #> [1] 2 x == 2 #> [1] FALSE x - 2 #> [1] 4.44e-16
解决方式是使用 dplyr::near() 函数进行比较,详见 。
13.3.2 多重条件
你可以将多个 if 语句串联起来:
if (this) { # 做一些操作 } else if (that) { # 做另外一些操作 } else { #
但如果你有一长串 if 语句,那么就要考虑重写了。重写的一种方法是使用 switch() 函数, 它先对第一个参数求值,然后按照名称或位置在后面的参数列表中匹配返回结果:
#> function(x, y, op) { #> switch(op, #> plus = x + y, #> minus = x - y, #> times = x * y, #> divide = x / y, #> stop("Unknown op!") #> ) #> }
13.3.3 代码风格
if 和 function 后面总是要跟着一对大括号({}),其中的内容应该缩进两个空格。这样通过左侧空白就可以很容易地知道代码层次。
左大括号不应该自己占一行,而且后面要换行。右大括号应该自己占一行,除非后面跟着 else。大括号中的代码一定要缩进:
# 好 if (y < 0 && debug) { message("Y is negative") } if (y == 0) { log(x) } else { y ^ x } # 不好 if (y < 0 && debug) message("Y is negative") if (y == 0) { log(x) } else { y ^ x }
如果 if 语句非常短,可以在一行内写下,那么可以不用大括号:
y <- 10 x <- if (y < 20) "Too low" else "Too high"
我们建议只对特别短的 if 语句采用这种形式,其他情况下还是完整形式更易于阅读:
if (y < 20) { x <- "Too low" } else { x <- "Too high" }
13.4 函数参数
函数的参数通常分为两大类:一类提供需要进行计算的数据,另一类控制计算过程的细节。举例如下。
在 log() 函数中,数据是 x,细节则是对数的底,即 base。
在 mean() 函数中,数据是 x,细节则是从 x 前后两端(trim)移除多大比例的数据,以 及如何处理缺失值(na.rm)。
在 t.test() 函数中,数据是 x 和 y,检验的细节则是 alternative、mu、paired、var. equal 以及 conf.level 等设置。
在 str_c() 函数中,你可以向 … 参数提供任意数量的字符串作为数据,连接的细节则由 sep 和 collapse 参数控制。
通常情况下,数据参数应该放在最前面,细节参数则放在后面,而且一般都有默认值。设置默认值的方式与使用命名参数调用函数的方式是一样的:
# 使用近似正态分布计算均值两端的置信区间 mean_ci <- function(x, conf = 0.95) { se <- sd(x) / sqrt(length(x)) alpha <- 1 - conf mean(x) + se * qnorm(c(alpha / 2, 1 - alpha / 2)) } x <- runif(100) mean_ci(x) > [1] 0.498 0.610 mean_ci(x, conf = 0.99) > [1] 0.480 0.628
默认值应该几乎总是最常用的值。这种原则的例外情况非常少,除非出于安全考虑。例如,将 na.rm 的默认值设为 FALSE 是情有可原的,因为缺失值有时是非常重要的。虽然代码中经常使用的是 na.rm = TRUE,但是通过默认设置不声不响地忽略缺失值并不是一种良好的做法。
在调用函数时,应该在其中 = 的两端都加一个空格。逗号后面应该总是加一个空格, 逗号前面则不要加空格(与英文写法相同)。使用空格可以使得函数的重要部分更易读:
# 好 average <- mean(feet / 12 + inches, na.rm = TRUE) # 不好 average<-mean(feet/12+inches,na.rm=TRUE)
13.4.1 选择参数名称
参数名称也很重要。通常应该选择那些较长的、更具描述性的名称,但 R 中有一些非常短的通用名称,你应该记住它们。
x, y, z:向量。
w:权重向量。
df:数据框。
i, j:数值索引(通常用于表示行和列)。
n:长度或行的数量。
p:列的数量。
除此之外,你还可以考虑使用现有 R 函数中的参数名称。例如,使用 na.rm 来确定是否需要除去缺失值。
13.4.2 检查参数值
当编写的函数越来越多时,你有时会记不清某个函数到底是用来做什么的。这时就很容易 使用无效的参数来调用函数。为了解决这种问题,应该对函数参数进行明确的限制。
13.4.3 点点点(…)
R 中的很多函数可以接受任意数量的输入。它们需要一个特殊参数:...(读作点点点)。这个特殊参数会捕获任意数量的未匹配参数。
这个参数的作用非常大,因为你可以将它捕获的值传给另一个函数。如果你的函数是另一 个函数的包装器,那么这种一网打尽的方式就非常有用了。例如,我们经常用以下方式创建辅助函数来包装 str_c() 函数:
commas <- function(...) stringr::str_c(..., collapse = ", ") commas(letters[1:10]) #> [1] "a, b, c, d, e, f, g, h, i, j" rule <- function(..., pad = "-") { title <- paste0(...) width <- getOption("width") - nchar(title) - 5 cat(title, " ", stringr::str_dup(pad, width), "\n", sep = "") } rule("Important output") #> Important output ----------------------------------------
这里 ... 可以将我们不想处理的所有参数传递给 str_c()。虽然非常方便,但这种技术是有代价的:所有拼写错误的参数都不会引发错误消息。这使得我们很难发现输入错误:
x <- c(1, 2) sum(x, na.mr = TRUE) > [1] 4
如果想要检查 ... 中的值,那么你可以使用 list(...)。
13.4.4 惰性求值
R 中的参数求值的方式是惰性的,即直到需要参数时才会进行求值。这意味着,如果没有 使用参数,那么它就一直没有实际值。
13.5 返回值
13.5.1 显式返回语句
函数的返回值通常是最后一个语句的值,但你可以通过 return() 语句提前返回一个值。我 们认为最好有节制地使用 return() 语句,因为提前返回的一般都是比较简单的情况。常见 的提前返回原因就是输入为空:
complicated_function <- function(x, y, z) { if (length(x) == 0 || length(y) == 0) { return(0) } # 这里是复杂的代码
需要提前返回的另一个原因是,if 语句的一个分支非常复杂,而另一个分支则特别简单。 例如,你可能写出如下的 if 语句:
f <- function() { if (x) { # 需要 # 多行 # 代码 # 才能 # 完成 # 的 # 操作 # express } else { # 返回一个非常简单的值
但如果第一个分支中的代码非常长,到达 else 语句前,你可能就已经记不清 condition 了。解决这个问题的一种方法是将简单情形提前返回:
f <- function() { if (!x) { return(something_short) } # 需要 # 多行 # 代码 # 才能 # 完成 # 的 # 操作 # express
这样应该会使得代码更容易理解,因为不需要太多的上下文。
13.5.2 使得函数支持管道
如果想要让自己的函数支持管道操作,那么你应该仔细思考一下返回值。可以支持管道操 作的函数有两种主要类型:转换函数与副作用函数。
转换函数会传入一个明确的“基本”对象作为第一个参数,对这个对象进行处理后,再将其返回。例如,在 dplyr 中,这个关键的对象就是数据框。如果能够确定在自己的领域内应该使用哪种数据类型,那么你就可以让自己的函数支持管道操作了。
副作用函数经常用来执行某种行为,比如绘图或保存文件,而不是转换对象。这些函数会 “悄悄地”返回第一个参数,因此,默认情况下,第一个参数不显示在输出中,但仍然可 以由管道操作使用。
13.6 环境
刚开始编写函数时,不需要对环境有多深入的理解。但我们还是应该了解一些关于环境的知识,因为这些知识对于理解函数如何运行非常重要。 函数的环境决定了 R 如何寻找对象的值。例如,查看以下函数:
f <- function(x) { x + y }
在很多编程语言中,这段代码会引发一个错误,因为函数没有定义 y。这种代码在 R 中是有效的,因为 R 使用称为词法定界的一种规则来搜索对象的值。因为 y 没有在函数中进行 定义,所以 R 会在定义函数的环境中寻找 y:
y <- 100 f(10) #> [1] 110 y <- 1000 f(10) #> [1] 1010