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调用windows api锁定计算机示例
Apr 17 Python
Python实现端口复用实例代码
Jul 03 Python
Python获取央视节目单的实现代码
Jul 25 Python
Python如何通过subprocess调用adb命令详解
Aug 27 Python
python实现的自动发送消息功能详解
Aug 15 Python
Python目录和文件处理总结详解
Sep 02 Python
Python文本处理简单易懂方法解析
Dec 19 Python
解决pycharm同一目录下无法import其他文件
Feb 12 Python
使用python实现微信小程序自动签到功能
Apr 27 Python
python获得命令行输入的参数的两种方式
Nov 02 Python
Python基础知识之变量的详解
Apr 14 Python
Python识别花卉种类鉴定网络热门植物并自动整理分类
Apr 08 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
论建造顺序的重要性
2020/03/04 星际争霸
php中计算中文字符串长度、截取中文字符串的函数代码
2011/08/09 PHP
table标签的结构与合并单元格的实现方法
2013/07/24 PHP
md5 16位二进制与32位字符串相互转换示例
2013/12/30 PHP
jQuery1.6 类型判断实现代码
2011/09/01 Javascript
extjs每个组件要设置唯一的ID否则会出错
2014/06/15 Javascript
关于延迟加载JavaScript
2015/05/05 Javascript
js电话号码验证方法
2015/09/28 Javascript
javascript 判断是否是微信浏览器的方法
2016/10/09 Javascript
微信小程序之小豆瓣图书实例
2016/11/30 Javascript
Angularjs单选改为多选的开发过程及问题解析
2017/02/17 Javascript
使用JavaScript实现点击循环切换图片效果
2017/09/03 Javascript
JS实现div模块的截图并下载功能
2017/10/17 Javascript
vue.js项目中实用的小技巧汇总
2017/11/29 Javascript
vue数组对象排序的实现代码
2018/06/20 Javascript
详解package.json版本号规则
2019/08/01 Javascript
vue使用微信JS-SDK实现分享功能
2019/08/23 Javascript
[01:55]2014DOTA2国际邀请赛 BBC正赛第一天总结
2014/07/10 DOTA
[05:15]2018年度CS GO社区贡献奖-完美盛典
2018/12/16 DOTA
[57:29]Alliance vs KG 2019国际邀请赛小组赛 BO2 第二场 8.16
2019/08/17 DOTA
python实现排序算法
2014/02/14 Python
利用Python脚本在Nginx和uwsgi上部署MoinMoin的教程
2015/05/05 Python
Python读写Json涉及到中文的处理方法
2016/09/12 Python
python使用pymysql实现操作mysql
2016/09/13 Python
Python编程深度学习绘图库之matplotlib
2018/12/28 Python
PythonWeb项目Django部署在Ubuntu18.04腾讯云主机上
2019/04/01 Python
python使用mitmproxy抓取浏览器请求的方法
2019/07/02 Python
windows中安装Python3.8.0的实现方法
2019/11/19 Python
凯特·丝蓓英国官网:Kate Spade英国
2016/11/07 全球购物
在校生汽车维修实习自我鉴定
2013/09/19 职场文书
优秀求职信范文分享
2013/12/19 职场文书
车间主任岗位职责
2014/03/16 职场文书
清明节网上祭英烈活动总结
2014/04/30 职场文书
网络营销策划方案
2014/06/04 职场文书
uniapp开发小程序的经验总结
2021/04/08 Javascript
英镑符号 £
2022/02/17 杂记