Python函数式编程指南(一):函数式编程概述


Posted in Python onJune 24, 2015

1. 函数式编程概述

1.1. 什么是函数式编程?

函数式编程使用一系列的函数解决问题。函数仅接受输入并产生输出,不包含任何能影响产生输出的内部状态。任何情况下,使用相同的参数调用函数始终能产生同样的结果。

在一个函数式的程序中,输入的数据“流过”一系列的函数,每一个函数根据它的输入产生输出。函数式风格避免编写有“边界效应”(side effects)的函数:修改内部状态,或者是其他无法反应在输出上的变化。完全没有边界效应的函数被称为“纯函数式的”(purely functional)。避免边界效应意味着不使用在程序运行时可变的数据结构,输出只依赖于输入。

可以认为函数式编程刚好站在了面向对象编程的对立面。对象通常包含内部状态(字段),和许多能修改这些状态的函数,程序则由不断修改状态构成;函数式编程则极力避免状态改动,并通过在函数间传递数据流进行工作。但这并不是说无法同时使用函数式编程和面向对象编程,事实上,复杂的系统一般会采用面向对象技术建模,但混合使用函数式风格还能让你额外享受函数式风格的优点。

1.2. 为什么使用函数式编程?

函数式的风格通常被认为有如下优点:

1.逻辑可证
这是一个学术上的优点:没有边界效应使得更容易从逻辑上证明程序是正确的(而不是通过测试)。
2.模块化
函数式编程推崇简单原则,一个函数只做一件事情,将大的功能拆分成尽可能小的模块。小的函数更易于阅读和检查错误。
3.组件化
小的函数更容易加以组合形成新的功能。
4.易于调试
细化的、定义清晰的函数使得调试更加简单。当程序不正常运行时,每一个函数都是检查数据是否正确的接口,能更快速地排除没有问题的代码,定位到出现问题的地方。
5.易于测试
不依赖于系统状态的函数无须在测试前构造测试桩,使得编写单元测试更加容易。
6.更高的生产率
函数式编程产生的代码比其他技术更少(往往是其他技术的一半左右),并且更容易阅读和维护。

1.3. 如何辨认函数式风格?

支持函数式编程的语言通常具有如下特征,大量使用这些特征的代码即可被认为是函数式的:

函数是一等公民

函数能作为参数传递,或者是作为返回值返回。这个特性使得模板方法模式非常易于编写,这也促使了这个模式被更频繁地使用。
以一个简单的集合排序为例,假设lst是一个数集,并拥有一个排序方法sort需要将如何确定顺序作为参数。
如果函数不能作为参数,那么lst的sort方法只能接受普通对象作为参数。这样一来我们需要首先定义一个接口,然后定义一个实现该接口的类,最后将该类的一个实例传给sort方法,由sort调用这个实例的compare方法,就像这样:

#伪代码

interface Comparator {

    compare(o1, o2)

}

lst = list(range(5))

lst.sort(Comparator() {

    compare(o1, o2) {

        return o2 - o1 //逆序

})

可见,我们定义了一个新的接口、新的类型(这里是一个匿名类),并new了一个新的对象只为了调用一个方法。如果这个方法可以直接作为参数传递会怎样呢?看起来应该像这样:
def compare(o1, o2): 

    return o2 - o1 #逆序 

lst = list(range(5)) 

lst.sort(compare)

请注意,前一段代码已经使用了匿名类技巧从而省下了不少代码,但仍然不如直接传递函数简单、自然。

匿名函数(lambda)

lambda提供了快速编写简单函数的能力。对于偶尔为之的行为,lambda让你不再需要在编码时跳转到其他位置去编写函数。
lambda表达式定义一个匿名的函数,如果这个函数仅在编码的位置使用到,你可以现场定义、直接使用:

lst.sort(lambda o1, o2: o1.compareTo(o2))

相信从这个小小的例子你也能感受到强大的生产效率:)

封装控制结构的内置模板函数

为了避开边界效应,函数式风格尽量避免使用变量,而仅仅为了控制流程而定义的循环变量和流程中产生的临时变量无疑是最需要避免的。
假如我们需要对刚才的数集进行过滤得到所有的正数,使用指令式风格的代码应该像是这样:

