计算机语言中的闭包

予早 2025-02-21 01:08:21
Categories: Tags:

计算机中的闭包是指:一个实体该实体捆绑的非该实体内的环境状态组合称之为闭包。

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>,)

函数的__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

特殊例子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

简而言之,闭包这个概念用来说明一件事情:允许子命名空间直接引用父命名空间的名称