一、函数式编程
函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。
我们首先要搞明白计算机(Computer)和计算(Compute)的概念。
在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。
而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。
对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。
二、高阶函数
什么是高阶函数?简单点说就是把一个函数作为参数传入给另个一函数,这样的函数称为高阶函数;另外一个函数返回一个函数也称为高阶函数;函数式编程就是指这种高度抽象的编程范式。在Python中所有对象都是一等对象,所以函数是一等对象,也就是说定义函数跟我们定义一个字符串、一个类在内存中都是一样的。我们以实际代码为例子,一步一步深入介绍Python中函数的一些特性。
1. 变量指向函数
以Python内置的求绝对值的函数abs()为例,调用该函数用以下代码:
1 2 |
>>> abs(-10) 10 |
但是,如果只写abs呢?
1 2 |
>>> abs <built-in function abs> |
可见,abs(-10)是函数调用,而abs是函数本身。
要获得函数调用结果,我们可以把结果赋值给变量:
1 2 3 |
>>> x = abs(-10) >>> x 10 |
但是,如果把函数本身赋值给变量呢?
1 2 3 |
>>> f = abs >>> f <built-in function abs> |
结论就是,函数本身也可以赋值给变量,即变量可以指向函数。就是函数如果不加括号,是不会执行的,代表的是一个函数对象,它是可以作为变量来传递。
如果一个变量指向了一个函数,那么,可否通过该变量来调用这个函数?用代码验证一下:
1 2 3 |
>>> f = abs >>> f(-10) 10 |
成功!说明变量f现在已经指向了abs函数本身。直接调用abs()函数和调用变量f()完全相同。
Tips:引用是什么?在 Python 中一切都是对象,包括整型数值,函数,类,都是对象。当我们定义一个变量 a=10 的时候,实际上在内存当中有一个地方存了值 10,然后用 a 这个变量名存了 10 所在内存位置的引用。引用就好像 C 语言里的指针,大家可以把引用理解成地址。a 只不过是一个变量名字,a 里面存的是 10 这个数值所在的地址,就是 a 里面存了数值 10 的引用。
相同的道理,当我们在 Python 中定义一个函数 def outer() 的时候,内存当中会开辟一些空间,存下这个函数的代码、内部的局部变量等等。这个 outer 只不过是一个变量名字,它里面存了这个函数所在位置的引用而已。我们还可以进行 x,y = demo 这样的操作就相当于把 outer 里存的东西赋值给 x 和 y,这样 x 和 y 都指向了 outer 函数所在的引用,在这之后我们可以用 x() 或者 y() 来调用我们自己创建的 outer(),调用的实际上根本就是一个函数,x、y 和 outer 三个变量名存了同一个函数的引用。
同时我们发现,一个函数,如果函数名后紧跟一对括号,相当于现在我就要调用这个函数,如果不跟括号,相当于只是一个函数的名字,里面存了函数所在位置的引用。
2. 函数名也是变量
那么函数名是什么呢?函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!
如果把abs指向其他对象,会有什么情况发生?
1 2 3 4 5 |
>>> 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交互环境。
PS:由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10。
3. 函数嵌套及跨域访问
我可以在一个函数中嵌套一个或多个函数,并且被嵌套函数是可以跨域到封装域去访问变量。
1 2 3 4 |
def outer(x): def inner(): print(x + 1) inner() |
执行结果:
1 2 |
>>> outer(10) 11 |
说明:一个函数(主函数)内部是可以嵌套另一个函数(子函数)的,比如outer函数从内部嵌套了inner。一个函数本地域没有的变量,是可以跨到它的封装域(主函数与子函数之间的范围)去寻找的,比如被嵌套函数inner内部的x变量可以到封装域去获取。
4. 函数作为参数传入
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
1 2 |
def add(x, y, f): return f(x) + f(y) |
当我们调用add(-5, 6, abs)时,参数x,y和f分别接收-5,6和abs,根据函数定义,我们可以推导计算过程为:
1 2 3 4 5 |
x = -5 y = 6 f = abs f(x) + f(y) ==> abs(-5) + abs(6) ==> 11 return 11 |
用代码验证一下:
1 2 |
>>> add(-5, 6, abs) 11 |
编写高阶函数,就是让函数的参数能够接收别的函数。
下面写一个复杂点的sort函数,支持升序和降序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def sort(lst, reverse=False): # reverse用来定义是升序还是降序; dst = [] for n in lst: for i, e in enumerate(dst): # 利用enumerate获得索引和值; if not reverse: if n < e: dst.insert(i, n) # 指定索引位置添加元素; break # if条件为真时使用break跳出循环避免执行for...else; else: if n > e: dst.insert(i, n) break else: # else为了给dst列表追加第一个元素,以及追加不符合for循环的元素; dst.append(n) return dst |
执行效果如下:
1 2 3 4 |
>>> sort([3,2,4,1]) [1, 2, 3, 4] >>> sort([3,2,4,1],reverse=True) [4, 3, 2, 1] |
然后把这个功能改造成高阶函数,支持字典排序。
先写一个cmp比较函数:
1 2 3 4 5 6 7 |
def cmp(x, y): if x['age'] > y['age']: return 1 if x['age'] < y['age']: return -1 if x['age'] == y['age']: return 0 |
执行效果如下:
1 2 |
>>> cmp({'age':3},{'age':6}) -1 |
改造sort函数,把cmp函数当做参数传给sort函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def sort(lst, cmp, reverse=False): dst = [] for n in lst: for i, e in enumerate(dst): if not reverse: if cmp(n, e) < 0: dst.insert(i, n) break else: if cmp(n, e) > 0: dst.insert(i, n) break else: dst.append(n) return dst |
执行以下sort函数,结果如下:
1 2 3 4 5 6 7 |
# 升序; >>> sort([{'age':3},{'age':4},{'age':1},{'age':2}],cmp,reverse=False) [{'age': 1}, {'age': 2}, {'age': 3}, {'age': 4}] # 降序; >>> sort([{'age':3},{'age':4},{'age':1},{'age':2}],cmp,reverse=True) [{'age': 4}, {'age': 3}, {'age': 2}, {'age': 1}] |
可以看出这里我们把函数当做参数传递给另一个函数,称之为高阶函数。实现了解耦,数据跟方法分开了,这样我们的比较函数cmp就可以自己定义比较函数,而不需关系sort函数了。
通过把比较函数传递给sort函数,我们可以简单实现字典的排序。但是还有一个问题,我们这个sort函数无法接受其他类型的参数,比如对list进行排序。下面可以再次改造一下这个函数,比如当需要对字典进行排序时就传入cmp函数,当对列表进行排序时就无需传入cmp函数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
def sort(lst, cmp=None, reverse=False): def default_cmp(x, y): if x > y: return 1 if x < y: return -1 if x == y: return 0 if cmp is None: cmp = default_cmp dst = [] for n in lst: for i, e in enumerate(dst): if not reverse: if cmp(n, e) < 0: dst.insert(i, n) break else: if cmp(n, e) > 0: dst.insert(i, n) break else: dst.append(n) return dst |
下面我们传入一个list进行排序看看(此时无需传入cmp函数),结果如下:
1 2 3 4 5 6 7 |
# 升序; >>> sort([3,4,1,2],reverse=False) [1, 2, 3, 4] # 降序; >>> sort([3,4,1,2],reverse=True) [4, 3, 2, 1] |
5. 函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:
1 2 3 4 5 |
def sum(*args): a = 0 for n in args: a = a + n return a |
但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
1 2 3 4 5 6 7 |
def par_sum(*args): def sum(): a = 0 for n in args: a = a + n return a return sum |
当我们调用par_sum()时,返回的并不是求和结果,而是求和函数:
1 2 3 |
>>> f = par_sum(1, 3, 5, 7, 9) >>> f <function lazy_sum.<locals>.sum at 0x101c6ed90> |
调用函数f时,才真正计算求和的结果:
1 2 |
>>> f() 25 |
在这个例子中,我们使用了嵌套函数,在函数par_sum中又定义了函数sum。并且,内部函数sum可以引用外部函数par_sum的参数和局部变量,当par_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用par_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:
1 2 3 4 |
>>> f1 = par_sum(1, 3, 5, 7, 9) >>> f2 = par_sum(1, 3, 5, 7, 9) >>> f1==f2 False |
f1()和f2()的调用结果互不影响。
三、闭包
Python 中的装饰器是通过利用了函数特性的闭包实现的,所以在讲装饰器之前,我们需要先了解函数特性,以及闭包是怎么利用了函数特性的。
首先,我们知道了函数是可以嵌套的,就是可以在一个函数内部再定义一个函数的,如果在一个函数的内部定义了另一个函数,外部的我们叫他外函数,内部的我们叫他内函数。
在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用,这样就构成了一个闭包。
一般情况下,在我们认知当中,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束。
再来看看闭包在代码中的表现形式:
1 2 3 4 5 6 7 8 |
def outer(x): def inner(): # 函数嵌套 return x # 跨域访问,引用了外部变量x return inner # 外函数返回了内函数的引用 closure = outer('外部变量') # 函数作为变量赋给closure print(closure()) # 执行闭包 |
说明:我们分析下这个流程,外部函数 outer 接收到’外部变量’,传给内部函数 inner,作为它 return 的参数,最后 outer 返回 inner 函数,返回的内函数 inner 的引用作为变量传递给 closure,最后执行 closure 这个函数对象,实际上是执行了 inner 这个函数,返回了值 ‘外部变量’,这样就实现了一个简单的闭包。外函数结束的时候发现内部函数将会用到自己的临时变量,那么这个临时变量就不会释放,会绑定给这个内部函数。闭包用起来简单,实现起来可不容易。而正是因为闭包这种程序结构的存在,才有了更为强大的装饰器。
再说明一下,外函数返回内函数的引用后,相当于内函数的内存地址赋值给了新变量。此时,返回的内函数并没有立刻执行,而是直到调用才会执行。我们再来看一个例子:
1 2 3 4 5 6 7 |
def count(): fs = [] for i in range(1, 4): def f(): return i*i fs.append(f) return fs |
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。我们可以执行以下count()看看:
1 2 |
>>> count() [<function f at 0x10f258140>, <function f at 0x10f258758>, <function f at 0x10f258e60>] |
然后我们调用这三个函数:
1 |
>>> f1, f2, f3 = count() |
你可能认为调用f1(),f2()和f3()结果应该是1,4,9;但实际结果是:
1 2 3 4 5 6 |
>>> f1() 9 >>> f2() 9 >>> f3() 9 |
全部都是9!原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9。
返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
1 2 3 4 5 6 7 8 9 |
def count(): def f(j): def g(): return j*j return g fs = [] for i in range(1, 4): fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f(); return fs |
再看看结果:
1 2 3 4 5 6 7 |
>>> f1, f2, f3 = count() >>> f1() 1 >>> f2() 4 >>> f3() 9 |
一个函数可以返回一个计算结果,也可以返回一个函数。
返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量,不然就会产生不可控值。
<参考>