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算法之求n个节点不同二叉树个数
Oct 27 Python
python中将字典形式的数据循环插入Excel
Jan 16 Python
python 实现倒排索引的方法
Dec 25 Python
python使用BeautifulSoup与正则表达式爬取时光网不同地区top100电影并对比
Apr 15 Python
Python中@property的理解和使用示例
Jun 11 Python
Django中celery执行任务结果的保存方法
Jul 12 Python
python Pandas库基础分析之时间序列的处理详解
Jul 13 Python
Pycharm 文件更改目录后,执行路径未更新的解决方法
Jul 19 Python
python使用原始套接字发送二层包(链路层帧)的方法
Jul 22 Python
使用matlab或python将txt文件转为excel表格
Nov 01 Python
Python多线程多进程实例对比解析
Mar 12 Python
jupyter notebook 写代码自动补全的实现
Nov 02 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 array_flip() 删除重复数组元素专用函数
2010/05/16 PHP
PHP中几种常见的超时处理全面总结
2012/09/11 PHP
PHP中读取文件的8种方法和代码实例
2014/08/05 PHP
将PHP从5.3.28升级到5.3.29时Nginx出现502错误
2015/05/09 PHP
php 实现简单的登录功能示例【基于thinkPHP框架】
2019/12/02 PHP
JavaScript中使用replace结合正则实现replaceAll的效果
2010/06/04 Javascript
JavaScript中的console.group()函数详细介绍
2014/12/29 Javascript
jquery简单实现外部链接用新窗口打开的方法
2015/05/30 Javascript
jQuery基于ajax实现带动画效果无刷新柱状图投票代码
2015/08/10 Javascript
解决JS无法调用Controller问题的方法
2015/12/31 Javascript
JS简单编号生成器实现方法(附demo源码下载)
2016/04/05 Javascript
require、backbone等重构手机图片查看器
2016/11/17 Javascript
Vue中定义全局变量与常量的各种方式详解
2017/08/23 Javascript
vue-router history模式下的微信分享小结
2018/07/05 Javascript
vue监听键盘事件的快捷方法【推荐】
2018/07/11 Javascript
springMvc 前端用json的方式向后台传递对象数组方法
2018/08/07 Javascript
vue.js input框之间赋值方法
2018/08/24 Javascript
nodejs通过钉钉群机器人推送消息的实现代码
2019/05/05 NodeJs
vue-cli3项目配置eslint代码规范的完整步骤
2020/09/10 Javascript
一个基于flask的web应用诞生(1)
2017/04/11 Python
python:print格式化输出到文件的实例
2018/05/14 Python
django反向解析和正向解析的方式
2018/06/05 Python
对python的unittest架构公共参数token提取方法详解
2018/12/17 Python
python中执行smtplib失败的处理方法
2020/07/01 Python
使用HTML5的链接预取功能(link prefetching)给网站提速
2012/12/13 HTML / CSS
副总经理工作职责
2013/11/28 职场文书
班主任评语大全
2014/04/26 职场文书
学习雷锋精神演讲稿
2014/05/10 职场文书
教师批评与自我批评
2014/10/15 职场文书
工会经费申请报告
2015/05/15 职场文书
离婚答辩状怎么写
2015/05/22 职场文书
2015军训通讯稿大全
2015/07/18 职场文书
预备党员入党感想
2015/08/10 职场文书
小学中队委竞选稿
2015/11/20 职场文书
2016计算机专业毕业生自荐信
2016/01/28 职场文书
Windows7下FTP搭建图文教程
2022/08/05 Servers