Python学习笔记: 闭包
闭包的基本定义
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。详见 维基百科
Python 中的闭包
首先从例子引入 Python3 中的闭包
1 #!/usr/bin/env python3 2 #-*- coding:utf-8 -*- 3 #File Name:01_closure.py 4 #Created Time:2019-01-09 09:32:20 5 6 7 def line(k, b): 8 # 在函数内部再定义一个函数,并且这个函数用到了外部函数的变量, 9 # 这个内部函数的用到的相关变量即被称为闭包 10 # 本例中,k,b 和 x 即为闭包 11 def foo(x): 12 return k*x + b 13 return foo # 返回内部函数的引用 14 15 16 if __name__ == "__main__": 17 18 print("***** y = x + 1 *****") 19 a = line(1,1) 20 print("x = 1 --> y = %d" % a(1)) 21 print("x = 2 --> y = %d" % a(2)) 22 print("x = 3 --> y = %d" % a(3)) 23 24 print("***** y = 4 * x + 1 *****") 25 b = line(4,1) 26 print("x = 1 --> y = %d" % b(1)) 27 print("x = 2 --> y = %d" % b(2)) 28 print("x = 3 --> y = %d" % b(3))
运行结果:
***** y = x + 1 ***** x = 1 --> y = 2 x = 2 --> y = 3 x = 3 --> y = 4 ***** y = 4 * x + 1 ***** x = 1 --> y = 5 x = 2 --> y = 9 x = 3 --> y = 13
分析:
- 这个例子中,函数line与变量a,b构成闭包。在创建闭包的时候,通过line的参数 k, b 说明了这两个变量的取值,这样,就确定了函数的最终形式(y = x + 1和y = 4x + 1)。只需要变换参数a,b,就可以获得不同的直线表达函数。由此,可以看出,闭包具有跟函数类似的提高代码可复用性的作用。
- 如果没有闭包,每次新创建直线函数的时候需要同时指定 k,b,x。这样,就需要更多的参数传递,也减少了代码的可移植性。
注意点:
- 由于闭包引用了外部函数的局部变量,则外部函数的局部变量没有及时释放,会消耗内存。
- 但是相较于类而言,其已经大大降低了内存占用
为了进一步的对 Python3 中的闭包的调用流程进行说明,再看以下例程:
1 #!/usr/bin/env python3 2 #-*- coding:utf-8 -*- 3 #File Name: 02_closure_test.py 4 #Created Time:2019-01-09 09:32:20 5 6 import sys 7 8 def line(k, b): 9 # 在函数内部再定义一个函数,并且这个函数用到了外部函数的变量, 10 # 这个内部函数的用到的相关变量即被称为闭包 11 # 本例中,k,b 和 x 即为闭包 12 def foo(x): 13 return k*x + b 14 print("foo 函数的 id 为:\t\t%d" % id(foo)) 15 return foo 16 17 18 if __name__ == "__main__": 19 20 print("***** y = x + 1 *****") 21 print("line 的 id 为: \t\t\t%d" % id(line)) 22 a = line(1,1) # 会打印 foo 的 id 23 print("a = line(1,1),a 的 id 为: \t%d" % id(a)) 24 print("x = 1 --> y = %d" % a(1)) 25 print("***** y = 3*x + 2 *****") 26 print("line 的 id 为: \t\t\t%d" % id(line)) 27 b = line(3,2) # 会打印 foo 的 id 28 print("b = line(3,2),b 的 id 为: \t%d" % id(b)) 29 print("x = 1 --> y = %d" % b(1))
运行结果:
***** y = x + 1 ***** line 的 id 为: 2452122878432 foo 函数的 id 为: 2452124629872 a = line(1,1),a 的 id 为: 2452124629872 x = 1 --> y = 2 ***** y = 3*x + 2 ***** line 的 id 为: 2452122878432 foo 函数的 id 为: 2452124630008 b = line(3,2),b 的 id 为: 2452124630008 x = 1 --> y = 5
从结果中可看出,line 函数的 id 没有发生改变,而不同闭包的 id 则各不相同。说明闭包在运行时可以有多个不同的实例
运行过程分析
Python 解释器运行到 1 处时,会将 line 函数加载到内存中。
运行到 2 处时,会调用内存中的 line 函数,创建(图中的 3 )相应的内存空间 8,通过 14 行中的 retrun 语句,使得 a 指向了内存 8 中的 foo 函数。 第 26 行执行的过程类似,只不过又开辟了一块新的内存空间。
由于 line 函数中的 foo 被外部程序所引用,因此 8,9 内存空间并不会随着 line 函数运行的结束而被释放。从而使用相应的 foo 函数可以被重复调用。
Python3 闭包修改外部变量的值
可以借助 noloacl 关键字,也可借助列表,下面的例程演示 nolocal 关键字实现的方法:
1 #!/usr/bin/env python3 2 #-*- coding:utf-8 -*- 3 #File Name: 03_closure_modify_local_var.py 4 #Created Time:2019-01-09 09:32:20 5 6 import sys 7 8 def line(k, b): 9 # 在函数内部再定义一个函数,并且这个函数用到了外部函数的变量, 10 # 这个内部函数的用到的相关变量即被称为闭包 11 # 本例中,k,b 和 x 即为闭包 12 def foo(x): 13 nonlocal k 14 k += 1 15 print("***** k = %d" % k) 16 return k*x + b 17 return foo 18 19 20 if __name__ == "__main__": 21 22 print("***** y = x + 1 *****") 23 a = line(1,1) 24 print("x = 1 --> y = %d" % a(1)) 25 print("x = 2 --> y = %d" % a(2)) 26 print("x = 3 --> y = %d" % a(3)) 27 28 print("***** y = 4*x + 1 *****") 29 b = line(4,1) 30 print("x = 1 --> y = %d" % b(1)) 31 print("x = 2 --> y = %d" % b(2)) 32 print("x = 3 --> y = %d" % b(3))
运行结果:
***** y = x + 1 ***** ***** k = 2 x = 1 --> y = 3 ***** k = 3 x = 2 --> y = 7 ***** k = 4 x = 3 --> y = 13 ***** y = 4*x + 1 ***** ***** k = 5 x = 1 --> y = 6 ***** k = 6 x = 2 --> y = 13 ***** k = 7 x = 3 --> y = 22
程序中如果没有用 nolocal 关键字,则程序会报错,因为 Python 解释器不能断定此时的 k 到底是函数 line 内的变量,还是函数 foo 类的局部变量。