闭包

前言

闭包包并不是python中特有的概念,在函数式编程语言中都有,例如 JavascriptRuby 这样的语言中都存在闭包。

闭包对初学者来说绝对是一个晦涩难懂的概念,即使你去看权威的维基百科,他的描述也像论文一样难懂,我们来看看他是怎么说的。

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。

看完后是不是一愣一愣的,说的啥,每个字都认识,就是不理解。

其实啊,闭包本质上其实还是个函数,只不过这个函数比较特殊,特殊在哪呢?

普通函数

先来看个普通函数

demo.py
1
2
3
4
def simple_adder(x, y):
return x + y

result = simple_adder(2, 3) # 5

xy 作为形参传入函数 simple_adder ,这两个参数可以理解为函数的局部变量,函数被调用时,会在栈上创建其执行环境,函数执行完成后,局部变量随着栈帧的销毁被垃圾回收,局部变量无法在外部引用。

闭包函数

再来看这个复杂一点的函数(假设你对函数是一等对象这个概念已经有所了解)

1
2
3
4
5
def adder(x):
def func(y):
return x + y

return func

现在定义一个函数 adder, 然后还有一个嵌套函数 func。外层函数的返回值是嵌套函数func对象。(不知道什么是一等对象的先看文末指引)

执行:

1
2
3
add2 = adder(2)
add2(3) # 5
add2(5) # 7

对于普通函数来说,调用完adder(2)这个函数时,局部变量x的值2本应该销毁了,但是当我们执行第二行add2(3)的时候,这个2居然还活着,其实这就是闭包最神奇的地方,因为它能访问自由变量x,即使x所在的环境(adder函数)已经不再了。

这里的嵌套函数func就是一个“闭包”函数。

闭包使得函数可以绑定一个状态值(属性),比如这里 add2 这个函数就绑定属性值2,他会一直跟随着这个闭包函数。

闭包只在函数式编程语言中才有,对于纯面向对象语言,比如Java就没法实现上面的效果,如果要实现必须用来实现。因为面向对象中,类天然就可以附带属性(状态)

如果把前面的闭包用类来实现类似的效果,可以通过如下写法实现, 其中x就是类Adder的一个状态属性,2这个状态值会一直跟着add2这个实例对象

1
2
3
4
5
6
7
8
9
10
11
class Adder:
def __init__(self, x):
self.x = x

def __call__(self, y, *args, **kwargs):
return self.x + y


add2 = Adder(2)
print(add2(3)) # 5
print(add2(5)) # 7

什么时候使用闭包

1、可以用来替换类, 假设一个类中就只有一个方法,那么这时候用闭包来实现更优雅,比如前面例子

2、替换全局变量,上面例子中,如果不要闭包来实现,另一个做法就是定义一个全部变量,这样任何时候都能访问到,但是全局变量的弊端也在于此,不安全,因为他暴露在外面,有可能在其他地方被修改。而闭包相当于把变量隐藏起来,外部是没法去访问的,只有闭包自己可以访问。

其实你们经常用的装饰器就是闭包的一种应用

__closure__属性

在python中,每个函数都有个属性叫__closure__,通过它我们可以检查函数是不是一个闭包函数

1
print(simple_adder.__closure__)  # None

__closure__返回值为None,那肯定不是闭包

1
print(add5.__closure__[0].cell_contents)  # 2     

add5 是一个闭包,__closure__返回的是一个只读的tuple对象,cell_contents 是该函数绑定的自由变量。

参考文章