在计算机科学中,闭包 又称 词法闭包 或 函数闭包,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。闭包被广泛应用于函数式语言中。
从上面这段话中可以看出闭包的两个重要条件是引用自由变量和函数,与闭包这个名称结合起来看,这个函数就像是一个包,而这个函数所引用的变量就好比函数这个包中封闭起来的东西,包中的东西被紧紧封闭在包中,函数所引用的变量也被与这个函数所绑定。
首先来看两个概念 Nonlocal variable 和 Nested function
Nonlocal variable & Nested function
Nonlocal variable
是相对于某个函数来说的,指的是这个函数所调用的在本函数作用域之外的变量,Nested function
指的被定义在一个函数(outer enclosing function)中的函数,这个nested function
可以调用包围它的作用域中的变量。
看一个例子
代码语言:javascript复制def print_msg(msg):
# outer enclosing function
def printer():
# nested function
print(msg)
printer()
>>> print_msg("Hello")
Hello
在这个例子中函数printer
就是一个nested function
,而变量msg
就是一个nonlocal variable
。这里需要注意的是,printer
虽然可以访问msg
,但是不可以改变它,如果尝试更改会出现UnboundLocalError: local variable 'msg' referenced before assignment
。
def print_msg(msg):
def printer():
msg = 'a'
print(msg)
printer()
>>> print_msg("Hello")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in print_msg
File "<stdin>", line 3, in printer
UnboundLocalError: local variable 'msg' referenced before assignment
local variable 'msg' referenced before assignment
如果必须要更改这个变量的值,在Python3中新引入的nonlocal
语句可以解决。
def print_msg(msg):
def printer():
nonlocal msg
msg = 'a'
print(msg)
printer()
>>> print_msg("Hello")
Helloa
在Python2中使用global
也可解决,但是global
会直接查找全局变量,而nonlocal
则是按优先级从本地-->全局
进行搜索。
闭包函数
下面使外层函数(outer enclosing function)返回一个函数
代码语言:javascript复制def print_msg(msg):
def printer():
print(msg)
return printer
>>> another = print_msg("Hello")
>>> another()
Hello
将print_msg("Hello")
返回的函数赋值给another
,再调用another
函数时,发现已经离开了print_msg
函数的作用域,但是"Hello"
已经被绑定给another
,所以仍然能够正常调用,这就是Python中的闭包。
删除print_msg
之后,another
仍然能够正常调用。
>>> del print_msg
>>> print_msg("Hello")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'print_msg' is not defined
name 'print_msg' is not defined
>>> another()
Hello
闭包的应用
当符合下面几个条件时就形成了闭包:
- 有一个
Nested function
- 这个
Nested function
访问了父函数作用域中的变量 - 父函数返回了这个
Nested function
闭包主要运用在需要讲父函数作用域中的变量绑定到子函数的场景之中,在释放掉父函数之后子函数也不会受到影响。运用闭包可以避免对全局变量的使用。对于一个只有需要实现少数方法的类我们也可以用闭包来替代,这样做可以减少资源的使用。
下面需要用类定义不同动物的叫声
代码语言:javascript复制class Animal:
def __init__(self, animal):
self.animal = animal
def sing(self, voice):
return "{} sings {}".format(self.animal, voice)
>>> dog = Animal("dog")
>>> cow = Animal("cow")
>>> dog.sing("wong")
'dog sings wong'
>>> cow.sing("mow")
cow sings mow'
用闭包替代
代码语言:javascript复制def make_sing(animal):
def make_voice(voice):
return "{} sings {}".format(animal, voice)
return make_voice
>>> dog = make_sing("dog")
>>> dog("wong")
'dog sings wong'
>>> cow = make_sing("cow")
>>> cow("mow")
'cow sings mow'
闭包与装饰器
闭包通常用来实现一个通用的功能,Python中的装饰器就是对闭包的一种应用,只不过装饰器中父函数的参数是一个函数,下面这个例子通过装饰器实现了在子函数执行前后输出提示信息。
代码语言:javascript复制def make_wrap(func):
def wrapper(*args):
print("before function")
func(*args)
print("after function")
return wrapper
@make_wrap
def print_msg(msg):
print(msg)
>>> print_msg("Hello")
before function
Hello
after function
装饰器也可以进行叠加
代码语言:javascript复制def make_another(func):
def wrapper(*args):
print("another begin")
func(*args)
print("another end")
return wrapper
@make_another
@make_wrap
def print_msg(msg):
print(msg)
>>> print_msg("Hello")
another begin
before function
Hello
after function
another end
闭包的内部实现
Code Object
为了了解闭包的内部实现,需要用compile
命令得出相应的code object
>>> code_obj = compile("print_msg('Hello')", "", "single")
这里第一个参数是一个可以被exec
或 eval
解析的模块、语句或者表达式;
第二个参数是用来存放运行时错误的文件;
第三个选择single
模式,与前面第一个参数填写的表达式相匹配,如果第一个参数是表达式则需要用eval
模式,如果是模块则应该用exec
模式。
下面通过dis
讲code_obj
反编译成助记符
>>> dis.dis(code_obj)
1 0 LOAD_NAME 0 (print_msg)
2 LOAD_CONST 0 ('Hello')
4 CALL_FUNCTION 1
6 PRINT_EXPR
8 LOAD_CONST 1 (None)
10 RETURN_VALUE
Python3中通过__code__
访问函数的code object
(Python2中为func_code
)
>>> print_msg.__code__
<code object print_msg at 0x10d5c7300, file "<stdin>", line 1>
Cell Object
cell object
用来存储被多个作用域所引用的变量。
比如下面函数中msg
被print_msg
所引用,也被printer
所引用,所以msg
会被存在一个cell object
中
def print_msg(msg):
def printer():
print(msg)
return printer
查看其__closure__
属性可以验证我们的想法
>>> print_msg("Hello").__closure__
(<cell at 0x10d121d38: str object at 0x10d4a6f48>,)
尽管这两个引用都被存在同意个cell object
,但是他们仍然只在各自的作用域下作用。
闭包分析
首先反编译print_msg
>>> dis.dis(print_msg)
2 0 LOAD_CLOSURE 0 (msg)
2 BUILD_TUPLE 1
4 LOAD_CONST 1 (<code object printer at 0x10d5c7780, file "<stdin>", line 2>)
6 LOAD_CONST 2 ('print_msg.<locals>.printer')
8 MAKE_FUNCTION 8
10 STORE_FAST 1 (printer)
4 12 LOAD_FAST 1 (printer)
14 RETURN_VALUE
LOAD_CLOSURE 0 (msg)
将变量msg
进栈。BUILD_TUPLE 1
将栈顶的元素取出,创建元组,并将该元组push进栈。LOAD_CONST 1
从print_msg.__code__.co_consts[1]
中取出,为printer
的code object
的地址,将其push进栈。LOAD_CONST 2
从print_msg.__code__.co_consts[2]
中取出,将其push进栈。STORE_FAST 1
从栈顶取出之前创建的函数对象的地址信息赋给局部变量printer(局部变量名记录在__code__.co_varnames中) __code__.co_varnames的内容为('msg','printer')
将变量msg(记录在__code__.co_cellvars[0])绑定栈顶的函数对象地址。
LOAD_FAST 1
将msg
的值压入栈。RETURN_VALUE
返回栈顶。
可以看到在STORE_FAST 1
中将变量msg
绑定到了printer
函数,从而达到了闭包中内部函数访问外部函数变量的效果。