R语言学习1:R中的匿名函数、闭包与函数工厂

R语言是一门学习路线陡峭的编程语言,其中大大小小的沟壑无数。作为函数式编程语言,首先需要对R的函数进行深入的学习。

1. 为什么要进行函数式编程?R函数编程初步

简单来说,在R语言中采用函数式编程就是为了提高效率同时减少错误。下面采用例子的方式进行说明:

首先我们建立一个随机的数据框df

set.seed(1014)
df <- data.frame(replicate(6, sample(c(1:10, -99), 6, replace = TRUE)))
names(df) <- letters[1:6]
df

该数据如下图

R语言学习1:R中的匿名函数、闭包与函数工厂_第1张图片

为对该数据框中的缺失值(-99)进行替换(替换为R标准的缺失值NA),我们可以采用如下代码:

df$a[df$a == -99] <- NA

显然,该方法针对每列需重复6次,且每次复制粘贴语句需修改代码中的两个字母(复制粘贴并修改字母是很多初学者都会做的,但根据经验犯错概率也是相当高的)。

针对该问题,我们采用编写函数的方式对缺失值进行处理。

fix_missing <- function(x) {
    x[x == -99] <- NA
    x
}

fix_missing(df$a)

运行该段代码,则返回缺失值处理后的df$a,如下图

> fix_missing(df$a)
[1]  1 10  7  2  1  6

此时,该函数减少了出错误的可能性,即规范了“将某列中的-99赋值为NA”的过程,但如果此时仍采用复制粘贴6遍的方式处理每一列,仍可能出错,如下:

df$a <- fix_missing(df$a)
df$b <- fix_missing(df$b)
df$c <- fix_missing(df$c)
df$d <- fix_missing(df$d)
df$e <- fix_missing(df$e)
df$f <- fix_missing(df$g)

最后一行在手动修改过程中依然出错。此时的解决方法就是使用lapply函数(R中最重要的函数之一),对数据框df中的每列数据(本质为list,“lapply”也即“list apply”,是对每列数据应用相同的函数进行计算),具体实现如下:

df <- lapply(df, fix_missing)

此时,fix_missing函数被应用于df数据框中的每一列中,但输出结果和我们想象中存在差别,数据框df被赋值成为list的数组

R语言学习1:R中的匿名函数、闭包与函数工厂_第2张图片

可采用如下两种方式进行修改:

# method 1
df <- as.data.frame(lapply(df, fix_missing))
# method 2
df[] <- lapply(df, fix_missing) 

方法1:将缺失值处理后的list数组重新整合为data.frame

方法2:利用原有的df,获得处理缺失值后的dataframe。

个人而言,从语句简洁和内存占用两方面考虑,我会选择方法2.(当然,方法2更能体现R程序员对R运行机制的理解和自身水平)

当仅需对部分列进行处理时,采用lapply也很方便,如下

df[1:4] <- lapply(df[1:4], fix_missing)

 总而言之,学会使用函数和lapply()的组合,可以极大提高编程及后续的运行效率。

注:

#1. lapply() 使用C语言实现,因此效率较高。采用R语言的循环语句也可以实现类似的功能,代码如下:

out <- vector("list", length(df))
for(i in seq_along(df)) {
    out[[i]] <- fix_missing(df[[i]])
}
out <- as.data.frame(out)
names(out) <- letters[1:6]

#2.  最后我们讲解下lapply的另一方便应用。在数据分析中,如果想获取同一数据集的多个指标(如中位数、均值和标准差),一种方法是依次调用多个函数,如下:

median(df$a)
mean(df$a)
sd(df$a)

但如果要继续计算df$b列的结果,那就又再次进入了复制粘贴重复代码的漩涡了。此时需要用到的技术就是R语言中强大的列表存储函数功能,即可将函数名存储在列表中,而后采用lapply()对列表中的每个函数都进行一次运算,代码如下:

summary_rewrite <- function(x) {
    funs <- c(median, mean, sd)
    lapply(funs, function(f) f(x, na.rm = TRUE))
}
summary_rewrite(df$a)

2. 匿名函数和闭包

如果对python编程有初步经验,那么一定对python中lambda函数印象深刻,R中也有类似的匿名函数可以使用。使用匿名函数进行编程可省去大量重复命名函数的痛苦(很多时候编程中最痛苦的事情就是命名了...)。

上一节中讲过lapply()的用法,即

lapply(数据框/列表数组,函数名)

但很多时候往往只想对列表数组中每个列表进行很简单的操作,那根本没有必要再写一个新函数并且命名(有时也实在不好命名),那就可以采用匿名函数。比如我要实现一个功能是计算每列的均值,并且给均值平方,那么实现的代码如下:

lapply(df, function(x) mean(x, na.rm = TRUE)^2)

该语句中就采用了匿名函数,当函数体较为简单时,使用匿名函数可以提高编程效率。

当在lapply外使用匿名函数时需要注意的是需要采用括号,将函数体和输入区分开,如下:

(function(x) 3) (3)
(function(x) x + 100) (100)

