在大多数其他语言中,函数只是一段可执行的代码,并不是对象。一旦被执行,就被固化了,常驻于内存。因此,函数不能被实例化的。
但是在python中,一切皆对象(类)。
函数可以赋值给一个变量,可以作为参数传递给另外一个函数,也可以把一个函数当作另外一个函数的返回结果。
1 | def a(): |
我们注意到,在上例中,函数a()
是一个类(class)。
什么是闭包
闭包的概念和变量作用域相关,定义为:由函数和定义时的外部环境变量叫做闭包,调用时不再受其他的变量影响。
闭包 = 函数+环境变量
下面的写法称为“闭包”,即
1 | def curve_pre(): |
看起来上边的例子顺理成章。如果我们改成下边的写法,将a
的赋值a=10
写在函数外边,看似f=curve_pre()
返回的是f
就是curve()
函数,那么此时我们得到的结果应该是什么呢?
1 | def curve_pre(): |
在第一种写法中,看似也应该得到结果40。但在第一个结果中我们得到了100!
这就是闭包的现象。
尽管curve()
没有执行,只是作为返回结果。但当在模块中执行时,参数不会取模块中的变量,而是依然取定义时环境变量a=25
。因此,函数curve(x)
与定义函数时的环境变量a=25
形成了一个闭包。
由此,定义一下闭包:
由函数和定义时的外部环境变量共同构成的叫做闭包。一旦闭包形成好,这个函数在任何时候被调用都不受重新赋值影响。
特别需要注意的是,环境变量一定在函数定义的外部,且不能为全局变量。举个反例,在下面的例子中,a=25
为全局变量,所以 curve(x)
与a=25
并没有形成一个闭包,最终结果为40。
1 | a = 25 |
那么,闭包的环境变量存在哪里了呢?如何获取呢?
答:内置变量 f.__closure__
,定义时的环境变量在f.__closure__[0].cell_contents
1 | def curve_pre(): |
闭包的意义
闭包的意义在于保存了函数定义时的环境(现场)。若没有闭包的存在,则很容易受外部环境的影响,难以保证函数运行的正确进行。
闭包的经典误区
在不同的环境下将是不同的闭包。使用闭包时需要注意的几点:
1 | def f1(): |
上述程序将会按照①②③的顺序进行输出,
① 环境变量为10
② 执行f2(),相当于执行闭包,a=20
③ 重点。执行f2()是不会影响a=20被python认为是局部变量,局部变量不会影响外部变量的
上边是不是一个闭包呢?答案是:不是。我们可以通过输出 f.__closure__
验证一下。
1 | f = f1() # f变量用于接收结果 |
特别注意,以下几种写法均不是闭包,需要与闭包严格区分。
误区一
返回变量。如果我们返回a
会报错,不是闭包。
1 | def f1(): |
误区二
函数内部a
被赋值,则a
会被认为是局部变量,不再会引用外部的环境变量。
1 | def f1(): |
正确的写法
与是否返回无关,但必须要引用a
。如下例中c=20*a
或return a
的写法均可。
1 | def f1(): |
闭包的用处
需要注意的是,闭包并不是必不可少的,它只是函数式编程的一种写法。在实际应用过程中,是否使用闭包是根据需求而定的。
下面我们通过一个问题来了解闭包在实际问题中的应用。
假设有个旅行者,x=0
代表地标中的起点,通过调用函数计算旅行者当前的位置。例如:旅行者走了3步,结果为3;旅行者继续走了5步,则结果为8。
我们不难发现,这一问题的关键在于:每次调用函数时,需要保存上一次结果状态。
非闭包的写法
先看一下常规思路的写法:
1 | origin = 0 |
代码会报错:local variable 'origin' referenced before assignment
。按理解,当我们执行:
new_pos = origin + step
origin
内部没有变量,则会沿着作用域链向上寻找。但由于我们写了 origin = new_pos
,在定义时,等号左边的变量会被认为是局部变量,默认此时的变量在函数内部存在的,就不会在外部寻找了。
所以当我们注释掉origin = new_pos
,代码将不再报错,但origin在外部寻找,输出结果为:
1 | print(go(2)) # 0 |
我们期望的是:函数中的origin作为全局变量来使用。所以加上global
关键字即可。
1 | origin = 0 |
闭包的写法
1 | origin = 0 |
代码报错:local variable 'pos' referenced before assignment
,是因为 pos = new_pos
,pos
被认为是局部变量,在使用前new_pos = pos + step
未声明。
在此引入nonlocal
,可强制声明某变量不是局部变量。
1 | origin = 0 |
尽管闭包(函数式编程)的思维方式很烧脑,但我们仍然推荐闭包的写法。因为过多使用全局变量是糟糕的行为,函数无法保证自封闭性,就缺少了它存在的意义。在闭包的写法中,全局变量origin
始终为0,我们并没有对全局变量进行更改。
小结
1.闭包是工厂模式
2.环境变量常驻内存,容易造成内存泄露。
3.闭包python和javascript,python强调环境变量,javascript的切入点则是可实现函数外部调用函数内部的变量。
我们仍使用之前的例子:
1 | def curve_pre(): |
我们调用f
的时候,实质是调用curve()
函数,但curve()
内部引入了环境变量a
,对于外部模块,a
是个局部变量,则实现了模块级别(函数外部)调用函数内部的变量的机制。