注册 登录
  • 欢迎访问"运维那点事",推荐使用Google浏览器访问,可以扫码关注本站的"微信公众号"。
  • 如果您觉得本站对你有帮助,那么可以扫码捐助以帮助本站更好地发展。

Python函数式编程:高阶函数

Python编程 彭东稳 3700次浏览 已收录 0个评论

一、函数式编程

函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。

而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

我们首先要搞明白计算机(Computer)和计算(Compute)的概念。

在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。

而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。

对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。

二、高阶函数

什么是高阶函数?简单点说就是把一个函数作为参数传入给另个一函数,这样的函数称为高阶函数;另外一个函数返回一个函数也称为高阶函数;函数式编程就是指这种高度抽象的编程范式。在Python中所有对象都是一等对象,所以函数是一等对象,也就是说定义函数跟我们定义一个字符串、一个类在内存中都是一样的。我们以实际代码为例子,一步一步深入介绍Python中函数的一些特性。

1. 变量指向函数

以Python内置的求绝对值的函数abs()为例,调用该函数用以下代码:

但是,如果只写abs呢?

可见,abs(-10)是函数调用,而abs是函数本身。

要获得函数调用结果,我们可以把结果赋值给变量:

但是,如果把函数本身赋值给变量呢?

结论就是,函数本身也可以赋值给变量,即变量可以指向函数。就是函数如果不加括号,是不会执行的,代表的是一个函数对象,它是可以作为变量来传递。

如果一个变量指向了一个函数,那么,可否通过该变量来调用这个函数?用代码验证一下:

成功!说明变量f现在已经指向了abs函数本身。直接调用abs()函数和调用变量f()完全相同。

2. 函数名也是变量

那么函数名是什么呢?函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!

如果把abs指向其他对象,会有什么情况发生?

把abs指向10后,就无法通过abs(-10)调用该函数了!因为abs这个变量已经不指向求绝对值函数而是指向一个整数10!

当然实际代码绝对不能这么写,这里是为了说明函数名也是变量。要恢复abs函数,请重启Python交互环境。

PS:由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10。

3. 函数嵌套及跨域访问

我可以在一个函数中嵌套一个或多个函数,并且被嵌套函数是可以跨域到封装域去访问变量。

执行结果:

说明:一个函数(主函数)内部是可以嵌套另一个函数(子函数)的,比如outer函数从内部嵌套了inner。一个函数本地域没有的变量,是可以跨到它的封装域(主函数与子函数之间的范围)去寻找的,比如被嵌套函数inner内部的x变量可以到封装域去获取。

4. 函数作为参数传入

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

一个最简单的高阶函数:

当我们调用add(-5, 6, abs)时,参数x,y和f分别接收-5,6和abs,根据函数定义,我们可以推导计算过程为:

用代码验证一下:

编写高阶函数,就是让函数的参数能够接收别的函数。

下面写一个复杂点的sort函数,支持升序和降序。

执行效果如下:

然后把这个功能改造成高阶函数,支持字典排序。

先写一个cmp比较函数:

执行效果如下:

改造sort函数,把cmp函数当做参数传给sort函数:

执行以下sort函数,结果如下:

可以看出这里我们把函数当做参数传递给另一个函数,称之为高阶函数。实现了解耦,数据跟方法分开了,这样我们的比较函数cmp就可以自己定义比较函数,而不需关系sort函数了。

通过把比较函数传递给sort函数,我们可以简单实现字典的排序。但是还有一个问题,我们这个sort函数无法接受其他类型的参数,比如对list进行排序。下面可以再次改造一下这个函数,比如当需要对字典进行排序时就传入cmp函数,当对列表进行排序时就无需传入cmp函数,如下:

下面我们传入一个list进行排序看看(此时无需传入cmp函数),结果如下:

5. 函数作为返回值

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:

但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:

当我们调用par_sum()时,返回的并不是求和结果,而是求和函数:

调用函数f时,才真正计算求和的结果:

在这个例子中,我们使用了嵌套函数,在函数par_sum中又定义了函数sum。并且,内部函数sum可以引用外部函数par_sum的参数和局部变量,当par_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。

请再注意一点,当我们调用par_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:

f1()和f2()的调用结果互不影响。

三、闭包

简单地讲,闭包就是引用了外部变量的内部函数,而闭包的实现正是利用了以上函数特性(高阶函数)。再来看看闭包是如何实现的:

说明:我们分析下这个流程,outer接收到’外部变量’,传给inner,作为它return的参数,最后outer返回inner函数,返回的inner函数作为变量传递给closure,最后执行closure这个函数对象,实际上是执行了inner这个函数,返回了 ‘外部变量’,这样就实现了一个简单的闭包。闭包用起来简单,实现起来可不容易。而正是因为闭包这种程序结构的存在,才有了更为强大的装饰器。

另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用才会执行。我们来看一个例子:

在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。我们可以执行以下count()看看:

然后我们调用这三个函数:

你可能认为调用f1(),f2()和f3()结果应该是1,4,9;但实际结果是:

全部都是9!原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9。

返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

再看看结果:

一个函数可以返回一个计算结果,也可以返回一个函数。

返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量,不然就会产生不可控值。


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (2)or分享 (0)
关于作者:

您必须 登录 才能发表评论!