在谈 Python 装饰器之前,先看闭包在维基上的定义:
闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)
下面通过一些代码来理解闭包的概念。
# hello_closure.py bar = "Hello!" # 自由变量 bar print("1. hello_closure module bar: %s" % bar) def foo(): global bar # 参见名字搜索规则 LEGB print("3. hello_closure module bar: %s" % bar) # 开始引用外部的自由变量 bar = bar + " Closure!" print("4. hello_closure module bar: %s" % bar) if __name__ == "__main__": print("2. hello_closure module bar: %s" % bar) foo() print("5. hello_closure module bar: %s" % bar) foo() print("6. hello_closure module bar: %s" % bar) bar = "Hello!" print("7. hello_closure module bar: %s" % bar) 1. hello_closure module bar: Hello! 2. hello_closure module bar: Hello! 3. hello_closure module bar: Hello! 4. hello_closure module bar: Hello! Closure! 5. hello_closure module bar: Hello! Closure! 3. hello_closure module bar: Hello! Closure! 4. hello_closure module bar: Hello! Closure! Closure! 6. hello_closure module bar: Hello! Closure! Closure! 7. hello_closure module bar: Hello!
可以看到,模块 hello_closure 中,函数 foo 引用了外部的自由变量 bar,并且可以修改外部变量。但很显然 foo 和 bar 的组合还并不是一个闭包(foo 函数依赖 bar 变量的存在,bar 变量单独存在)。那么,怎么才能让自由变量与函数一同存在,也即让自由变量和函数对外表现为一个整体呢?答案是看下方的代码。
# test.py - 该模块与 hello_closure.py 同级 from hello_closure import foo bar = "Hi!" # 以下三次 foo 函数调用属于同一个闭包实例 print("1. test module bar: %s" % bar) foo() print("2. test module bar: %s" % bar) foo() print("3. test module bar: %s" % bar) foo() 1. hello_closure module bar: Hello! 1. test module bar: Hi! 3. hello_closure module bar: Hello! 4. hello_closure module bar: Hello! Closure! 2. test module bar: Hi! 3. hello_closure module bar: Hello! Closure! 4. hello_closure module bar: Hello! Closure! Closure! 3. test module bar: Hi! 3. hello_closure module bar: Hello! Closure! Closure! 4. hello_closure module bar: Hello! Closure! Closure! Closure!
我们通过一些调用策略(在模块 test 中调用 foo 函数,延长了自由变量的生命周期,让自由变量依赖函数 foo 存在),使得自由变量 bar 的生命周期与 foo 函数的生命周期一致。函数 foo 与变量 bar 组合成了运行期实体,即产生了一个闭包(此闭包是广义上的闭包,但不是 Python 中的闭包,以下介绍的如无特殊说明均是 Python 中的闭包)。那么,如何强制让函数和自由变量的生命周期一致?一定产生闭包呢?继续看下方代码。
# some_closures.py def create_closure_bar(): foo = "Hello!" def bar(): print(foo) foo = foo + " Closure!" # 对于读操作,根据 Python 的 LEGB 规则从变量名称关联的目标对象进行读取,对于写操作,默认始终从 L(ocals) 中查找目标对象 return bar # 如果非要实现等同之前示例的闭包,则 Python 3 中可以写成下方这样 # def create_closure_bar(): # foo = "Hello!" # 局部变量 foo # def bar(): # nonlocal foo # 声明 foo 变量为非 L(ocals) 变量。nonlocal 关键字用来在函数或其他作用域中使用外层(非全局)变量,也即 nonlocal 只能绑定局部变量 # foo = foo + " Closure!" # print(foo) # return bar # 如果非要实现等同之前示例的闭包,则 Python 2/3 中可以写成下方这样 # def create_closure_bar(): # foo = ["Hello!"] # 容器变量 foo,其元素 "Hello!" 存储在堆上 # def bar(): # foo[0] = foo[0] + " Closure!" # foo[0] 指向 "Hello!" 的引用,而不是 "Hello!" 这个目标对象,因此,可以进行写操作 # print(foo[0]) # return bar if __name__ == "__main__": bar = create_closure_bar() # 此时可以保证变量 foo 和闭包函数 bar 的生命周期一致,创建闭包函数 bar bar() # 调用闭包函数(相当于在前文 test 模块中调用 bar) bar() # 由于 LEGB 规则仅限于读取操作,因此这里 foo 变量的值与上一次调用一致。也即每次调用,都是单独的闭包实例 Hello! Closure! Hello! Closure!
作为调用方,你应该也发现了,闭包其实就是普通的函数调用,它只是在函数的基础上将一些状态及对这些状态的逻辑进行了封装,形成一个更自然更易被调用的接口函数(在这里可以简单理解为:避免调用方每次调用原函数 foo 都自己去编写调用的上下文 bar)。 这也说明,一个封装优雅的闭包,一定是通过分析上下文,简化调用方的操作,让调用方可以在不知晓内部实现细节的情况下,达到它的目的。
下面举一些《Python 3 学习笔记》中例子,让我们体会什么是闭包。
# examples.py # example 1 def make(): x = [1, 2] return lambda: print(x) a = make() a() [1, 2]
# example 2 def make(): x = 100 def test(): print(x) return test f = make() f() 100
# example 3 def make(): x = [1, 2] print(hex(id(x))) return lambda: print(x) f = make() print(f.__closure__) # 闭包所引用的环境变量(自由变量),它被保存在函数对象的 __closure__ 属性中 f() print(f.__code__.co_freevars) # ('x',) 当前函数引用外部自由变量列表 print(make.__code__.co_cellvars) # ('x',) 被内部闭包函数引用的变量列表 0x1f635713a88 (<cell at 0x000001F635721558: list object at 0x000001F635713A88>,) [1, 2] ('x',) ('x',)
# example 4 def make(x): return lambda: print(x) a = make([1, 2]) b = make(100) print(a is b) # False 每次返回新的函数对象实例 print(a.__closure__) print(b.__closure__) print(a.__closure__[0].cell_contents) # cell_contents 属性返回闭包中的自由变量 False (<cell at 0x000001F6357215E8: list object at 0x000001F6357282C8>,) (<cell at 0x000001F635721A08: int object at 0x00007FF977197D60>,) [1, 2]
# example 5 多个闭包共享同一自由变量。实质上和我们之前一个闭包的不同实例共享一个自由变量并更改了该自由变量值的例子是类似的 def queue(): data = [] push = lambda x: data.append(x) pop = lambda: data.pop(0) if data else None return push, pop push, pop = queue() # push 与 pop 共享自由变量 data print(push.__closure__ == pop.__closure__) # True 值相等 print(push.__closure__) print(pop.__closure__) for i in range(10, 13): push(i) while True: x = pop() if not x: break print(x) True (<cell at 0x000001F635619408: list object at 0x000001F635725108>,) (<cell at 0x000001F635619408: list object at 0x000001F635725108>,) 10 11 12
# example 6 自引用 1 def make(x): def test(): test.x = x # 引用自己,效果相当于 this,并增加属性 x print(test.x) return test a, b = make(1234), make([1, 2]) print(a.__closure__) # 自由变量中包括闭包本身和属性 x print(b.__closure__) # 自由变量中包括闭包本身和属性 x a() b() print(hasattr(a, "x")) # True (<cell at 0x000001F6357212B8: function object at 0x000001F63572CC18>, <cell at 0x000001F635721B58: int object at 0x000001F63573E270>) (<cell at 0x000001F6357217C8: function object at 0x000001F63572C3A8>, <cell at 0x000001F635721408: list object at 0x000001F635728908>) 1234 [1, 2] True
# example 7 延迟绑定 def make(n): x = [] for i in range(n): x.append(lambda: print(i)) return x a, b, c = make(3) a(), b(), c() # 2 2 2 x 列表 x 中的闭包在执行中均保存变量 i 的引用,当 make 执行结束,i 等于 2,执行闭包时,打印引用变量 i 的值 2 print(a.__closure__) # 存储的是 cell 对象,该对象内部引用了函数关联的自由变量(而不是直接存储自由变量) print(b.__closure__) print(c.__closure__) 2 2 2 (<cell at 0x000001F635619A38: int object at 0x00007FF977197120>,) (<cell at 0x000001F635619A38: int object at 0x00007FF977197120>,) (<cell at 0x000001F635619A38: int object at 0x00007FF977197120>,)
# example 8 使用复制解决延迟绑定问题 广义上的闭包,非 Python 闭包 def make(n): x = [] for i in range(n): x.append(lambda o=i: print(0)) return x a, b, c = make(3) a(), b(), c() # 0 1 2 匿名函数的参数 o 是私有变量,也即变量 o 是不共享的。其复制执行时当前 i 引用的整数对象的引用 print(a.__closure__) # None print(b.__closure__) # None print(c.__closure__) # None 0 1 2 None None None
# example 9 自引用 2 在其他模块导入 make 时,可形成广义上的闭包,非 Python 闭包 x = "hello" def make(): make.x = x # 引用自己,效果相当于 this,并增加属性 x print(make.x) make() print(make.__closure__) # None hello None
# example 10 自引用 3 在其他模块导入 make 时,非广义上的闭包,也非 Python 闭包。 def make(x): make.x = x # 引用自己,效果相当于 this,并增加属性 x print(make.x) make("closure") print(make.__closure__) # None closure None
示例 example 10 可以通过在第二个模块使用偏函数 partial 固定参数 x 创建绑定自由变量的闭包函数,并在第三个模块中使用该闭包函数,使之成为广义闭包。但这很明显没有任何意义,还有很多缺点。下面引用《Python 3 学习笔记》对闭包优缺点的总结,来完结这篇文章。
闭包的缺点可能和优点一样明显。 闭包具备封装特征,可实现隐式上下文状态,并减少参数,在设计上,其可部分替代全局变量,或将执行环境与调用接口分离。 其首要缺点是,对自由变量隐式依赖,会提升代码的复杂度,这直接影响测试和维护。其次,自由变量生命周期的提升,会提高占用内存。
最后,虽然闭包提供了将调用函数与上下文环境打包的另一种抽象,便利了调用方,但它增加了维护的难度和内存的开销,请大家慎重使用。
原文始发于:Python 闭包与装饰器(上篇)
主题测试文章,只做测试使用。发布者:熱鬧獨處,转转请注明出处:http://www.cxybcw.com/11757.html