一.什么是函数式编程?
函数式编程使用一系列的函数解决问题。函数仅接受输入并产生输出,不包含任何能影响产生输出的内部状态。任何情况下,使用相同的参数调用函数始终能产生同样的结果。
在一个函数式的程序中,输入的数据“流过”一系列的函数,每一个函数根据它的输入产生输出。函数式风格避免编写有“边界效应”(side effects)的函数:修改内部状态,或者是其他无法反应在输出上的变化。完全没有边界效应的函数被称为“纯函数式的”(purely functional)。避免边界效应意味着不使用在程序运行时可变的数据结构,输出只依赖于输入。
可以认为函数式编程刚好站在了面向对象编程的对立面。对象通常包含内部状态(字段),和许多能修改这些状态的函数,程序则由不断修改状态构成;函数式编程则极力避免状态改动,并通过在函数间传递数据流进行工作。但这并不是说无法同时使用函数式编程和面向对象编程,事实上,复杂的系统一般会采用面向对象技术建模,但混合使用函数式风格还能让你额外享受函数式风格的优点。
为什么使用函数式编程:
函数式的风格通常被认为有如下优点:
- 逻辑可证 这是一个学术上的优点:没有边界效应使得更容易从逻辑上证明程序是正确的(而不是通过测试)。
- 模块化 函数式编程推崇简单原则,一个函数只做一件事情,将大的功能拆分成尽可能小的模块。小的函数更易于阅读和检查错误。
- 组件化 小的函数更容易加以组合形成新的功能。
易于调试 细化的、定义清晰的函数使得调试更加简单。当程序不正常运行时,每一个函数都是检查数据是否正确的接口,能更快速地排除没有问题的代码,定位到出现问题的地方。 - 易于测试 不依赖于系统状态的函数无须在测试前构造测试桩,使得编写单元测试更加容易。
- 更高的生产率 函数式编程产生的代码比其他技术更少(往往是其他技术的一半左右),并且更容易阅读和维护。
下面依次介绍一些函数式编程的特性:
二.高阶函数
高阶函数英文叫Higher-order function。什么是高阶函数?
- 变量可以指向函数
如abs
是求绝对值的函数,如下:
>>>f = abs
>>>f
<built-in function abs>
由上可见函数本身也可以赋值给变量,即:变量可以指向函数。并且调用也是可以的
>>>f(-2))
2
- 函数名也是变量
函数名是什么呢?函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!
如果把abs指向其他对象,会有什么情况发生?
>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
可以发现把abs指向10后,就无法通过abs(-10)调用该函数了!因为abs这个变量已经不指向求绝对值函数而是指向一个整数10!
当然实际代码绝对不能这么写,这里是为了说明函数名也是变量。要恢复abs函数,需要重启Python交互环境。
- 传入函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。如下一个最简单的高阶函数:
def add(x, y, f):
return f(x) + f(y)
用代码验证一下:
>>> add(-5, 6, abs)
11
一些常见的高阶函数
-
map
map函数是对一个序列的每个项依次执行函数,下面是对一个序列每个项都乘以2:
>>>a=map(lambda x:x*x,[1,2,3])
>>>list(a)
[1, 4, 9]
-
reduce
reduce函数是对一个序列的每个项迭代调用函数,下面是求3的阶乘:
>>>from functools import reduce
>>>b=reduce (lambda x,y:x*y ,range(1,4))
>>>b
6
用reduce就可以用一行代码写出阶乘函数,如下:
>>>from functools import reduce
>>>g=lambda x: reduce (lambda a,b:a*b ,range(1,x+1))
>>>g(4)
24
-
filter
Python内建的filter()函数用于过滤序列。
和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素
例如,在一个list中,删掉偶数,只保留奇数,可以这么写:
def is_odd(n):
return n % 2 == 1
list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]
三.匿名函数
lambda是匿名函数,没有函数名,在闭包里和map reduce中很好用。
lambda提供了快速编写简单函数的能力。对于偶尔为之的行为,lambda让你不再需要在编码时跳转到其他位置去编写函数。 lambda表达式定义一个匿名的函数,如果这个函数仅在编码的位置使用到,你可以现场定义、直接使用
如下面是求两数乘积的函数:
lst.sort(lambda o1, o2: o1.compareTo(o2))
相信从这个小小的例子你也能感受到强大的生产效率
四.闭包
闭包就是函数中又定义函数,里面的函数可以使用外部作用域内的变量,但不是全局变量,所以一次创建外部函数,则多次调用内部函数时外部作用域内的变量还是有效的;多次运行外部函数,则会重新创建闭包,原来外部变量的值也不会受到影响,如上例可以实现每次调用内部函数是的外部变量+1操作。这里说一下作用域的事儿,原来的python2只有全局作用域和局部作用域,python3新增的nonlocal作用域就是专门针对闭包的,nonlocal声明的变量只能访问外部作用域的变量,不能访问全局作用域的变量,这样就不用全局变量搞来搞去错都不知道哪里出的了。
如下所示:
def funA():
x=0
def funB():
nonlocal x
x+=1
print (x)
return funB
a=funA()
a()
# 输出1
a()
# 输出2
如果再定义一个b=funA()
,会得到一个新的funB,不会影响之前的闭包的值
a=funA()
a()
# 1
a()
# 2
b=funA()
b()
# 1
a()
# 3
可见,闭包在不影响可读性的同时也省下了不少代码量。
五.其他特性
- 避免使用变量
为了避开边界效应,函数式风格尽量避免使用变量,而仅仅为了控制流程而定义的循环变量和流程中产生的临时变量无疑是最需要避免的。 假如我们需要对刚才的数集进行过滤得到所有的正数,使用指令式风格的代码应该像是这样:
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)
这个函数带来的好处不仅仅是少写了几行代码这么简单。 封装控制结构后,代码中就只需要描述功能而不是做法,这样的代码更清晰,更可读。因为避开了控制结构的干扰,第二段代码显然能让你更容易了解它的意图。 另外,因为避开了索引,使得代码中不太可能触发下标越界这种异常,除非你手动制造一个。 函数式编程语言通常封装了数个类似“过滤”这样的常见动作作为模板函数。唯一的缺点是这些函数需要少量的学习成本,但这绝对不能掩盖使用它们带来的好处。
- 内置的不可变数据结构
为了避开边界效应,不可变的数据结构是函数式编程中不可或缺的部分。不可变的数据结构保证数据的一致性,极大地降低了排查问题的难度。 例如,Python中的元组(tuple)就是不可变的,所有对元组的操作都不能改变元组的内容,所有试图修改元组内容的操作都会产生一个异常。 函数式编程语言一般会提供数据结构的两种版本(可变和不可变),并推荐使用不可变的版本。