python tips:最内嵌套作用域规则,闭包与装饰器
在作用域与名字空间提到,python是静态作用域,变量定义的位置决定了变量作用的范围。变量沿着local,global,builtins的路径搜索,直觉上就是从里到外搜索变量,这称为最内嵌套作用域规则。
从里到外的搜索
1 a = 1 2 3 def f(): 4 a = 2 5 def b(): 6 print(a) 7 b() 8 9 f()
输出结果
1 2
最内嵌套作用域规则有一个神奇的特性,它对local变量的搜索只依赖于静态代码的组成,而与代码如何调用没有关系。
闭包
1 a = 1 2 3 def f(): 4 a = 2 5 def b(): 6 print(a) 7 return b 8 9 b = f() 10 b()
输出结果
1 2
函数f返回函数b,在第九行调用f得到函数b,此时函数f调用完成,应该被销毁,它包含的局部变量a也应该随之销毁。所以调用函数b时应该得不到函数f的变量a才对,事实却相反,函数b打印出了本应被销毁的a变量。
局部作用域是静态实现的,跟代码如何调用,何时调用没有关系。对于局部变量a来说,它的作用域是函数f内,当然包含了函数b。而且在函数b中确实引用了a,为了让b在任何时候都能够得到它,a会将自己绑定到函数b上。
这样b得到了一个额外的变量a,在函数f被销毁后,仍然能够使用变量a。这种变量与函数绑定的结果称为闭包(closure),这种绑定的变量是静态变量(类似于为函数添加了属性)。
直观上,闭包的作用就是为函数添加静态变量。
函数计数器
1 def fn(): 2 print("call fn") 3 4 def count(fn): 5 i = [0] 6 def s(*arg): 7 i[0] += 1 8 print(f"times: {i[0]}") 9 fn(*arg) 10 return s 11 12 fn = count(fn) 13 for _ in range(5): 14 fn()
输出结果
1 times: 1 2 call fn 3 times: 2 4 call fn 5 times: 3 6 call fn 7 times: 4 8 call fn 9 times: 5 10 call fn
有了闭包,不用传入额外的参数,函数自身就能记住状态的变化(闭包提供静态变量)。
装饰器
1 def count(fn): 2 i = [0] 3 def s(*arg): 4 i[0] += 1 5 print(f"times: {i[0]}") 6 fn(*arg) 7 return s 8 9 @count 10 def fn(): 11 print("call fn") 12 13 for _ in range(5): 14 fn()
装饰器其实就是一个语法糖,fn上方加@count,等价于fn = count(fn)。
计数功能仍靠闭包实现。
@count必须出现在count定义函数之后,否则无法识别。
总结:
1. 最内嵌套作用域对于local变量的搜索只与代码组成有关,与动态运行环境无关
2. 闭包是外层变量与内层函数的绑定结果,主要作用是位函数添加静态变量
3. 装饰器是语法糖,装饰函数一般都返回一个闭包。