函数式编程这个概念我们可能或多或少都听说过,刚听说的时候不明觉厉,觉得这是一个非常黑科技的概念。但是实际上它的含义很朴实,但是延伸出来许多丰富的用法。
在早期编程语言还不是很多的时候,我们会将语言分成高级语言与低级语言。比如汇编语言,就是低级语言,几乎什么封装也没有,做一个赋值运算还需要我们手动调用寄存器。而高级语言则从这些面向机器的指令当中抽身出来,转而面向过程或者是对象。也就是说我们写代码面向的是一段计算过程或者是一个计算机当中抽象出来的对象。如果你学过面向对象,你会发现和面向过程相比,面向对象的抽象程度更高了一些,做了更加完善的封装。
在面向对象之后呢,我们还可以做什么封装和抽象呢?这就轮到了函数式编程。
函数我们都了解,就是我们定义的一段程序,它的输入和输出都是确定的。我们把一段函数写好,它可以在任何地方进行调用。既然函数这么好用,那么能不能把函数也看成是一个变量进行返回和传参呢?
OK,这个就是函数式编程最直观的特点。也就是说我们写的一段函数也可以作为变量,既可以用来赋值,还可以用来传递,并且还能进行返回。这样一来,大大方便了我们的编码,但是这并不是有利无害的,相反它带来许多问题,最直观的问题就是由于函数传入的参数还可以是另一个函数,这会导致函数的计算过程变得不可确定,许多超出我们预期的事情都有可能发生。
所以函数式编程是有利有弊的,它的确简化了许多问题,但也产生了许多新的问题,我们在使用的过程当中需要谨慎。
传入、返回函数
在我们之前介绍filter、map、reduce以及自定义排序的时候,其实我们已经用到了函数式编程的概念了。
比如在我们调用sorted进行排序的时候,如果我们传入的是一个对象数组,我们希望根据我们制定的字段排序,这个时候我们往往需要传入一个匿名函数,用来制定排序的字段。其实传入的匿名函数,其实就是函数式编程最直观的体现了:
sorted(kids, key=lambda x: x['score'])
除此之外,我们还可以返回一个函数,比如我们来看一个例子:
def delay_sum(nums):
def sum():
s = 0
for i in nums:
s += i
return s
return sum
如果这个时候我们调用delay_sum传入一串数字,我们会得到什么?
答案是一个函数,我们可以直接输出,从打印信息里看出这一点:
>>> delay_sum([1, 3, 4, 2])
<function delay_sum.<locals>.sum at 0x1018659e0>
我们想获得这个运算结果应该怎么办呢?也很简单,我们用一个变量去接收它,然后执行这个新的变量即可:
>>> f = delay_sum([1, 3, 4, 2])
>>> f()
10
这样做有一个好处是我们可以延迟计算,如果不使用函数式编程,那么我们需要在调用delay_sum这个函数的时候就计算出结果。如果这个运算量很小还好,如果这个运算量很大,就会造成开销。并且当我们计算出结果来之后,这个结果也许不是立即使用的,可能到很晚才会用到。既然如此,我们返回一个函数代替了运算,当后面真正需要用到的时候再执行结果,从而延迟了运算。这也是很多计算框架的常用思路,比如spark。
闭包
我们再来回顾一下我们刚才举的例子,在刚才的delay_sum函数当中,我们内部实现了一个sum函数,我们在这个函数当中调用了delay_sum函数传入的参数。这种对外部作用域的变量进行引用的内部函数就称为闭包。
其实这个概念很形象,因为这个函数内部调用的数据对于调用方来说是封闭的,完全是一个黑盒,除非我们查看源码,否则我们是不知道它当中数据的来源的。除了不知道来源之外,更重要的是它引用的是外部函数的变量,既然是变量就说明是动态的。也就是说我们可以通过改变某些外部变量的值来改变闭包的运行效果。
这么说有点拗口,我们来看一个简单的例子。在Python当中有一个函数叫做math.pow其实就是计算次方的。比如我们要计算x的平方,那么我们应该这样写:
math.pow(x, 2)