函数的编写
格式如下所示:
|
|
函数案例之一:求斜边长
输入直角三角形的两个边长,求斜边长
|
|
运行结果如下所示:
|
|
函数案例之二:平方和
给出两个数字后,直接给出这两个数的平方和
|
|
运行结果如下所示:
|
|
函数案例之三:转换百分比
创建一个函数,将小数转换为百分数,如下所示:
|
|
运行结果如下所示:
|
|
在R中,函数其实就是一种对象,可以像操作其他对象一样操作了娄,例如可以将函数赋予一个新的对象来实现它的拷贝,如下所示:
|
|
运行结果如下所示:
|
|
需要注意的是,我们在输入ppaste <- addPercent
,函数addPercent
后面我们并没有加()
,这说明,只是将函数addPercet
本身给ppaste
这个变量,而不是调用addPercent
这个函数。当我们输入ppaste
,不加括号时,就会出现函数的内容,如下所示:
|
|
其实在R中,只输入函数本身,不输入括号,都会显示函数的内容,再看如下代码:
|
|
函数的返回结果return()
还以前面的addPercent()
函数为例说明一下,它的代码如下所示:
|
|
在大括号中最后一行是return(result)
,把它删掉,再运行代码,如下所示:
|
|
可以发现,addPercent(x)
没有返回结果,而print(addPercent(x))
返回了原计算结果。因此在这里看来,return(result)
这行代码是多余的,其实不一定,如果我们想要提前结束函数的运行,就会有用了,现在把addPercent()
改造一下,加入一行代码,即if(!is.numeric(x)) return(NULL)
,如下所示:
|
|
运行结果如下所示:
|
|
变量x
是数字,运行没问题,但变量y
是字符串,就出现了问题,此时reurn()
语句就派上了用场,函数判断出来了变量y
不是数字,就返回了NULL
。
函数的简化
只要函数体只包含一行代码,那么包围函数体的大括号{}
有的时候可以省略,将这行代码直接放在参数列表后面即可,如下所示:
|
|
运行结果如下所示:
|
|
现在我们使用这种方式来重写addPercent()
函数,如下所示:
|
|
运行结果如下所示:
|
|
但是,通常情况下并不推荐这么写,因为可读性太差。其实这种方式还可以继续简化,那就是匿名函数。
给函数添加更多的参数
addPercent()
会自动将传入的数字乘以100,如果待转换的数字已经是百分数了,那么就需要将其除以100,再传入参数,如下所示:
|
|
运行结果如下所示:
|
|
此时,为了更加方法,可以给addPercent()
函数再添加一个参数,用于控制数字的乘或除,例如我们添加一个mult
参数,如下所示:
|
|
运行结果如下所示:
|
|
设置默认值
还以上面的案例为例说明一下,如果我们忘记了传递mult
参数,那么就会出错,如下所示:
|
|
为避免这种情况,可以为mult
参数添加一个默认值,如下所示:
|
|
运行结果如下所示:
|
|
三点参数
如果我们再为addPercent()
函数添加一个参数用于控制保留的小数位置,此时就已经有了3个参数,参数已经比较多了,这样参数的传入列表会很长,R中为此有一个很好的解决方案,就是三点参数(...
),其实从R的很多函数中我们就能看到这种形式,例如我们在pheatmap
这个包中的pheatmap()
这个函数中就能看到这种形式,如下所示:
|
|
可以看到,最后就是三个点(...
),说明参数列表还有,省略了。
现在使用这种三点参数来修改addPercent()
函数,其格式如下所示:
|
|
运行结果如下所示:
|
|
将函数当作参数
在R中,可以将函数当作参数,例如在apply()
系列函数中就能遇到。在前面的案例中,addPercent()
中使用了round()
函数,但是如果我们想要使用其它的函数,例如signif()
,那么就可以将这个函数当作参数,具体的实现形式如下所示:
|
|
这种形式下,在percent <- FUN(x*mult, ...)
这一步,默认情况下使用的是round()
函数,如果要使用signif()
这个函数,使用addPercent()
时,直接在FUN=
中直接添加上signif
即可,如下所示:
|
|
计算结果如下所示:
|
|
现在解释一下这个函数的调用过程:
- 向量
x
乘以mult
(默认是100); - R将
signif()
函数的代码传递给FUN
参数,这样FUN()
就成了signif()
函数的一份拷贝,功能和行为都与之相同; - R接收参数
digits
并将其传递给FUN()
。
在这里需要注意的是,FUN=signif
这个参数中,signif
并没有添加小括号,如果添加了小括号,就是将signif()
函数的调用结果而非函数本身赋值给参数FUN
。
匿名函数
在上面的案例中,参数FUN
其实可以传递各种参数,甚至也可以没有函数名称,直接复制代码即可,因此FUN
参数除了使用函数名进行赋值外,还可以直接将代码放在FUN
中,这种直接放代码的形式就是以匿名函数的方式进行传递的,所谓的匿名函数(Anonymous Function
)就是指没有名称的函数,看下面的案例:
|
|
运行结果如下所示:
|
|
其实还可以继续简化,如下所示:
|
|
计算结果如下所示:
|
|
匿名函数的使用前提是,①函数本身代码很短,②只会使用一次,不会用在别的地方。
函数匹配
前面提到的“将函数作为参数”案例中,通过将函数名将代码传递一个参数,这就意味着,假如有一个与函数名相同的对象,就会出错,例如我们在调用addPercent()
函数之前,再定义一个名为round
的对象,如下所示:
|
|
此时再调用addPercent()
函数,如下所示:
|
|
运行结果如下所示:
|
|
此时就出错了,因为R没有将round()
函数传递给FUN
,而是把向量round
赋予给了FUN
,为了避免这种情况,可以调用一下match.fun()
函数,如下所示:
|
|
运行结果如下所示:
|
|
match.fun
函数会查找与名称round
相匹配的函数,并将代码复制给FUN
,而不会找到round
向量,另外,match.fun()
还支持字符对象,例如FUN='round'
这样传递参数也是有效的。
处理作用域
在前面的案例中,我们只使用了Workspace,也就是说,创建的每一个以对象都存在于整个环境中,这个环境被称为全局环境(Global Environment)。在前面的案例中,函数内部的 一些参数,例如x,mult和FUN并非都是创建在Workspace中的对象,它们是在函数内部创建的对象,这些变量在退出函数回到Workspace后都无法使用了,看下面的一个案例:
首选创建一个对象x
和函数test()
,如下所示:
|
|
运行结果如下所示:
|
|
从上面的案例可以发现,test()
这个函数的功能就是接收一个参数x
,输出到控制台,然后将其删除,并再次输出。函数内部虽然已经使用了rm(x)
来删除这个x
,但是x
还是能输出,不过,两次输出的x
内容并不一样。
函数的检索路径
在一个函数被调用时,它将首选创建一个临时的本地环境(Local Environment),这个本地环境嵌套在全局环境中,这意味着本地环境中仍然可以访问全局环境内的对象。只要函数执行完毕,本地环境就会立即释放,同时其中的所有对象也会被销毁。
换一种说法就是:函数创建的环境始终位于调用它的环境之内,而调用它的环境被称为父环境(Parent Environment)。所以,当我们从Worksapce中通过脚本或命令行调用某个函数时,它的父环境恰好为全局环境。
下图就是test()
函数调用的原理:
外层的大矩形表示全局环境,而内层的灰色矩形则表示test函数的本地环境。在全局环境中,我们将对象x
赋值为1:5
,而在调用函数内部,则另外创建了一个参数x
,赋值为5:1
,这个参数成为了本地环境中的一个对象。
当R在代码中发现了一个对象x
时,它将首先检索本地环境。而恰好能够找到参数x
,所以它将被用在第一次的cat()
调用中,接下来一行,R移除了这个对象x
。所以,当R到达第三行时,就再也找不到本地环境中的对象x
了。
此时,R会顺序环境栈上移,到达全局环境,并在其中查找名为x
的对象,由于其中也恰好有x
,因此它将被用于第二次的cat()
调用。
如果在函数内部使用rm()
,在默认情况下,它只会删除位于函数中的对象,这可以避免在函数中使用更大范围数据集所带来的内存消耗风险,因为我们可以在使用这个对象后立即将其删除,而无需等待函数执行完毕。
使用内部函数
test()
函数出现的调用全局环境对象的问题其实是没有意义的,因为我们从一开始就应该避免函数对全局环境对象的依赖。事实上,R背后隐藏的整个概念都不支持将全局变量应用到函数内部。R作为一门函数式编程语言,它的一个主要思想就是在于任何函数的输出结果都不能依赖于外部环境,而仅仅是由传入的参数决定。只要各个参数的值相同,结果就不会发生变化。这种操作的优势在于,有时候我们想要在某个函数内部重复地执行某种操作,但离开这个函数后,这一操作又是没有意义的。
假设我们要比较几盏灯在半供电和全供电时亮度的差异,而用来遮挡外面太阳光的窗帘又不能完全阻挡光线的进入,因此还需要测量日光提供的亮度,然后将灯光的测量结果减去日光的亮度,来修正最终的结果。
要计算半供电时的灯光效率,可以实现下面的函数:
|
|
在ccalculate.eff()
函数内部,可以看到有另外一个函数的定义:min.base()
,这个函数定义在calculate.eff()
函数的本地环境中,并且也会在离开函数时销毁,也就是说,它并不存在于Workspace内。
我们可以像下面这样调用这个函数:
|
|
计算结果如下所示:
|
|
从源代码中可以看到,min.base()
的定义中使用了对象control
,但这个对象并没有出现在函数的参数列表中,其原因可以看一下这个函数的调用过程,如下所示:
调用过程如下所示:
- 函数
calculate.eff()
创建了一个本地环境,包含对象x
(其值为fifty
),y
(其值为hundred
),control
(值为nothing
),以及函数min.base()
; - 函数
min.base()
在calculate.eff()
函数内创建了一个新的本地环境,包含对象z
,值为x
; min.base()
在calculate.eff()
的环境内查找对象control
,计算其中每个元素的平均值,并将z
的每个元素减去这个平均值,之后返回结果;- 与前一个过程相同,只是
z
的值换成了y
。 3
和4
的结果相除,结果返回到全局环境。
本地环境所嵌入的环境实际上是函数被定义的地方,而非调用的地方。假设我们在calculate.eff()
内使用addPercent()
来格式化数字,那么addPercent()
所创建的本地环境不会被嵌入在calculate.eff()
中,而是在全局环境内,也就是addPercent()
被定义的地方。
方法分配
关于函数的另外一个问题就是函数的方法问题。因为理解了这方面的知识,才能理解函数如何能够根据传入参数的类型来确定返回不同的结果。R中有一个机制被称为通用函数系统
(Generic Function System),它允许用户使用相同的名称调用不同的函数。
例如,当我们输出一个列表时,输出会以行的方式进行排列,当我们输出一个数据框的时候,则会以列的方式显示,由此可见,print()
函数对待列表和数据框的方式是不同的,但是所用的函数却是一样的。
现在看一下print()
函数的代码,如下所示:
|
|
最后两行可以忽略,它表示的是外层空间
的东西,供R语言的高手使用,第1行可以看出来,print()
这个函数的函数体只有一行代码。
像这种什么都不做,仅仅是将对象正确地传递给其他函数的函数被称为通用函数
(Generic Function
)。print()
就是一个通用函数。真正完成实际工作的函数被称方法
(Methods
)。可见,所有的方法都是函数,但并不是每个函数都是方法。
通过UseMethod
调用方法
print()
仅靠那一行代码肯定是完成无法完成不同方式打印向量、数据框、列表等复杂任务的,真正完成的其实是UseMethod()
这个函数,这个函数做的所有事情就是告诉R查找一个能够处理传入参数x
类型相匹配的函数。R会完整地遍历定义的函数名称,查找以print
形状,后面接一个点号加上对象类型名的函数。
我们也可以通过apropos('print\\.')
的命令来实现这个过程,如下所示:
|
|
apropos()
括号内的引号之间可以添加正则表达式,这与grep()
函数非常类似。假如我们要打印一个数据框,那么R将查找函数print.data.frame()
,并使用它来打印作为参数传入的对象,我们可以手工调用这个函数,如下所示:
|
|
运行结果如下所示:
|
|
函数的输出结果与调用通过的print(small.one)
函数是完全相同的,因为print()
将打印small.one
的任务完全交给了print.data.frame()
函数来完成。
使用默认方法
对于列表来说,你可能会觉得print.list()
函数承担了打印任务,但实际上print.list()
函数并不存在,此时R会忽略对象的类型,直接调用默认方法print.default()
来打印列表。
许多通过函数都有一个默认方法,当无法调用特定方法执行时将派上用场,如果存在默认方法,那么它的名称必然是函数名后面加点,再加上default
。我们来看一下默认方法打印small.one
这个变量的结果:
|
|
运行结果如下所示:
|
|
实现自己的通用函数
用户自己也可以实现自己的通用函数。现在看前面的addPercent()
函数,它的代码如下所示:
|
|
对于addPercent()
函数来说,传入的参数x
不能是字符向量,因为它无法参与后面的乘法运算,但可以利用方法分配机制,实现一个特定的函数来处理这一问题,如下所示:
|
|
需要注意的是,这里的对象不是向量,而是字符,同样的原理,之前那个addPercent(0
函数也应该变成针对特定类型对象的方法,将名称改为addPercent.numeric
。在实现方法分配时,如果代码不是过长,应该尽量把所有函数的实现放在同一个脚本文件中,这样只要运行一个脚本,就可以使用完整的函数功能。
接着定义最外层的addPercent()
函数,如下所示:
|
|
函数只定义了两个参数,x
和...
,...
参数确保在addPercent.numeric()
函数中使用的其它参数依然有效,这些附加参数将会原封不动地传递给内部调用的函数。当把完整的脚本文件发送给控制台后,就哦可以向addPercent()
函数传入字符向量或数值向量了,如下所示:
|
|
运行结果如下所示:
|
|
其中,addPercent(small.one)
这行代码出错。出错的信息显示,没有适当的方法用于处理传入的数据框,此时 们还可以修改这种错误提示,如下所示:
|
|
这段代码的功能在于显示一条信息,这条信息相对于R默认的错误信息更容易理解,现在运行完整代码,如下所示:
|
|
结果如下所示:
|
|
函数返回多个结果
当函数需要返回多个结果时,通常用list
的形式返回结果,如下所示:
|
|
运行后如下所示:
|
|
optimize()
解函数方程
用到的函数是optimize()
,先看一个案例:
经济学的一个基本模型就是随着价格上涨,产品的销量会下降,可以使用如下的函数来表示:
|
|
那么期望收益就是产品价格与销量的乘积,如下所示:
|
|
现在使用curve()
函数将这两个函数画出来,它接收一个函数作为参数,并绘制出一定范围内的函数图像,我们假设定价范围在50美元到60美元之间,那么这个图像如下所示:
|
|
从上图的右图可以看出来,收益明显有一个最高点,因此现在使用R中的optimize()
函数计算出这个最大值,如果要使用optimize()
这个函数,要输入它目标函数(这里是revenue()
)和区间(这里是50~150
),默认情况下,optimize()
计算的是最小值,但此时我们要计算最大值,就需要调整一下,如下所示:
|
|
计算结果如下所示:
|
|
replicate()
函数
replicate
函数与rep
函数类似,rep
函数的功能是将一个参数重复数次,如下所示:
|
|
而replicate
函数则是把某个表达式重复计算数次,多数情况下,它们的计算结果都相同,除非是使用了随机数时才有可能不同,如下所示:
|
|
replicate
函数的这种功能在比较复杂的例子中使用很广,例如Monte Carlo个分析中。
现在看一个比较简单的案例。
在这个案例中,我们会分析某人上下班时使用不同交通工具所花费的时间,我们在这个案例中会创建一个time_for_commute
函数,这个函数用sample
随机挑选一种交通工具(小汽车、公交车或自行车),然后使用rnorm
或rlnorm
找到一个正态分布或对数正态分布的行程时间,代码如下所示:
|
|
运行结果如下所示:
|
|
代码解释:switch
语句很难使这个函数被量化,这就说明,为了找到上下班时间的分布,我们需要多次调用time_for_commute
来生成每天的数据。
R函数是一种弱类型函数
函数一旦定义了,就可以被调用,调用语法为:函数名(参数1,参数2,...)
,前面已经提到,此处略去。
R中的函数不是强类型的,因此R中的函数非常灵活。不是强类型是说,在R中调用函数之前,输入函数中的对象类型是不固定的。即使我们设计了一个函数,它是针对标题运算的,例如+
,当函数+
作用到向量上时,它也会自动拓展以适用于向量运算,例如,我们可以运行以下代码,而不必对函数做任何修改:
|
|
运行结果如下所示:
|
|
上面是我们定义了一个函数add()
,它是对两个数字进行求和,当我们传递了一个向量时,函数会分别将向量的两个元素与另一个参数相加。现在们继续看另外一个案例,如下所示:
|
|
结果如下所示:
|
|
在上面的这个案例中,我们没有检查输入类型,add()
函数便可以将两个参数代入表达式中进行运算。其中,as.Date( )
创建了一个 Date 对象,用来表示日期。这里没有对add()
)函数进行任何更改,它就可以
完美地作用于对象 Date。只有在两个参数上+
没有被很好地定义时,函数才会失效,再看一个案例:
|
|
参考资料
- R语言轻松入门与提高 [法]Andrie de Vries ,[比利时]Joris Mey [法] Andrie de Vries 著
- 学习R.[美] Richard,Cotton 著刘军 译
- R语言编程指南.任坤