lst2 = list()

for i in range(len(lst)): #模拟经典for循环

    if lst[i] > 0:

        lst2.append(lst[i])

这段代码把从创建新列表、循环、取出元素、判断、添加至新列表的整个流程完整的展示了出来,俨然把解释器当成了需要手把手指导的傻瓜。然而,“过滤”这个动作是很常见的,为什么解释器不能掌握过滤的流程,而我们只需要告诉它过滤规则呢?
在Python里,过滤由一个名为filter的内置函数实现。有了这个函数,解释器就学会了如何“过滤”,而我们只需要把规则告诉它:

lst2 = filter(lambda n: n > 0, lst)

这个函数带来的好处不仅仅是少写了几行代码这么简单。
封装控制结构后,代码中就只需要描述功能而不是做法,这样的代码更清晰,更可读。因为避开了控制结构的干扰,第二段代码显然能让你更容易了解它的意图。
另外,因为避开了索引,使得代码中不太可能触发下标越界这种异常,除非你手动制造一个。

函数式编程语言通常封装了数个类似“过滤”这样的常见动作作为模板函数。唯一的缺点是这些函数需要少量的学习成本,但这绝对不能掩盖使用它们带来的好处。

闭包(closure)

闭包是绑定了外部作用域的变量(但不是全局变量)的函数。大部分情况下外部作用域指的是外部函数。

闭包包含了自身函数体和所需外部函数中的“变量名的引用”。引用变量名意味着绑定的是变量名,而不是变量实际指向的对象;如果给变量重新赋值,闭包中能访问到的将是新的值。

闭包使函数更加灵活和强大。即使程序运行至离开外部函数,如果闭包仍然可见,则被绑定的变量仍然有效;每次运行至外部函数,都会重新创建闭包,绑定的变量是不同的,不需要担心在旧的闭包中绑定的变量会被新的值覆盖。
回到刚才过滤数集的例子。假设过滤条件中的 0 这个边界值不再是固定的,而是由用户控制。如果没有闭包,那么代码必须修改为:

class greater_than_helper:

    def __init__(self, minval):

        self.minval = minval

    def is_greater_than(self, val):

        return val > self.minval

 

def my_filter(lst, minval):

    helper = greater_than_helper(minval)

    return filter(helper.is_greater_than, lst)

请注意我们现在已经为过滤功能编写了一个函数my_filter。如你所见,我们需要在别的地方(此例中是类greater_than_helper)持有另一个操作数minval。
如果支持闭包,因为闭包可以直接使用外部作用域的变量,我们就不再需要greater_than_helper了:

def my_filter(lst, minval): 

    return filter(lambda n: n > minval, lst)

可见,闭包在不影响可读性的同时也省下了不少代码量。

函数式编程语言都提供了对闭包的不同程度的支持。在Python 2.x中,闭包无法修改绑定变量的值,所有修改绑定变量的行为都被看成新建了一个同名的局部变量并将绑定变量隐藏。Python 3.x中新加入了一个关键字 nonlocal 以支持修改绑定变量。但不管支持程度如何,你始终可以访问(读取)绑定变量。

内置的不可变数据结构

为了避开边界效应,不可变的数据结构是函数式编程中不可或缺的部分。不可变的数据结构保证数据的一致性,极大地降低了排查问题的难度。
例如,Python中的元组(tuple)就是不可变的,所有对元组的操作都不能改变元组的内容,所有试图修改元组内容的操作都会产生一个异常。
函数式编程语言一般会提供数据结构的两种版本(可变和不可变),并推荐使用不可变的版本。

递归

递归是另一种取代循环的方法。递归其实是函数式编程很常见的形式,经常可以在一些算法中见到。但之所以放到最后,是因为实际上我们一般很少用到递归。如果一个递归无法被编译器或解释器优化,很容易就会产生栈溢出;另一方面复杂的递归往往让人感觉迷惑,不如循环清晰,所以众多最佳实践均指出使用循环而非递归。

这一系列短文中都不会关注递归的使用。
<第一节完>

