55、Python之函数高级:一篇文章彻底搞懂什么是“闭包”

引言

在Python编程中,有一个让一些新手望而却步的概念——“闭包(Closure)”。

一方面闭包的定义及作用机制确实稍微有点复杂,另一方面,这个概念似乎更偏向数学、理论场面,新手学来,似乎无太多应用场景。

但是,基于前面介绍的,关于函数对象、函数是一等公民、高阶函数,以及函数对象的内部细节的探究等。我相信理解并掌握“闭包”的概念及使用,会变得更加容易,是“水到渠成、自然而然”的事情。

本文的主要内容有:

1、探究“闭包”概念背后的本质

2、一个闭包实例

3、闭包的作用机制

探究“闭包”概念背后的本质

前面的文章中,我们曾经提及编程学习中的两大核心,即“数据的表达”和“数据的处理”,也可以表述为“数据结构”和“算法”。其实,这是一个极简的表达。

更加完整来看的话,每一个程序更加完整的构成,都需要有以下几个部分:

1、输入数据的集合

2、内部状态的集合

3、计算规则的集合

4、输出数据的集合

这其实也是计算机科学背后的图灵机的抽象。从这个抽象的角度,如果看通常的编程语言中的函数,我们可以发现,似乎是对图灵机的删减版本,删减的部分是“内部状态的集合”。也就是说,纯粹的函数是更加聚焦于计算规则,而没有内部状态的图灵机。

但是,有些时候,我们确实是需要内部状态的,根据内部状态的不同,对于相同的输入,函数能给出不一样的输出。

于是便有了“闭包”的概念,闭包可以粗略理解为“有内部状态的函数”,它是一个函数与其相关的上下文环境状态的组合。

这样看来,闭包其实就是一个简单的对象,这个对象只有一个方法。

反过来看,对象可以看成是几个共享内部状态的闭包,这种说法也是可以成立的。

所以,闭包和对象,是一而二,二而一的关系,本质上是一个东西。

由此,我们可以进一步得出,函数式编程与面向对象编程,在底层基础上也是一致的结论。

如果在其他函数式编程语言中,理解函数、闭包、对象似乎还有些复杂。

但是,如果我们聚焦到Python中来看,由于“一切皆对象”这个底层的“一”的存在,Python中的函数是对象,其本身也是具有状态(即属性)的。那么,在Python中我们似乎又不必严格区分什么是函数、什么是闭包了,因为Python中的函数都是对象,都有状态,有状态的函数就是闭包,那么Python中一切函数似乎也都是闭包了。

当然,以上只是基于底层概念所做的一些推论而已。在实际的编程语法上,普通函数和闭包还是有些许的差别的。

结合上一篇文章中,关于函数对象的内部构成的探究,我们可以得出如下的结论:

1、Python中的函数都是对象,所以,从广义上来说,Python中的函数都可以是有状态的函数。

2、Python中的函数状态,就是其定义执行的上下文环境。

3、Python中的普通函数,其内部状态也就是其定义所处的全局命名空间,这个状态在函数对象中,可以通过__globals__属性来进行读写。

4、Python中的特殊函数对象,特指“闭包”,其内部状态除了__globals__属性所对应的全局命名空间外,还有一个__closure__属性所存储的“闭包”所特有的内部状态。

暂且停止概念的论述,只要有个理念性的感知即可。后面,我们通过实际代码进一步演示这些概念。

一个闭包实例

我们以一个带初始值的累加器的实现为例,来分别看一下普通函数和闭包的使用,从而分析、比较闭包的特点及特定场景中的优势所在。

在真正使用闭包之前,我们先看一下普通的有状态的函数的使用:

首先定义一个模块m1.py:

total = 20


def acc(step):
    global total
    total += step

然后,我们在入口文件中导入m1模块,进行函数调用:

import m1

print(f"初始值为:{m1.total}")
print(f"函数内部状态:{m1.acc.__globals__['total']}")
m1.acc(5)
print(f"加5之后变为:{m1.total}")
print(f"函数内部状态:{m1.acc.__globals__['total']}")
m1.acc(10)
print(f"又加了10之后变为:{m1.total}")
print(f"函数内部状态:{m1.acc.__globals__['total']}")

执行结果:

d89e8f22352f5968f864370c01d76f48.jpeg

从执行结果来看,Python中定义的普通函数确实是有状态的,也实现了累加器的效果。

