前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >18. R编程(四:函数详述、匿名函数、变量作用域)

18. R编程(四:函数详述、匿名函数、变量作用域)

作者头像
北野茶缸子
发布2021-12-17 09:08:01
2.1K0
发布2021-12-17 09:08:01
举报
文章被收录于专栏:北野茶缸子的专栏

部分内容参见:生信技能树 课程

1. 定义函数

函数定义使用function关键字,一般格式为:

★函数名 <- function(形式参数表) 函数体”

定义函数有一定的规范性,在定义与调用时都不能省略圆括号。

实际上, “function(参数表) 函数体”这样的结构本身也是一个表达式, 其结果是一个函数对象。在通常的函数定义中, 函数名只不过是被赋值为某个函数对象, 或者说是“绑定”(bind)到某个函数对象上面。同一个函数对象可以被多个函数名绑定。函数是普通R对象, 在编程语言术语中称这样的函数为第一级函数(first class functions), 或函数是第一级对象(first class objects), 即函数在R语言中与其他普通数值型对象、字符型对象有相同的地位。

因为函数也是R对象, 也可以拥有属性。所谓对象, 就是R的变量所指向的各种不同类型的统称。

可以将多个函数存放在一个列表中。例如,在用随机模拟比较不同的统计模型时, 常常将要对一组数据采用的多个并行的建模函数存放在列表中, 对许多组模拟数据的每一组用循环的方法应用列表中的每一个建模函数分别得到结果。

2. 创建与使用函数

  • 函数创建可以没有参数
代码语言:javascript
复制
hello <- function(){
  print("Hi there!")
  TRUE
}
hello()
  • R 的向量化调用

我们可以直接为某个参数传入一个向量,R 会自动的遍历整个向量并在函数中执行并返回一个新的向量:

代码语言:javascript
复制
> my_f = function(x){ x*3 }
> my_f(c(1,2,3,4,5))
[1]  3  6  9 12 15
  • R 的默认参数

和py 一样,R 也可以指定缺省值,有缺省值的形式参数在调用时可以省略对应的实参, 省略时取缺省值。

  • R 的参数顺序

R函数调用时全部或部分形参对应的实参可以用“形式参数名=实参”的格式给出, 这样格式给出的实参不用考虑次序, 不带形式参数名的则按先后位置对准。

代码语言:javascript
复制
function(x, y){
c("The first one is", x, "The second one is", y)}
> my_f(3,4)
[1] "The first one is"  "3"                 "The second one is"
[4] "4"                
> my_f(y = 3, x = 4)
[1] "The first one is"  "4"                 "The second one is"
[4] "3"                
> my_f(y = 3, 4)
[1] "The first one is"  "4"                 "The second one is"
[4] "3"

作为好的程序习惯, 调用R函数时, 如果既有按位置对应的参数又有带名参数, 应仅有一个或两个是按位置对应的, 按位置对应的参数都写在前面, 带名参数写在后面, 按位置对应的参数在参数表中的位置应与定义时的位置一致。在定义函数时,没有缺省值的参数写在前面, 有缺省值的参数写在后面。不遵守这样的约定容易使得程序被误读, 有时会在运行时匹配错位。

  • 函数的递归调用

最经典的斐波那契数列,在python 中我们通过在函数中引用函数自身来表示递归调用,R 也同样可以实现:

代码语言:javascript
复制
fib1 <- function(n){
  if(n == 0) return(0)
  else if(n == 1) return(1)
  else if(n >=2 ) {
    fib1(n-1) + fib1(n-2)
  }
}
  
> for (i in 1:5) print(fib1(i))
[1] 1
[1] 1
[1] 2
[1] 3
[1] 5  

但这样定义有个问题,当我们的原先的函数改名了,使用新被定义的函数执行代码,就会发生报错了:

代码语言:javascript
复制
> tmp = fib1
> fib1 = 1
> tmp(3)
Error in fib1(n - 1) : 没有"fib1"这个函数