Python 相关文章推荐
python批量修改文件名的实现代码
Sep 01 Python
Python实现从url中提取域名的几种方法
Sep 26 Python
python中self原理实例分析
Apr 30 Python
python自动zip压缩目录的方法
Jun 28 Python
Python列表推导式与生成器表达式用法示例
Feb 08 Python
Python实现针对给定单链表删除指定节点的方法
Apr 12 Python
Python连接Mssql基础教程之Python库pymssql
Sep 16 Python
利用python脚本如何简化jar操作命令
Feb 24 Python
python retrying模块的使用方法详解
Sep 25 Python
Python Django view 两种return的实现方式
Mar 16 Python
使用python实现微信小程序自动签到功能
Apr 27 Python
Python列表嵌套常见坑点及解决方案
Sep 30 Python
web.py在SAE中的Session问题解决方法(使用mysql存储)
Jun 24 #Python
Python实现LRU算法的2种方法
Jun 24 #Python
Python中线程编程之threading模块的使用详解
Jun 23 #Python
Python Property属性的2种用法
Jun 21 #Python
Python中实现三目运算的方法
Jun 21 #Python
Python中有趣在__call__函数
Jun 21 #Python
Python的装饰器模式与面向切面编程详解
Jun 21 #Python
You might like
利用PHP动态生成VRML网页
2006/10/09 PHP
php读取文件内容的方法汇总
2015/01/24 PHP
php开发时容易忘记的一些技术细节
2016/02/03 PHP
IE中jquery.form中ajax提交没反应解决方法分享
2012/09/11 Javascript
JS实现一键回顶功能示例代码
2013/10/28 Javascript
JS保留两位小数 四舍五入函数的小例子
2013/11/20 Javascript
使用jQuery的attr方法来修改onclick值
2014/07/07 Javascript
jQuery中replaceAll()方法用法实例
2015/01/16 Javascript
JavaScript随机生成信用卡卡号的方法
2015/04/07 Javascript
浅谈被jQuery抛弃的函数及替代函数
2015/05/03 Javascript
jQuery实现图片与文字描述左右滑动自动切换的方法
2015/07/27 Javascript
Jquery跨域获得Json的简单实例
2016/05/18 Javascript
js css+html实现简单的日历
2016/07/14 Javascript
JS字符串false转boolean的方法(推荐)
2017/03/08 Javascript
Vue.js 2.0学习教程之从基础到组件详解
2017/04/24 Javascript
解决BootStrap Fileinput手机图片上传显示旋转问题
2017/06/01 Javascript
JS对象与JSON互转换、New Function()、 forEach()、DOM事件流等js开发基础小结
2017/08/10 Javascript
Nodejs实现文件上传的示例代码
2017/09/26 NodeJs
微信小程序利用canvas 绘制幸运大转盘功能
2018/07/06 Javascript
jQuery实现菜单的显示和隐藏功能示例
2018/07/24 jQuery
layui layer select 选择被遮挡的解决方法
2019/09/21 Javascript
深入理解 TypeScript Reflect Metadata
2019/12/12 Javascript
VUE动态生成word的实现
2020/07/26 Javascript
js数组的基本使用总结
2021/01/18 Javascript
[57:55]EG vs Fnatic 2018国际邀请赛小组赛BO2 第一场 8.19
2018/08/21 DOTA
[01:05:59]Mineski vs Secret 2019国际邀请赛淘汰赛 败者组 BO3 第二场 8.22
2019/09/05 DOTA
Python 实现将数组/矩阵转换成Image类
2020/01/09 Python
html5 canvas绘制矩形和圆形的实例代码
2016/06/16 HTML / CSS
Canvas高级路径操作之拖拽对象的实现
2019/08/05 HTML / CSS
农村产权制度改革实施方案
2014/03/21 职场文书
求职信范文大全
2014/05/26 职场文书
2014乡镇干部纪律作风整顿思想汇报
2014/09/13 职场文书
西柏坡导游词
2015/02/05 职场文书
2015年春训学习心得体会范文
2015/03/09 职场文书
2016年师德师风学习心得体会
2016/01/12 职场文书
JS开发前端团队展示控制器来为成员引流
2022/08/14 Javascript