之前学 js 的时候第一次见到闭包,当时不甚了了,还为了应付面试强行记住了一个模棱两可的“定义”:在函数中嵌套定义函数,而且在外层将内层函数返回,一同返回了外层函数的环境。当时从字面意思以及当时一个经典例子试图去理解闭包,加之"闭包"这个翻译也很不容易让人味出其中的道理,致使对其总感受懵懵懂懂。最近工做须要,用起 python,又遇到闭包,此次看到了一些新奇有趣的资料,这才算大体把一些字面上的概念(first-class functions,bind,scope等等)贯通在一块儿,反过来对闭包有了更深的理解。python
引用资料列在最后,十分推荐你们去读读。web
计算机中有些英文专业词汇,字面直译,不免因缺乏上下文而显得苍白拗口,须得多方铺垫,方能味得古怪下面的原理。闭包(closure)即是一个这样牵扯了许多上下文的概念,包括编程语言最基本的绑定(binding),环境(environments),变量做用域(scope)以及函数是第一等公民(function as the first-class)等等。面试
在Python中,binding 是编程语言最基本的抽象手法,它将一个值绑定到一个变量上,而且稍后能够引用或者修改该变量。下面是几种不一样层次的绑定,每组语句在运行时将一个名字与对应值绑定到其定义所在的环境中。编程
包括将名字绑定到一块内存,经过赋值语句实现,固然函数调用时,形参和实参结合也是绑定:bash
In [1]: square = 4
复制代码
将名字绑定到一组复合运算,即函数定义,利用 def 关键字实现:数据结构
In [1]: def square(x):
return x*x
复制代码
将名字绑定到一个数据集合,即类定义,使用 class 实现:闭包
In [1]: class square:
def __init__(self, x):
self.x = x
def value(self):
return self.x * self.x
复制代码
依照执行顺序,屡次同名绑定,后面会覆盖前面:app
In [1]: square = 3
In [2]: square
Out[2]: 3
In [3]: def square(x):
...: return x * x
...:
...:
In [4]: square
Out[4]: <function __main__.square(x)>
In [5]: class square:
...: def __init__(self, x):
...: self.x = x
...:
In [6]: square
Out[6]: __main__.square
复制代码
说这些都是抽象,是由于它们提供了对数据,复合操做或数据集合的封装手段,即将一个名称与复杂的数据或逻辑进行捆绑,使调用者不用关心其实现细节,而且以此来做为构建更复杂的工程的基本元素。能够说绑定是编程的基石。编程语言
回到本文的主题上来,闭包首先是函数,只不过是一种特殊的函数,至于这个特殊性在哪,等稍后引入更多概念后再进行阐述。函数
scope(做用域),顾名思义,也就是某个binding 能罩多大的范围,或者说你能够在多大范围内访问一个变量。每一个函数定义会构造一个局部定义域。
python,和大多数编程语言同样,使用的是静态做用域(static scoping,有时也称 lexical scoping)规则。在函数嵌套定义的时候,内层函数内能够访问外层函数的变量值。所以你能够把做用域想象成一个容器,即它是能够嵌套的,而且内层做用域会扩展外层做用域,而最外层做用域即全局做用域。
上一小节提到了,屡次同名绑定,后面会覆盖前面,其实有隐含前提:在同一做用域内。若是是嵌套做用域,实际上是隐藏的关系,内层函数的变量定义会遮蔽外层函数同一名字定义,可是在外层做用域中,该变量还是原值:
In [16]: a = 4
In [17]: def outer():
...: a = 5
...: print(a)
...: def inner():
...: a = 6
...: print(a)
...: inner()
...: print(a)
...:
In [18]: outer()
5
6
5
In [19]: print(a)
4
复制代码
能够看出,做用域其实也能够从另外一个角度理解,即咱们在某个环境(environment)中,在肯定一个name binding 值的时候,会从最内层做用域顺着往外找,找到的第一个该名字 binding 的对应的值即为该 name 引用到的值。
须要强调的时候,函数的嵌套定义会引发定义域的嵌套,或者说环境扩展(内层扩展外层)关系。类的定义又稍有不一样,class 定义会引入新的 namespace,namespace 和 scope 是常拿来对比的概念,但这里按下不表,感兴趣的能够本身去查查资料。
说到这里,要提一下,一个常被提及的例子:
In [50]: a = 4
In [51]: def test():
...: print(a) # 这里应该输出什么?
...: a = 5
...:
In [52]: test()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-52-fbd55f77ab7c> in <module>()
----> 1 test()
<ipython-input-51-200f78e91a1b> in test()
1 def test():
----> 2 print(a)
3 a = 5
4
UnboundLocalError: local variable 'a' referenced before assignment
复制代码
想象中,上面 print
处应该输出 4 或者 5 才对,为何会报错呢?这是由于 test
函数在定义时候,分词器会扫一遍 test 函数定义中的全部 token,看到赋值语句 a=5
的存在,就会明确 a
是一个局部变量,所以不会输出4。 而在执行到 print(a)
的时候,在局部环境中,a
还未被binding,所以会报 UnboundLocalError
。
稍微探究一下,虽然 python 是解释执行的,即输入一句,解释一句,执行一句。可是对于代码块(即头部语句,冒号与其关联的缩进块所构成的复合语句(compound sentences),常见的有函数定义,类定义,循环语句等等)来讲,仍是会总体先扫一遍的。
通常来讲,组成编程语言的元素,如变量,函数和类,会被设定不一样的限制,而具备最少限制的元素,被咱们成为该编程语言中的一等公民。而一等公民最多见的特权有:
套用到python中的函数来讲来讲,即一个函数能够被赋值给某个变量,能够被函数接收和返回,能够定义在其余函数中(即嵌套定义):
In [32]: def test():
...: print('hello world')
...:
In [33]: t = test # 赋值给变量
In [34]: t()
hello world
In [35]: def wrapper(func):
...: print('wrapper')
...: func()
...:
In [36]: wrapper(t) # 做为参数传递
wrapper
hello world
In [37]: def add_num(a):
...: def add(b): # 嵌套定义
...: return a + b
...: return add # 做为函数的返回值
...:
...:
In [38]: add5 = add_num(5)
In [39]: add5(4)
Out[39]: 9
复制代码
并非在全部语言中,函数都是一等公民,好比 Java,上面四项权利 Java 中的函数全都没有。
在这里,可以操做其余函数的函数(即以其余函数做为参数或者返回值的函数),叫作高阶函数。高阶函数使得语言的表达能力大大加强,但同时,也不容易用好。
每一个函数调用,会在环境中产生一个栈帧(frame),而且在栈帧中会进行一些绑定,而后压入函数调用栈中。在函数调用结束时,栈帧会被弹出,其中所进行的绑定也被解除,即垃圾回收,局部做用域也随之消亡。
In [47]: def test():
...: x = 4
...: print(x)
...:
In [48]: test()
4
In [49]: x
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-49-6fcf9dfbd479> in <module>()
----> 1 x
NameError: name 'x' is not defined
复制代码
即在调用结束后,局部定义的变量 x
在外边是访问不到的。可是如以前例子 中,返回的 add
函数却引用了已经调用结束的 add_num
中的变量 a
,怎么解释这种现象呢?能够记住一条,也是以前提到过的:
函数嵌套定义时,内部定义的函数所在的环境会自动扩展其定义所在环境
复制代码
所以在外部函数返回后,返回的内部函数依然维持了其定义时的扩展环境,也能够理解为因为内部函数引用的存在,外部函数的环境中全部的 bindings 并无被回收。
千呼万唤始出来,觉得高潮其实已结束。
闭包就是创建在前面的这些概念上的,上面提到的某个例子:
In [37]: def add_num(a):
...: def add(b): # 嵌套定义
...: return a + b
...: return add # 做为函数的返回值
...:
...:
In [38]: add5 = add_num(5)
In [39]: add5(4)
Out[39]: 9
复制代码
其实就是闭包。捡起以前伏笔,给出我对闭包的一个理解:它是一种高阶函数,而且外层函数(例子中的*add_num
)将其内部定义的函数(add
)做为返回值返回,同时因为返回的内层函数扩展了外层函数的环境(environment),也就是对其产生了一个引用,那么在调用返回的内部函数(add5
*)的时候,可以引用到其(add
)定义时的外部环境(在例子中,即 a
的值)。
说了这么多,其实只是在逻辑层面或者说抽象层面去解释闭包是什么,跟哪些概念纠缠在一块儿。但这些其实都不本质,或者说依然是空中楼阁,若是想要真正理解闭包,能够去详细了解下 python 的解释执行机制,固然,那就是编译原理范畴的东西了。