因此对于递归的时候,除非为了装X,否则谨慎使用。(ps:装X 还不够吸引人吗?)

3. 函数的返回值

  • 函数体中最后的表达式为函数返回值
代码语言:javascript
复制
> my_f = function(x){ x+1;x*3 }
> my_f(3)
[1] 9

如果需要指定,可以使用return(y)的方式在函数体的任何位置退出函数并返回y的值。

代码语言:javascript
复制
> my_f = function(x){ return(x+5); print("This won't be printed")}
> my_f(5)
[1] 10
  • invisible 可以让R命令行直接调用此函数时不自动显示返回值
代码语言:javascript
复制
> my_f = function(x){if (x > 0) x else invisible(x)}
> my_f(5)
[1] 5
> my_f(-2)

当预期返回的结果显示无意义或者显示在命令行会产生大量的输出时可以用此方法。

4. 函数的组成部分

一个自定义R函数由三个部分组成:

  • 函数体body(),即要函数定义内部要执行的代码;
  • formals(),即函数的形式参数表以及可能存在的缺省值;
  • environment(),是函数定义时所处的环境, 这会影响到参数表中缺省值与函数体中非局部变量的的查找。
代码语言:javascript
复制
> my_fn = function(x,y=100) x+y

> environment(my_fn)
<environment: R_GlobalEnv>
> body(my_f)
{
    x * 3
}
> body(my_fn)
x + y
> formals(my_fn)
$x


$y
[1] 100

注意,函数名并不是函数对象的必要组成部分。

R提供了一个非常方便的函数alist用来构建参数列表。我们可以像定义函数一样很简单地指定参数列表。

代码语言:javascript
复制
> f <- function(x, y=1, z=2) { x + y + z}
> formals(f) <- alist(x=, y=100, z=200)
> args(f)
function (x, y = 100, z = 200) 
NULL

5. 函数的使用技巧

  • 向量化与效率

关于程序效率,请比较如下两个表达式:

代码语言:javascript
复制
n/(n-1)/(n-2)*sum( (x - xbar)^3 ) / S^3
n/(n-1)/(n-2)*sum( ((x - xbar)/S)^3 )

ps:虽然能够理解这种S 表达式先后处理的差异,可是这个除了n 次还是无法理解啊。

这两个表达式的值相同。表面上看,第二个表达式更贴近原始数学公式, 但是在编程时, 需要考虑计算效率问题, 第一个表达式关于S只需要除一次, 而第二个表达关于S除了n次, 所以第一个表达式效率更高。一个函数如果仅仅用几次, 这些细微的效率问题不重要, 但是如果要编写一个R扩展包提供给许多人使用, 程序效率就是重要的问题。

  • 部分匹配

在调用函数时, 如果以“形参名=实参值”的格式输入参数, 则“形参名”与定义时的形参名完全匹配时最优先采用;如果“形参名”是定义时的形参名的前一部分子串, 即部分匹配, 这时调用表中如果没有其它部分匹配, 也可以输入到对应的完整形参名的参数中;按位置匹配是最后才进行的。有缺省值的形参在调用时可省略。

部分匹配允许我们节省一丁点儿键入工作量(狂喜),但确实实在不严谨,虽然R 默认是纵容我们这种行为的,但我们可以选择设置发出警告:

代码语言:javascript
复制
options(warnPartialMatchArgs = TRUE)

> my_f = function(asd){asd}
> my_f(a = 3)
[1] 3
Warning message:
In my_f(a = 3) : 'a'部分匹配为'asd'
  • do.call 与管道符号

do.call 可以对列表对象进行处理,相当于将列表中的所有元素作为参数进行处理:

代码语言:javascript
复制
> do.call(mean, list(3,4,5))
[1] 3

