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 02 Python
python目录操作之python遍历文件夹后将结果存储为xml
Jan 27 Python
详细介绍Python中的偏函数
Apr 27 Python
Python简单计算文件夹大小的方法
Jul 14 Python
tensorflow实现图像的裁剪和填充方法
Jul 27 Python
Python中的Socket 与 ScoketServer 通信及遇到问题解决方法
Apr 01 Python
python 模拟银行转账功能过程详解
Aug 06 Python
pytorch masked_fill报错的解决
Feb 18 Python
Python进程Multiprocessing模块原理解析
Feb 28 Python
python 6.7 编写printTable()函数表格打印(完整代码)
Mar 25 Python
如何在windows下安装Pycham2020软件(方法步骤详解)
May 03 Python
Python3.8官网文档之类的基础语法阅读
Sep 04 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-fpm配置详解
2014/02/12 PHP
Laravel 5框架学习之向视图传送数据
2015/04/08 PHP
zen_cart实现支付前生成订单的方法
2016/05/06 PHP
YII2框架中actions的作用与使用方法示例
2020/03/13 PHP
js生成随机数之random函数随机示例
2013/12/20 Javascript
jqGrid读取选择的多行的某个属性代码
2014/05/18 Javascript
js简单设置与使用cookie的方法
2016/01/22 Javascript
JS+Canvas绘制时钟效果
2020/08/20 Javascript
javascript添加前置0(补零)的几种方法
2017/01/05 Javascript
详解node如何让一个端口同时支持https与http
2017/07/04 Javascript
微信小程序实现顶部普通选项卡效果(非swiper)
2020/06/19 Javascript
详解vue-cli构建项目反向代理配置
2017/09/07 Javascript
jQuery ajax读取本地json文件的实例
2017/10/31 jQuery
three.js实现3D影院的原理的代码分析
2017/12/18 Javascript
vue 通过下拉框组件学习vue中的父子通讯
2017/12/19 Javascript
Vue项目添加动态浏览器头部title的方法
2018/07/11 Javascript
json数据格式常见操作示例
2019/06/13 Javascript
ES6常用小技巧总结【去重、交换、合并、反转、迭代、计算等】
2019/12/21 Javascript
javascript 函数的暂停和恢复实例详解
2020/04/25 Javascript
vue 需求 data中的数据之间的调用操作
2020/08/05 Javascript
[01:15:44]首部DOTA2纪录片今日23时全网上映
2014/03/19 DOTA
Python help()函数用法详解
2014/03/11 Python
python感知机实现代码
2019/01/18 Python
Python自定义函数计算给定日期是该年第几天的方法示例
2019/05/30 Python
Python人工智能之路 jieba gensim 最好别分家之最简单的相似度实现
2019/08/13 Python
使用python绘制温度变化雷达图
2019/10/18 Python
Pytorch Tensor的统计属性实例讲解
2019/12/30 Python
Python猴子补丁知识点总结
2020/01/05 Python
Anaconda3+tensorflow2.0.0+PyCharm安装与环境搭建(图文)
2020/02/18 Python
keras处理欠拟合和过拟合的实例讲解
2020/05/25 Python
BudgetAir印度:预订航班、酒店和汽车租赁
2019/07/07 全球购物
保荐人的岗位职责
2013/11/19 职场文书
幼儿园元旦家长感言
2014/02/27 职场文书
护士求职自荐信
2015/03/25 职场文书
一篇文章弄懂Python关键字、标识符和变量
2021/07/15 Python
python编程项目中线上问题排查与解决
2021/11/01 Python