计算机中的闭包是指:一个实体与该实体捆绑的非该实体内的环境状态的组合称之为闭包。
Python中在函数这个实体上实现了闭包,称之为函数闭包(函数+外部引用共同构成闭包),也可以在其他实体上实现闭包,不同语言有不同设计。
来看一下Python的闭包函数,这是Python中非常经典的闭包。
def make_counter():
count = 0
def counter():
nonlocal count # 注明非本地变量从而允许在本地修改
count += 1
return count
return counter
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
print(counter.__closure__) # (<cell at 0x00000249B0651570: int object at 0x00007FFD893209F8>,)
- 一个实体 counter函数
- 实体 counter函数引用了counter函数外部的环境状态,即变量count
- counter函数与count变量两者组合称之为闭包,由于实体为函数,又称之为函数闭包,表示以函数为实体的闭包,其中函数counter称之为闭包函数,表示该函数是闭包中的构成实体,count称之为外部引用
函数的__closure__属性用于存储闭包函数的外部引用,但注意,这里的外部引用不包含内置命名空间和全局命名空间中的引用
https://docs.python.org/3/reference/datamodel.html#function.__closure__
https://docs.python.org/3/reference/datamodel.html#codeobject.co_freevars
为什么__closure__不记录内置命名空间和全局命名空间的引用呢?
很简单,源头在于外部调用了counter函数,根据变量搜索规则,本地找不到就到父命名空间寻找,而对于闭包函数来说,找不到引用count就应当找父命名空间,即make_counter构成的命名空间,但问题在于make_counter构成的空间随着counter = make_counter()执行结束就释放了,按道理count会随之销毁,所以这里引入__closure__反向持有一个count的引用,这样外部命名空间持有count的一个引用,闭包函数通过__closure__持有count的一个引用,如果闭包函数被外部return出去,如上例,闭包函数被return出去,这时闭包函数引用被全局命名空间持有,闭包函数不会被销毁,而间接通过__closure__持有的count也不会被销毁,这样就可以正常调用闭包函数,若闭包函数不被return到外部,则count随之销毁即可,而对于内置命名空间和全局命名空间来说,只要解释器在运行那么全局命名空间和内置命名空间一定是加载的,两者中的引用不用考虑作为外部引用引用不到的情况。
既然如此,那么有一个很有意思的事情,就是手动把闭包函数counter的__closure__置为空,那么在全局命名空间调用闭包函数不就没法引用到外部引用了,然后程序不就寄了吗?
def make_counter():
count = 0
def counter():
nonlocal count # 注明非本地变量从而允许在本地修改
count += 1
return count
print(type(counter.__closure__)) # <class 'tuple'>
counter.__closure__ = tuple() # AttributeError: readonly attribute
return counter
counter = make_counter()
结果是不行的,会提示AttributeError: readonly attribute,这是在CPython层面做了限制,因为置空或者置其他值会导致程序出错或者引用关系变得复杂,总之是一种带来负面影响的操作,所以直接被限制了
来看一些闭包的特殊例子。
特殊例子一
count = 0
def counter():
global count # 注明全局变量从而允许在本地修改
count += 1
return count
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
print(counter.__closure__) # None
- 闭包函数counter与外部引用count构成闭包
- 由于count位于全局命名空间,故此时
__closure__为None
特殊例子2
create_multipliers = []
for i in range(5):
f = lambda x: x * i
print(f.__closure__) # 永远为None
create_multipliers.append(f)
print(f"可以访问全局变量i但Pycharm会提示i未定义: {i}")
for multiplier in create_multipliers:
print(multiplier(2))
特殊例子3
create_multipliers = []
i = 0
while i < 5:
f = lambda x: x * i
print(f.__closure__) # 永远为None
create_multipliers.append(f)
i+=1
print(f"可以访问全局变量i且Pycharm解析没问题: {i}")
for multiplier in create_multipliers:
print(multiplier(2))
特殊例子4
def create_multipliers():
return [lambda x: x * i for i in range(5)]
for multiplier in create_multipliers():
print(multiplier(2))
# 期望输出0, 2, 4, 6, 8
# 结果是 8, 8, 8, 8, 8
正确的使用方式是将i的值利用参数的方式进行传递:
def create_multipliers():
return [lambda x,i=i: x * i for i in range(5)]
s = create_multipliers()
for multiplier in s:
print(multiplier(2)) # 0, 2, 4, 6, 8
简而言之,闭包这个概念用来说明一件事情:允许子命名空间直接引用父命名空间的名称