magrittr包中的%>% 管道符号,则可以很方便的表现出步骤执行的顺序,可以参见:比如说对数据框类型的数据处理时使用。

6. 匿名函数

由于R 的语法限制,其并没有py 中通过lambda 创建匿名函数的功能。

但也可以通过其他函数来使用匿名函数,比如apply 家族。但由于R 的函数声明比py 相对复杂(需要声明function 来创建),因此语句上也复杂一些:

代码语言:javascript
复制
> sapply(c(-2, -0.5, 0, 0.5, 1, 1.5), function(x) if(abs(x) <= 1) x**2 else 1)
[1] 1.00 0.25 0.00 0.25 1.00 1.00

类似python,R 中也有Filter 函数:

代码语言:javascript
复制
Filter(fn, obj)

比如:

代码语言:javascript
复制
> Filter(function(x) is.numeric(x), list(1,2,'sd'))
[[1]]
[1] 1

[[2]]
[1] 2

此外函数integrate 也接受函数作为参数的。

简单理解来说,任何可以接受函数作为参数的函数,都可以使用匿名函数。

7. 变量作用域

全局变量与工作空间

在所有函数外面(如R命令行)定义的变量是全局变量。在命令行定义的所有变量都保存在工作空间 (workspace), 也称为全局环境中。在RStudio的Environment窗格可以看到“Global Environment”的内容, 分为数据框(Data)其它变量(Values)函数(Functions)三类。

在命令行可以用ls()查看工作空间内容。

代码语言:javascript
复制
> ls()
[1] "a"    "fib1" "fib2" "g"    "gv"   "i"    "my_f" "tmp" 

ls() 函数还支持正则匹配,我们可以使用pattern 参数匹配指定的变量。

此外,object.size 函数可以用来计算变量所占的存储大小,其也可以接受多个变量,计算其大小总和:

代码语言:javascript
复制
> object.size(ls())
560 bytes

局部变量

在一般计算机语言中, “变量”实际是计算机内存中的一段存储空间, 但是R中略微复杂一些, R的变量实际是指向R对象的引用, 称为“绑定” (这点和py 类似?)。在较简单的函数定义中大体上可以将R 变量看成是对应的存储空间。

函数的参数(自变量)在定义时并没有对应的存储空间, 所以也称函数定义中的参数为“形式参数”。函数的形式参数在调用时被赋值为实参值(这是一般情形), 形参变量和函数体内被赋值的变量都是局部的。这一点符合函数式编程(functional programming)的要求。所谓局部变量, 就是仅在函数运行时才存在, 一旦退出函数就不存在的变量。

代码语言:javascript
复制
 tmp = function(){b = 10; print(b)}
> b
错误: 找不到对象'b'

在函数调用时,行参被赋值为实参,在函数内部对形式参数作任何修改在函数运行完成后都不影响原来的实参变量, 而且函数运行完毕后形式参数不再与实际的存储空间联系。

代码语言:javascript
复制
a = 5
> tmp = function(){a = 10; print(a)}
> tmp()
[1] 10
> a
[1] 5

如果我们希望在函数内对变量进行修改,可以将其返回值进行赋值。

代码语言:javascript
复制
f <- function(x, inc=1){
  x <- x + inc
  x
}

这里还有一个方法,即使用超赋值 <<-,可以将当前作用域的结果作用到上一级环境中,但需要谨慎使用。函数内部可以读取全局变量的值,但一般不能修改全局变量的值。在现代编程指导思想中, 全局变量容易造成不易察觉的错误, 应谨慎使用, 当然,也不是禁止使用, 有些应用中不使用全局变量会使得程序更复杂且低效。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 北野茶缸子 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 定义函数
  • 2. 创建与使用函数
  • 3. 函数的返回值
  • 4. 函数的组成部分
  • 5. 函数的使用技巧
  • 6. 匿名函数
  • 7. 变量作用域
    • 全局变量与工作空间
      • 局部变量
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档