函数

函数 属于 R

1 个练习

关于 函数

函数早在基础概念中就介绍过了,比如下面这个例子:

squareit <- function(x) {
    x * x
}

squareit(3)
#> [1] 9

# shorter form
squareit_short <- function(x) x ^ 2

仔细观察squareit的定义,可以看到几个组成部分:

  • 函数接受一个形参x。
  • 函数有一个函数体,通常用花括号{ }括起来。
  • 函数对象创建好后,会被赋值给变量squareit。

在 R 中,函数是一等对象,和数字、字符串没什么两样。 因此,squareit <- function...这样的赋值在语法上和x <- 42完全一样。

Advanced

在平常使用中,访问函数的各个组成部分并不常见,但做起来很简单。

class(squareit)
#> [1] "function"
formals(squareit)
#> $x

body(squareit)
#> {
#>     x * x
#> }

用formals()得到的形参看起来像一个列表,用的是$x这种语法。 实际上,它的类型是pairlist,一种特殊的列表,里面存放的是键值对。

body是可执行代码,类型是language(你大概不需要关心这一点)。

实参

与其他脚本语言(比如 Python 和 Julia)不同,R 并不明确区分位置实参和关键字实参。

函数调用既可以按位置传值,也可以按名称传值。 对于实参很多、顺序很难记住的复杂函数,后者特别有用。

f <- function(x, y) x / y

# call positionally
f(4, 2)
#> [1] 2

# call by name
f(y = 2, x = 4)
#> [1] 2

可选实参

可以在函数定义里指定默认实参值,但这些实参必须放在所有没有默认值的实参后面。

之后我们可以选择接受默认值,或者把它覆盖掉。

g <- function(x, y = 2) x / y

# default y value
g(6)
#> [1] 3

# explicit y value
g(6, 3)
#> [1] 2

额外实参

如果要接受任意数量的额外实参,可以在定义中使用...(省略号)。 你可以把函数调用里多出来的值转换成一个向量,不过请继续往下读,还有另一种使用这些“点参数”的方式(在其他几门语言里叫“varargs”)。

var_f <- function(x, y, ...) {
  print(c(...))
}

var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"

函数环境

前面我们说过,形参和函数体都是函数的组成部分。

其实还有第三个组成部分:函数定义时所在的环境。

嵌套函数的情况可以说明这一点:

outer_func <- function(x) {
  inner_func <- function(y) {
    x * y
  }
  
  inner_func(3)
}

outer_func(5)
#> [1] 15

函数调用把x = 5传给外层函数,这个值在外层函数体里是可用的。

内层函数是外层函数体的一部分,因此能访问x的值。 换种说法,x = 5位于内层函数的环境中。

严格来说,这叫做闭包。

环境在处理点参数时尤其重要,因为通过点参数提供的值可以继续传递给函数体里的其他函数调用。 外层函数不需要知道、也不需要关心这些点参数是什么含义。

f_var <- function(x, ...) {
  sum(x, ...)
}

x <- c(1, 2, NA, 6)

# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA

# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9

这种技巧被 Tidyverse 系列库大量使用,比如stringr。 许多stringr函数只是对stringi和 base R 中底层函数的友好封装。 传给str_*()函数的额外实参,会被直接传递到那些底层函数。

函数作为返回值

外层函数可以定义一个内层函数(可以是具名的,也可以是下面会讲到的匿名函数),并把它作为返回值。

被返回的函数会带上它定义时所在的环境。

times_y <- function(x) {
  # anonymous function - see next section
  \(y) x * y
}

f <- times_y(3)
class(f)
#> [1] "function"

f(4)
#> [1] 12

匿名函数

定义函数时,我们通常会把生成的函数对象绑定到一个变量上:

x <- 2

# function definition captures x from the environment
doubleit_short <- function(y) x * y

这样在脚本后面使用这个函数会很方便,但这种绑定并不是必需的。 一个简短的、只用一次的函数,在当下这个场景里可能就够用了。 如果不绑定名称,它就叫做匿名函数。

匿名函数用得实在太多,所以(从 R v4.1.0 起)有了一种定义它们的简写语法:把function这个词换成反斜杠\。

上一节讲返回值时就用过这样的例子。

修改时复制

R 允许给向量的单个元素赋值。 如果我们把一个向量作为实参传进去,在函数体里修改它再返回,得到的就是修改后的向量。

但原来的那个向量怎么样了呢?

f <- function(vec) {
  vec[1] <- 42
  vec
}

vals <- c(1, 3, 4)

# f() returns a modified vector
f(vals)
#> [1] 42  3  4  

# the original vector is unchanged.
vals
#> [1] 1 3 4

R 是一门为数据科学设计的语言。 收集这些数据可能要花大量时间、精力,甚至可能是天文数字般的金钱:所以千万不要把数据损坏了!

总的原则(少数例外除外)是修改时复制。 如果一个对象(比如向量)的改动可能给后面带来问题,R 会返回一个修改后的副本,而原来的对象保持不变。

复制大型数据结构可能很耗费计算资源,但相比数据被损坏,这通常算是两害相权取其轻。

Caution

小心那些会导致同一份数据被反复复制的操作。

用 Tidyverse 作者 Hadley Wickham 的话来说(大意):

循环本身并不慢,但它让人太容易在循环里加入慢操作,这一点很危险。

向量化或高阶函数可以帮助你避开这类性能杀手。

通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 函数