很早之前就据说过了函数式编程,印象中是一种很晦涩难懂的编程模式,但却一直没有去进行了解。html
刚好这周组内的周会轮到我主持,一时也没想到要分享什么。灵光一闪,就选定函数式编程这个主题吧,反正组里的同事都没有学过,只须要讲解入门方面的知识就好,也正好能够借这个机会逼迫本身去学习下这种新的编程方式。python
通过初步了解,发现支持函数式编程的语言挺多的,除了像Lisp、Scheme、Haskell、Erlang这样专用的函数式编程语言,咱们经常使用的好多通用型编程语言(如Java、Python、Ruby、Javascript等)都支持函数式编程模式。考虑了下实际状况,最终仍是选择Python做为函数式编程的入门语言,由于组内同事都熟悉Python,以此做为切入点不会产生太大困难。linux
通过查询资料和初步学习,对函数式编程有了些概念,通过整理,便造成了分享PPT。程序员
如下即是此次分享的内容。express
一般,咱们在新学习一门技术或者编程语言的时候,一般都会先从相关概念和特性入手。对于新接触函数式编程的人来讲,可能会想知道以下几点:编程
可是我此次分享却没有按照这个思路,由于我感受在一开始就向听众灌输太多概念性的东西,反倒会让听众感到迷糊。由于通过查询资料发现,对于什么是函数化编程,很难能有一个协调一致的定义。并且因为我也是新接触,自身的理解可能会存在较大的误差。ruby
所以,我决定分享内容尽可能从你们熟悉的命令式编程切入,经过大量实例来向听众展示函数式编程思惟方式的不一样之处。在这以后,再回过头看这几个问题,相信听众应该都会有更深入的理解。bash
考虑到实际状况,本次分享但愿能达成的目标是:微信
首先从你们熟悉的命令式编程开始,咱们先回顾下平时在写代码时主要的情景。编程语言
其实,无论咱们的业务代码有多复杂,都离不开如下几类操做:
固然,这只是部分操做类型,除此以外还应该有类和模块、异常处理等等。但考虑到是入门,咱们就先只关注上面这三种最多见的操做。
对应地,函数式编程也有本身的关键字。在Python语言中,用于函数式编程的主要由3个基本函数和1个算子。
使人惊讶的是,仅仅采用这几个函数和算子就基本上能够实现任意Python程序。
固然,能实现是一回事儿,实际编码时是否这么写又是另一回事儿。估计要真只采用这几个基本单元来写全部代码的话,无论是在表达上仍是在阅读上应该都挺别扭的。不过,尝试采用这几个基本单元来替代上述的函数定义、条件控制、循环控制等操做,对理解函数式编程如何经过函数和递归表达流程控制应该会颇有帮助。
在开始尝试将命令式编程转换为函数式编程以前,咱们仍是须要先熟悉下这几个基本单元。
lambda这个关键词在不少语言中都存在。简单地说,它能够实现函数建立的功能。
以下即是lambda的两种使用方式。
func1 = lambda : <expression()>
func2 = lambda x : <expression(x)>
func3 = lambda x,y : <expression(x,y)>复制代码
在第一条语句中,采用lambda建立了一个无参的函数func1。这和下面采用def建立函数的效果是相同的。
def func1():
<expression()>复制代码
在第二条和第三条语句中,分别采用lambda建立了须要传入1个参数的函数func2,以及传入2个参数的函数func3。这和下面采用def
建立函数的效果是相同的。
def func2(x):
<expression(x)>
def func3(x,y):
<expression(x,y)>复制代码
须要注意的是,调用func1的时候,虽然不须要传入参数,可是必需要带有括号()
,不然返回的只是函数的定义,而非函数执行的结果。这个和在ruby中调用无参函数时有所不一样,但愿ruby程序员引发注意。
>>> func = lambda : 123
>>> func
<function <lambda> at 0x100f4e1b8>
>>> func()
123复制代码
另外,虽然在上面例子中都将lambda建立的函数赋值给了一个函数名,但这并非必须的。从下面的例子中你们能够看到,不少时候咱们都是直接调用lambda建立的函数,而并无命名一个函数,这也是咱们常据说的匿名函数的由来。
map()
函数的常见调用形式以下所示:
map(func, iterable)复制代码
map()
须要两个必填参数,第一个参数是一个函数名,第二个参数是一个可迭代的对象,如列表、元组等。
map()
实现的功能很简单,就是将第二个参数(iterable)中的每个元素分别传给第一个参数(func),依次执行函数获得结果,并将结果组成一个新的list
对象后进行返回。返回结果永远都是一个list
。
简单示例以下:
>>> double_func = lambda s : s * 2
>>> map(double_func, [1,2,3,4,5])
[2, 4, 6, 8, 10]复制代码
除了传入一个可迭代对象这种常见的模式外,map()
还支持传入多个可迭代对象。
map(func, iterable1, iterable2)复制代码
在传入多个可迭代对象的状况下,map()
会依次从全部可迭代对象中依次取一个元素,组成一个元组列表,而后将元组依次传给func;若可迭代对象的长度不一致,则会以None进行补上。
经过如下示例应该就比较容易理解。
>>> plus = lambda x,y : (x or 0) + (y or 0)
>>> map(plus, [1,2,3], [4,5,6])
[5, 7, 9]
>>> map(plus, [1,2,3,4], [4,5,6])
[5, 7, 9, 4]
>>> map(plus, [1,2,3], [4,5,6,7])
[5, 7, 9, 7]复制代码
在上面的例子中,之因此采用x or 0
的形式,是为了防止None + int
出现异常。
须要注意的是,可迭代对象的个数应该与func的参数个数一致,不然就会出现异常,由于传参个数与函数参数个数不一致了,这个应该比较好理解。
>>> plus = lambda x,y : x + y
>>> map(plus, [1,2,3])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes exactly 2 arguments (1 given)复制代码
另外,map()
还存在一种特殊状况,就是func为None。这个时候,map()
仍然是从全部可迭代对象中依次取一个元素,组成一个元组列表,而后将这个元组列表做为结果进行返回。
>>> map(None, [1,2,3,4])
[1, 2, 3, 4]
>>> map(None, [1,2,3,4], [5,6,7,8])
[(1, 5), (2, 6), (3, 7), (4, 8)]
>>> map(None, [1,2,3,4], [5,6,7])
[(1, 5), (2, 6), (3, 7), (4, None)]
>>> map(None, [1,2,3,4], [6,7,8,9], [11,12])
[(1, 6, 11), (2, 7, 12), (3, 8, None), (4, 9, None)]复制代码
reduce()
函数的调用形式以下所示:
reduce(func, iterable[, initializer])复制代码
reduce()
函数的功能是对可迭代对象(iterable)中的元素从左到右进行累计运算,最终获得一个数值。第三个参数initializer是初始数值,能够空置,空置为None时就从可迭代对象(iterable)的第二个元素开始,并将第一个元素做为以前的结果。
文字描述可能不大清楚,看下reduce()
的源码应该就比较清晰了。
def reduce(function, iterable, initializer=None):
it = iter(iterable)
if initializer is None:
try:
initializer = next(it)
except StopIteration:
raise TypeError('reduce() of empty sequence with no initial value')
accum_value = initializer
for x in it:
accum_value = function(accum_value, x)
return accum_value复制代码
再加上以下示例,对reduce()
的功能应该就能掌握了。
>>> plus = lambda x, y : x + y
>>> reduce(plus, [1,2,3,4,5])
15
>>> reduce(plus, [1,2,3,4,5], 10)
25复制代码
filter()
函数的调用形式以下:
filter(func, iterable)复制代码
filter()
有且仅有两个参数,第一个参数是一个函数名,第二个参数是一个可迭代的对象,如列表、元组等。
filter()
函数的调用形式与map()
比较相近,都是将第二个参数(iterable)中的每个元素分别传给第一个参数(func),依次执行函数获得结果;差别在于,filter()
会判断每次执行结果的bool
值,并只将bool
值为true
的筛选出来,组成一个新的列表并进行返回。
>>> mode2 = lambda x : x % 2
>>> filter(mode2, [1,2,3,4,5,6,7,8,9,10])
[1, 3, 5, 7, 9]复制代码
以上即是Python函数式编程基本单元的核心内容。
接下来,咱们就开始尝试采用新学习到的基本单元对命令式编程中的条件控制
和循环控制
进行转换。
在对条件控制
进行替换以前,咱们先来回顾下Python中对布尔表达式求值时进行的“短路”处理。
什么叫“短路”处理?简单地讲,就是以下两点:
f(x) and g(y)
中,当f(x)
为false
时,不会再执行g(y)
,直接返回false
f(x) or g(y)
中,当f(x)
为true
时,不会再执行g(y)
,直接返回true
结论是显然易现的,就再也不过多解释。
那么,对应到条件控制语句,咱们不难理解,以下条件控制语句和表达式是等价的。
# flow control statement
if <cond1>: func1()
elif <cond2>: func2()
else: func3()复制代码
# Equivalent "short circuit" expression
(<cond1> and func1()) or (<cond2> and func2()) or (func3())复制代码
经过这个等价替换,咱们就去除掉了if/elif/else
关键词,将条件控制语句转换为一个表达式。那这个表达式和函数式编程有什么关系呢?
这时咱们回顾上面讲过的lambda
,会发现lambda
算子返回的就是一个表达式。
基于这一点,咱们就能够采用lambda
建立以下函数。
>>> pr = lambda s:s
>>> print_num = lambda x: (x==1 and pr("one")) \
.... or (x==2 and pr("two")) \
.... or (pr("other"))
>>> print_num(1)
'one'
>>> print_num(2)
'two'
>>> print_num(3)
'other'复制代码
经过函数调用的结果能够看到,以上函数实现的功能与以前的条件控制语句实现的功能彻底相同。
到这里,咱们就实现了命令式条件控制语句向函数式语句的转换。而且这个转换的方法是通用的,全部条件控制语句均可以采用这种方式转换为函数式语句。
接下来咱们再看循环控制
语句的转换。在Python中,循环控制是经过for
和while
这两种方式实现的。
for
循环语句的替换十分简单,采用map()
函数就能轻松实现。这主要是由于for
语句和map()
原理相同,都是对可迭代对象里面的每个元素进行操做,所以转换过程比较天然。
# statement-based for loop
for e in lst: func(e)
# Equivalent map()-based loop
map(func, lst)复制代码
>>> square = lambda x : x * x
>>> for x in [1,2,3,4,5]: square(x)
...
1
4
9
16
25
>>> map(square, [1,2,3,4,5])
[1, 4, 9, 16, 25]复制代码
while
循环语句的替换相比而言就复杂了许多。
下面分别是while
循环语句及其对应的函数式风格的代码。
# statement-based while loop
while <condition>:
<pre-suite>
if <break_condition>:
break
else:
<suite>
# Equivalent FP-style recursive while loop
def while_block():
<pre-suite>
if <break_condition>:
return 1
else:
<suite>
return 0
while_FP = lambda: <condition> and (while_block() or while_FP())
while_FP()复制代码
这里的难点在于,函数式while_FP
循环采用了递归的概念。当<condition>
为true
时,进入循环体,执行while_block()
;若<break_condition>
为true
时,返回1,while_FP()
调用结束;若<break_condition>
为false
时,返回0,会继续执行or
右侧的while_FP()
,从而实现递归调用;若<break_condition>
始终为false
,则会持续递归调用while_FP()
,这就实现了while
语句中一样的功能。
为了对函数式的while
循环有更深入的理解,能够再看下以下示例。这个例子是在网上找的,实现的是echo
功能:输入任意非"quit"字符时,打印输入的字符;输入"quit"字符时,退出程序。
➜ PythonFP python pyecho.py
IMP -- 1
1
IMP -- 2
2
IMP -- abc
abc
IMP -- 1 + 1
1 + 1
IMP -- quit
quit
➜ PythonFP复制代码
以下即是分别采用过程式和函数式语句实现的"echo"功能。
# imperative version of "echo()"
def echo_IMP():
while 1:
x = raw_input("IMP -- ")
print x
if x == 'quit':
break
echo_IMP()复制代码
def monadic_print(x):
print x
return x
# FP version of "echo()"
echo_FP = lambda: monadic_print(raw_input("FP -- "))=='quit' or echo_FP()
echo_FP()复制代码
到此为止,咱们对函数式编程总算有了点认识,到达以前设定的目标应该是没有问题了,看来函数式编程也并无想象中的那么难懂。
然而,这都只是函数式编程的皮毛而已,不信?再看下以下示例。
这个示例也是在网上找的,实现的是两个列表笛卡尔积的筛选功能,找出笛卡尔积元组集合中两个元素之积大于25的全部元组。
bigmuls = lambda xs,ys: filter(lambda (x,y):x*y > 25, combine(xs,ys))
combine = lambda xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs)))
dupelms = lambda lst,n: reduce(lambda s,t:s+t, map(lambda l,n=n: [l]*n, lst))
print bigmuls([1,2,3,4],[10,15,3,22])
[(3, 10), (4, 10), (2, 15), (3, 15), (4, 15), (2, 22), (3, 22), (4, 22)]复制代码
虽然这个例子中lambda/map/reduce/filter
都是咱们已经比较熟悉了的基本单元,可是通过组合后,理解起来仍是会比较吃力。
看到这里,有的同窗就开玩笑说我这标题名称很是贴切,《Python的函数式编程--从入门到“放弃”》,由于之后在工做中应该也不会再尝试使用函数式编程了,^_^。
不过,我仍是以为函数式编程挺有意思的,更高级的特性后面值得再继续学习。即便代码不用写成pure函数式风格,但在某些时候局部使用lambda/map/reduce/filter
也能大大简化代码,也是一个不错的选择。
另外,经过这次分享,再次切身体会到了教授是最好的学习方式,只有当你真正能将一个概念讲解清楚的时候,你才算是掌握了这个概念。
笔名九毫,英文名Leo Lee。
专一于软件测试领域和测试开发技术,享受在墙角安静地debug,也喜欢在博客上分享文字。
我的博客:debugtalk.com
我的微信公众号:DebugTalk