以上两行代码完全是为了理解原理,实际使用中很少构造这样恶心的东西。实际上,匿名函数最重要的应用之一就是构造闭包,此处需要加重,闭包!!!

如果你有JAVA或者JS的编程经验,那么你一定对闭包有着深刻的理解,或者深恶痛绝,然而在函数编程驱动的R语言中,闭包发挥的作用还是挺大的,深刻地理解R语言的闭包并学会使用,是由R语言调包小白升级成为“会编一点”的R语言大白的重要一步

一个伟人说过:闭包就是分家单过的儿子(子函数),但是他把爸爸(父函数)的家(父函数的数据和功能-工作环境)席卷了一空,连带把爸爸也接走了。——(By SteveHuxtable in 大二)

比方说我有一项工作是要计算某个数字减n的平方,如果按照朴素的想法,我们会构造一个函数如下

minN_then_square <- function(x, n) {
    (x - n)^2
}
minN_then_square(10, 9)

但这个函数客观来说不够优雅,如果你的需求是求分别减去1,2,3,...,10后的平方结果,那么代码又会变为冗杂的

minN_then_square(10, 1)
minN_then_square(10, 2)
minN_then_square(10, 3)
minN_then_square(10, 4)
minN_then_square(10, 5)
minN_then_square(10, 6)
minN_then_square(10, 7)
minN_then_square(10, 8)
minN_then_square(10, 9)

这还算好,但如果需求是分别求10到100分别减去1,2,3,...,10后的平方结果呢,灾难出现了,解决方法1则是开始运用循环语句,不过不够优雅,解决方法二就是采用闭包及工厂模式(优雅并高效)。

minN_then_square <- function(N) {
    function(x) {
        (x - N) ^ 2
    }
}

min1_then_square <- minN_then_square(1)
min2_then_square <- minN_then_square(2)
min3_then_square <- minN_then_square(3)

此时的min1_then_square就是一个闭包。它带有两方面内容:1)父环境定义的运算; 2)N=1这一数据

此时的min1_then_square()就可以直接实现减1后平方的功能,如下:

闭包就是将父函数的数据和运算封装到一起的新函数。

下面进入关键的函数工厂的讲解。

为啥要使用函数工厂的设计模式?(当然如果你懂啥是设计模式,那就不需要再学了)

如上文所说,有些问题需要遍历数组,寻找一个方程某个系数的合适的值(如机器学习中的梯度下降算法),在这种情况下,我们需要一方面保持函数的结构的稳定,另一方面又要将大量的数代入函数进行计算。此时,如果没有稳定的新函数生成模式,那么代码会冗杂,同时易出错。举例来说,就好像可口可乐公司要尝试新logo印出来打印在饮料瓶上应该多大最合适,这个工作一定是已经制作好了打印机,而后用打印机重复性地将不同大小的logo打印在饮料瓶上,而不是每次都要重新安装一次打印机。

其实上文中,已经涉及了函数工厂的思想,后续文章会对函数工厂和闭包应用进行更深的讲解。

 

附:

#1. 由于还没有讲解变量和函数的运行环境,那么我们就简单讲下相关内容。

在R语言中,一个变量的存在范围是有限的,比如在某个函数中创建的变量i,在这个函数以外就无法访问,如下

# global env
i = 10

output_i <- function(x) {
    # local i in func
    i <- x
    i
} 

output_i(100) #输出100
i             #输出10

可见,无论在output_i函数为i赋值为多少,当脱离函数的局部环境返回全局环境是,i依然是10.

也可在局部环境中对全局环境中的变量进行修改,其方法就是将“<-”改为"<<-",但这种修改是极其危险的。

# global env
i = 10

output_i <- function(x) {
    # local i in func
    i <<- x
    i
} 

output_i(100) #输出100
i             #输出100

讨论至此,那么闭包究竟是什么呢?回归基本概念,闭包就是在创建时将父函数空间中的变量和运算打包封装的函数。每当创建一个闭包(子函数)时父函数的变量和运算都会被复制一份给闭包,且这些变量和运算是不会被垃圾回收的。

我们写一个简单的闭包,对闭包的概念进行解释

# a simple one
multi10 <- function() {
    i = 10
    
    function() {
        i <<- i * 10
        i
    }
}

multi10_1 <- multi10()
multi10_2 <- multi10()

上段代码展示了闭包的写法,闭包multi10_1和multi10_2这两个闭包实现了对数据i和函数的封装。两个闭包有着不同的环境,两个闭包中的变量i不会共享。为了实验,我们执行闭包函数以查看结果。

multi10_1() # [1] 100
multi10_1() # [1] 1000
multi10_2() # [1] 100

可见,当闭包multi10_1()中的变量i已经为1000后,另一个闭包中的i仍为10(运行函数后变为100)。

至此,我们已经对匿名函数、函数作用域,闭包等概念有了了解。在后续的R函数相关内容讲解中,匿名函数和闭包会有大量应用。

该内容会在每周一和周四夜间更新。

 

参考书目:

1. 《高级R语言编程指南》, Hadley Wickham, 2016.

你可能感兴趣的:(数据挖掘)