Python--函数式编程
函数式编程就是一种抽象程度很高的编程范式。这个概念有些抽象,简单理解的话,只需要记住一点:函数式编程的一大特点是允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python并不是一种函数式编程语言,但是却为函数式编程提供了部分支持。下面,我将举一些在Python中常用的函数式编程的例子。
高阶函数
所谓高阶函数,是可以接收另一个函数作为参数的函数。这个听起来有点新奇,因为一般情况下函数的参数都是数据变量。但是,其实我在之前说道字典按值排序的时候,我们给 sorted() 函数的参数 key 赋值的时候,所赋的就是一个函数。这种将函数作为参数带入函数的用法有时会为我们提供极大的方便。
1. lambda 表达式
先来看看如何构建匿名函数。一般对函数的定义都是通过 def 关键字加函数名的形式进行。可也有一类特殊的匿名函数,没有名字,无目的地服务,但是却能返回一个可调用的函数对象,在一些特殊的应用中(比如字典的按值排序)非常方便。具体用法如下:
设计一个两数相加的加法匿名函数
lambda x, y: x + y
紧跟lambda关键字之后的是函数的参数,参数之后一个 “:” 号,后面是一个表达式,代表函数的返回值。lambda表达式只有一行,且代表了一个函数对象。
像上面这样,仅仅一个lambda表达式是没什么用的,这就好比是一个对象,却没有任何引用指向它,想要应用这个函数,需要一个引用,例如
addXY = lambda x, y: x + y print(addXY(1, 2)) # >>> 3
这个例子就再清楚不过了,lambda表达式创建了一个函数对象,引用addXY指向这个对象。
当然,lambda表达式所创建的函数对象也可以没有参数:
true = lambda :True
函数 true 没有参数,作用是返回 True
2. 高阶函数的应用
lambda表达式为我们展示了返回对象为函数的情况,那么,也就可以再深一步,看看函数对象能不能被当做参数传入函数。
看下面这个例子:
double = lambda x: x * 2 def double_add(x, y, double_fun): return double_fun(x) + double_fun(y) print(double_add(1, 2, double)) # >>> 6
先是lambda表达式创建了一个函数对象double,然后,double_add() 函数将函数double当做一个参数使用。我们发现,代码是完全没有问题的。
当然,不止是lambda表达式,任何定义的函数,它的函数名都可以以参数(实参)的形式带入其他函数,比如,下面的这三个函数的参数中就有别的函数名。
filter(), map(), 和 reduce()
初步了解了什么是高阶函数,我们就来看看Python中常用的三种高阶函数。
1. filter()
顾名思义,过滤器。语法是这样的:filter(func, seq) 也就是说,将一个序列和返回True or False的函数作为参数,通过函数来筛选序列,保留序列中使得函数值为True的的元素。具体用法如下
array = [1, 2, 3, 4, 5, 6] def isEven(num): return num % 2 == 0 for i in filter(isEven, array): print(i) # 依次输出2, 4, 6
可见,这里 filter() 函数以判断偶数的函数 isEven() 的函数名,和列表array作为参数,完成了对列表中元素是偶数的筛选,筛选的过程其实是依次将序列的每一个元素带入函数,函数会返回一个True or False,将返回True的元素作为筛选成功者。最后,filter() 函数生成的是一个可迭代对象。
2. map()
map() 函数与 filter() 类似,以函数和一个序列作为参数,将序列的每一个元素带入函数,函数的计算结果通过一个可迭代的对象返回。
a = [1, 2, 3, 4] for i in map(lambda x: x + 1, a): print(i) # 依次输出2, 3, 4, 5
这里,用lambda表达式替代了显式的函数名,当然是可以允许的。
函数要完成的计算是给序列的每个元素+1,我们发现,通过 map() 函数轻松实现了函数对于没个变量的“映射”。
此外,map() 当中也能存在不止一个序列,对于有多个序列的 map() 函数,则会并行地迭代每个序列:
a = [1, 2, 3] b = [2, 3, 4] for i in map(lambda x, y: x + y, a, b): print(i) # >>> 依次输出3, 5, 7
可见,map()是对这两个序列并行迭代,每次都计算出一个结果。
当然,其实map() 与 filter() 实现的功能我们之前讲的列表解析也可以完成,而且会更简单(详见:Python–列表解析)
3. reduce()
reduce这歌单词的含义是减少、降低的意思。Python当中实现的效果也是这样,他的语法规则如下:
reduce(func, [x1, x2, ..., xn]) = reduce(func, (func(x1, x2), [x3, ..., xn]))
有点类似于一个降维的过程,对一个序列的前两个元素执行函数的运算(当然,reduce里面使用的函数就一定是二元函数了),运算的结果作替代前两个元素加入序列,这样,整个序列就变成了n - 1长的,循环往复,直到只有一个元素时,输出结果
我们尝试用这种方法实现对一个列表的加和
from functools import reduce a = [1, 2, 3] print(reduce(lambda x, y: x + y, a)) # >>> 6
注意用reduce的时候应该先导入相关的模块。
当然,对列表加和这种运算,直接用 sum() 就行,是不必这么大费周章的,但是如果要把序列[1, 2, 3, 4]变换成整数1234,reduce就可以派上用场:
from functools import reduce print(reduce(lambda x, y: x * 10 + y, [1, 2, 3, 4])) # >>> 1234
对于这种两两迭代的运算,reduce是十分方便的。
偏函数的应用
Python中,偏函数的作用就是帮助我们固定一些函数的参数,使得代码的书写更加方便。
经常会遇到这种情况,我们的程序中经常出现某个函数,且这个函数的某一个参数经常为一个固定的值,而我出于某种原因,不想或不能设置这个值为默认参数。比如,将一个二进制字符串转换成相应的整数,应该这样做
s = "1001" print(int(s, base=2)) # >>> 9
而我现在可能需要大量应用这个函数为了方便,可以自己定义一个新函数:
def int2(s): return int(s, base=2) s = "1001" print(int2(s)) # >>> 9
而Python提供的偏函数机制就省去了我们自己定义的这个过程:
import functools int2 = functools.partial(int, base=2) s = "1001" print(int2(s)) # >>> 9
可见,偏函数能直接建立新函数。
当然,这个新函数的参数也是可以根据情况临时调整的
import functools int2 = functools.partial(int, base=2) s = "1001" print(int2(s, base=10)) # >>> 1001
应用偏函数时,一定需要警惕设置参数的时候,标清关键字,否则,会出现类似下面的错误:
import functools def f(a, b="result: "): return b + str(a) f1 = functools.partial(f, 2) print(f1()) # >>> "result: 2"
此时的偏函数 f1() 中,我们固定的参数是a,那现在这样是没有问题的。但是如果想要固定的参数是b,再这么写就会报错了:
import functools def f(a, b="result: "): return b + str(a) f1 = functools.partial(f, "new_result: ") print(f1(1)) # wrong!
尤其是参数一多,就更容易出错,所以,干脆就多“长点心”,在设置偏函数的时候,标清关键字。
闭包
闭包的概念,一向是不好理解的。所以,我先从内嵌函数说起,循序渐进。
内嵌函数
Python中允许在函数的内部再定义别的函数。我们把这种在函数内部定义的函数称为内嵌函数。比如下面这个例子:
def f_out(): def f_in(): return True return f_in() print(f_out()) # >>> True
在函数 f_out() 内部,我们定义了函数 f_in() ,函数 f_in() 的作用是返回 True ,而函数 f_out() 则是返回函数 f_in() 的结果。整个代码的意思是很清晰的。充分说明了函数内部是可以定义其他函数并在函数内部调用内嵌函数的。
由上一节Python–函数 可知,这个内嵌函数只能在外部函数的内部调用,如果在外部函数的函数体外调用,则一定会出错,道理跟在函数外部引用函数内部变量是一致的。比如:
def f_out(): def f_in(): return True return f_in() print(f_in()) # wrong!
在外部作用域调用内嵌函数,出错!
闭包
理解了内嵌函数,那就看看内嵌函数能否引用外部函数体的变量。还是上面的例子,我们略作改变:
def f_out(): a = 1 def f_in(): return a + 1 return f_in() print(f_out()) # >>> 2 print(a) # wrong!
代码第7行正确运行,结果是2.
这说明在函数 f_out() 内部调用内嵌函数 f_in() 时,位于外部函数作用域的变量a成功地被内嵌函数调用了。
代码第8行运行出错。
这道理不再重复了,还是内部变量不能被外部引用的问题。
好了,既然现在已经知道内嵌函数能够引用外部函数的变量。那我们再进一步:既然前面的函数式编程的例子告诉我们函数可以将函数作为返回值,那如果外部函数将内嵌函数作为返回值返回呢?
接下来,我们就来看一个非常经典的闭包的例子。此时,先不要管到底什么是闭包,只是看下面这个例子:
def counter(begin): count = [begin] def incr(): count[0] += 1 return count[0] return incr
上面的代码中,我们定义了一个函数 counter() 用作计数器。这个函数也是外部函数,在它里面,我们定义了函数 incr() 作为内嵌函数,incr() 引用了外部函数的变量:列表count. 这些都跟我们之前讲的例子没什么区别,最大的不同在于最后函数将它内部定义的内嵌函数返回。
这样做有什么意义呢?我们可以这样调用:
def counter(begin): count = [begin] def incr(): count[0] += 1 return count[0] return incr # 将外部函数的返回值(也就是内嵌函数)赋值给了一个外部变量 my_fun = counter(1) print(my_fun()) # >>> 2 print(my_fun()) # >>> 3 print(my_fun()) # >>> 4
我们发现,一个奇怪的现象出现了:按说,当外部函数 counter() 运行结束时,它的内部变量:列表count就应该消失才对,但是每次调用my_fun,输出是不一样的,那也就是说,列表count像是不曾消失一样,始终伴随着内嵌函数。
接下来,给出两个概念:
1. 闭包:如果在一个内部函数中,对外部函数作用域的变量(注意不是全局作用域)进行引用,那么这个内部函数就被称为闭包
2. *变量:被闭包引用的外部变量就被称为*变量。
换个角度来理解这个问题:如果计算机程序被分成:代码+数据,那么将数据保存起来再使用时好理解的,但是将代码保存起来,就不是那么好理解了。而实际上,闭包就是一种将代码(有状态)保存起来的一种方式。
闭包也是函数,却会携带一些额外的作用域(我觉得这些作用域可以理解为函数的状态),我们使用闭包的目的就是将这些有状态的函数封装起来,以便以后使用。从这个角度来看,其实闭包也是数据。
所以说,想要理解闭包,就一定要将代码同数据统一起来看,他们不是割裂的,而是“你中有我,我中有你”。理解了这一点,闭包就顺理成章了。