但是,我们也应该意识到,普通函数的状态,本质上不是独属于函数自身的,而是与函数所属模块中的所有函数、代码块所共享的模块自身的全局命名空间。

所以,这个状态显然是不够安全的,毕竟全局变量中存储的状态,被修改的途径比较多。

除了状态共享所导致的不安全之外,我们没法实现多个不同状态的累加器的并存。

下面,我们通过闭包实现这个累加器的需求:

def acc(total=20):
    def inner(step):
        nonlocal total
        total += step
        return total

    return inner


acc1 = acc(0)
print(acc1.__closure__)
print(f"初始状态:{acc1.__closure__[0].cell_contents}")
print(f"加10之后变为:{acc1(10)}")
print(f"加10之后变为:{acc1(100)}")

acc2 = acc(100)
print(acc2.__closure__)
print(f"初始状态:{acc2.__closure__[0].cell_contents}")
print(f"加10之后变为:{acc2(10)}")
print(f"加10之后变为:{acc2(100)}")
print(acc1)
print(acc2)

执行结果:

267bc78d18645cb083fc2aef10207770.jpeg

从执行结果来看,这种“闭包”的形式确实实现了不同累加器对象的并存,独属于闭包的内部状态存储在函数对象的__closure__属性中。

闭包的作用机制

我们现粗略给出一个“闭包”的定义:

所谓“闭包”是指在嵌套定义的函数中,一个内部函数引用了外部函数中的局部变量,并且该外部函数的作用域已经结束。这种内部函数和它应用的外部函数的变量一起构成了闭包。

闭包可以保存和访问它创建时的环境,即使在外部函数结束之后。

前面提到过,高阶函数有两个条件,满足一个即为高阶函数,即要么以函数作为参数进行传递,要么返回一个函数对象。闭包显然是满足第二个条件的高阶函数。

下面我们来简单概括一下创建一个闭包所需要的条件:

1、存在嵌套定义的函数。

2、内部函数引用了其外部函数中的变量。

3、外部函数返回了这个内部函数对象(注意,是返回函数对象,而非返回内部函数调用之后的结果)。

同时满足上面的3个条件,就可以创建闭包了。

本质上来说,每个函数都有自己的局部命名空间,也有其所属的全局命名空间。一旦,闭包的创建条件满足,一定会形成一个新的命名空间,也就是闭包命名空间。

只要是嵌套函数定义中内部函数引用到了外部函数中的局部变量,都会添加到闭包命名空间中。

再次看一下定义:

0ce26636deac7ea10b3c3da3978159a4.jpeg

可以看到函数对象中指向闭包命名空间的属性__closure__是一个元组,元素类型为_Cell,_Cell对象有cell_contents属性。

所以,上面代码中访问闭包命名空间中存储的内部状态是通过.__closure__[0].cell_contents来实现的。

此外,如果要在内部函数作用域中,试图修改外部函数中定义的局部变量(外部函数的形参也是外部函数的局部变量),则需要通过nonloacl关键字进行变量的声明,否则会被解释器理解为定义一个内部函数中的局部变量并赋上初始值。

这种关键字声明,我们在前面的函数局部作用域试图修改全局变量时也遇到过,即global关键字。

从global和nonlocal两个关键字也能看出来闭包作用域尴尬的地方,没有用global关键字,所以非全局命名空间,nonlocal,也就是不是local,不是局部命名空间。介于全局命名空间和局部命名空间夹缝之间的部分,姑且叫它闭包命名空间吧。

从名字解析所遵循的LEGB规则,也能看出来,L(local)和G(global)之间的E(enclosure),之前没有展开提,其实就是这个闭包命名空间。Python中的名字解析符合就近原则,内部的可以覆盖外部的。

其实,在我看来,没必要特别强调“闭包”的概念,只要能够理解Python函数自身作为对象,已经天然具备内部状态的特性。理解了这些底层的作用机制,已经足够了。

总结

本文详细解释了闭包的定义、本质,以及函数、闭包、对象这几个概念的区别与联系,同时介绍了闭包成立的条件以及相关内部状态的存储机制。在理解了闭包的基础之上,再次回顾了LEGB规则。

感谢您的拨冗阅读,希望对您理解Python中的闭包能有所帮助。

ac8568c541eab0dedec808aa8aa775d4.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南宫理的日知录

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值