英文称之为Closure,中文译为闭包,闭包的概念也并不是Python独有的,Javascript等高级语言中也存在闭包的概念。那什么是闭包?有一个经典的公式可以回答这个问题:
闭包 = 内部函数 + 引用环境
这个公式非常清楚地说明,闭包本质上是具备"封闭"上下文环境的函数。所谓"封闭",是说该函数的上下文环境不会随着函数调用结束而销毁,实际上会持久驻留内存。众所周知,函数通常定义在全局上下文,而闭包函数定义在函数内部,全局上下文也是持久驻留的,把函数定义在全局上下文中也可以有相同效果,那闭包有什么不一样的地方?答案是可以避免全局变量的污染。
接下来,我们从简单闭包开始,逐步探讨下闭包的使用。
1、简单闭包
首先最简单的闭包是外部函数只传递一个函数引用和内部函数不带参数的闭包。我们在Pycharm中演示下简单闭包:
在上图的代码中,有以下几个关键点:
- 定义一个outer_func,outer_func的参数为一个函数引用func;
- 在outer_func中定义inner_func,并返回inner_func的引用;
- inner_func调用outer_func中传递的func引用的函数;
- 全局定义一个没有参数的print_info函数;
- 通过outer_func(print_info)得到f,此时的f指向outer_func中的inner_func已用
- 调用函数f
运行结果为:
通过简单闭包的演示可以看出,闭包中的引用环境主要是外部函数的变量,包括外部函数参数和外部函数中定义的变量,这个闭包在整个运行环境中,外部函数的参数和变量是一直被"锁"在内存中的,这也是调用f时还能访问func的原因。
当然,实际应用中func不可能没有参数,接下来让我们分析下带参数的闭包。
2、带参闭包
所谓带参闭包,是指闭包函数可以带参数,也就是上节中的inner_func可以带参数。我们在Pycharm演示下:
运行结果:
通过增加info,可以让闭包传递参数,稍有经验的开发者一眼就能看出这种带不可变参数的闭包不够通用,因为不可能参数增加一个就再定义一个闭包。在Python中,一旦涉及可变参数,需要使用args和**kwargs,好了,我们用args和**kwargs改造下上面的闭包:
运行结果:
3、闭包隔离
前面两节中,我们定义完闭包后,在使用过程中只创建了一个闭包函数,现在我们在Pycharm中做如下实验:
运行结果:
要提一下的是,在outer_func中定义了count,如果在inner_func只是读取count的话直接使用count即可,如果在inner_func中要写count,则需要在inner_func中声明nonlocal,与普通函数中修改全局变量时使用global类似,只不过nonlocal修饰的变量是外部函数中的变量。在本实验中,使用outer_func(print_info)定义两个"相同"的闭包f1和f2,f1调用3次,f2调用1次,从运行结果来看f1中的count虽然增加了,但是并不影响f2中的count,这说明了闭包一个很重要的特点:内存隔离。也就是说两个闭包函数的引用环境是独立的,彼此不影响,就像类的实例对象一样。
4、结尾
经过前面三节的分析之后,我们再回顾下闭包概念的经典公式:
闭包 = 内部函数 + 引用环境
可以简单总结下:
- 闭包是定义在一个函数内部的函数;
- 外部函数传递一个函数引用给内部函数,并返回内部函数引用;
- 内部函数可以访问外部函数的变量,并调用外部函数传递的函数引用;
- 闭包之间内存隔离。