上一篇文章中讲到,函数可以作为参数传入一个函数,同样函数也可以作为其他函数的返回值,这一篇我们就讲讲将函数作为返回值。
首先来看上一篇博客求素数的例子
>>> def _div_fun(n):
def _div_check_fun(x):
return x % n > 0
return _div_check_fun
这是什么?外层函数中嵌套了一个函数,然后外层函数将内层函数作为返回值进行返回,同时,返回函数中的计算也拥有了外层函数的变量n,这种将外部函数的参数和自身的局部变量保存在函数中一起返回的结构就称为“闭包”,它拥有极大的威力!
再看一个例子
>>> def lazy_sum(*lst):
def sum():
ax = 0
for i in lst:
ax = ax + i
return ax
return sum
>>> f1 = lazy_sum(1,3,5,7)
>>> f2 = lazy_sum(1,3,5,7)
>>> f1 == f2
False
>>> f1()
16
>>> f2()
16
>>>
首先,因为不希望立即计算出求和的值,所以在内部再构造出一个sum函数,通过返回sum函数来达到在需要时再调用计算求和的目的;
同时,返回的sum函数中,使用了外层函数的参数lst
,也保存着自身的局部变量ax
;
其次,每一次调用lazy_sum
,都会返回一个新的函数,即使传入了相同的参数;
最后,闭包返回了sum
函数之后并没有立即执行,而是等到调用sum()
之后才执行函数。
定义
讲了这么多,对闭包有了一个大概的印象,知道了它就是一个函数返回的函数对象之后,再来看看它正统的定义
在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
定义中的自由变量指的就是外层函数的参数,即使外层函数的作用域结束之后,参数仍然存在不会被回收,因为它被闭包引用了!
为什么使用闭包?
基于上面的介绍,不知道读者有没有感觉这个东西和类有点相似,相似点在于他们都提供了对数据的封装。不同的是闭包本身就是个方法。和类一样,我们在编程时经常会把通用的东西抽象成类,(当然,还有对现实世界——业务的建模),以复用通用的功能。闭包也是一样,当我们需要函数粒度的抽象时,闭包就是一个很好的选择。
在这点上闭包可以被理解为一个只读的对象,你可以给他传递一个属性,但它只能提供给你一个执行的接口。因此在程序中我们经常需要这样的一个函数对象——闭包,来帮我们完成一个通用的功能,比如常用到的装饰器。
使用闭包
闭包的使用很广泛,譬如前面提到的高阶函数均需要函数作为参数传入,如若需要函数粒度的抽象上对函数进行更改,就可以使用闭包。
闭包中常见错误
>>> def count():
fs = []
for i in range(3):
def f():
return i * i
fs.append(f)
return fs
>>> f1, f2, f3 = count()
上面这段代码,是不是感觉一切ok?但结果却是这样的:
>>> f1()
4
>>> f2()
4
>>> f3()
4
这是什么Bug!
分析一下,返回的f1,f2,f3
调用时,用到了自由变量i
,很明显,在调用的时刻,i
已经循环完成变成了2,而并非储存每一个内部函数时相应的值。
重点就在于闭包不是立即执行的!所以闭包中不要引用循环变量,或者后期会发生变化的变量!
那么如果一定要引用变化的变量,应该怎么改呢?总体思想是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变
>>> def count():
fs = []
for i in range(3):
def f(i):
def g():
return i * i
return g
fs.append(f(i))
return fs
>>> f1, f2, f3 = count()
>>> f1()
0
>>> f2()
1
>>> f3()
4
关键在于立即执行,立即执行f(i)
,从而保存变量的当前值。
闭包与并行计算
闭包有效的减少了函数所需定义的参数数目。这对于并行运算来说有重要的意义。在并行运算的环境下,我们可以让每台电脑负责一个函数,然后将一台电脑的输出和下一台电脑的输入串联起来。最终,我们像流水线一样工作,从串联的电脑集群一端输入数据,从另一端输出数据。这样的情境最适合只有一个参数输入的函数。闭包就可以实现这一目的。
并行运算正成为一个热点。这也是函数式编程又热起来的一个重要原因。函数式编程早在1950年代就已经存在,但应用并不广泛。然而,我们上面描述的流水线式的工作并行集群过程,正适合函数式编程。由于函数式编程这一天然优势,越来越多的语言也开始加入对函数式编程范式的支持。
总的来说,闭包的理解没有那么困难,灵活使用闭包,能够发挥其强大的作用。