本笔记完全基于David Beazley的Python教程-Practical Python.
Returning function and closures
在Python中,函数和整数,字符串一样都是对象.因此函数可以作为另一个函数的返回值,函数可以赋值给变量,函数可以作为参数传递.如
1 | def add(x,y): |
这里的add函数并没有执行加法,而是返回了一个执行加法的新函数do_add.这里的变量a指向的是函数do_add,因此可以用a()的方式来调用内部函数.
上面中我们发现do_add函数可以调用函数体以外的变量,这是内部函数引用由外部函数定义的变量.在此我们需要介绍一下python的变量作用域.Python读取一个变量名,会按照如下顺序查找:L(Local)-E(Enclosing)-G(Global)-B(Built-in).这些分别表示如下作用域
L(Local):最内层,包含局部变量,如一个函数/方法的内部
E(Enclosing): 包含非局部也非全局的变量,例如两个嵌套函数,一个函数A中包含了一个函数B,那么对于B来说A的内部变量就是Enclosing作用域.
G(Global):当前脚本的最外层,比如当前模块的全局变量
B(Built-in):包含了内建的变量/关键字等,最后被搜索

1 | g_count=0 #Global |
最后的内建变量/关键字是通过一个名为builtin的标准模块来实现,我们可以用如下代码来查看预定义的变量,
1 | import builtins |
Python中只有模块(module),类(class)以及函数(def,lambda)才可以引入新的作用域,其他的代码如if/elif/else,try/except,for/while是不会引入新的作用域,也就是这些语句定义的变量,代码块外也是可以访问的.类其实构造的也不是作用域,而是一种临时命名空间,所以类中的函数仍然需要使用显式调用属性,因此类的作用域更多是基于函数形成的.
1 | if True: |
定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域.局部变量只能在其被声明的函数内部访问,全局变量可以在整个程序范围内访问.在函数内部声明的变量只在函数内部的作用域中有效,调用函数时,这些内部变量会被加入到函数内部的作用域中,并且不会影响到函数外部的同名变量.
1 | total=0 |
从此我们可看出,虽然全局变量可以在整个文件都可以被访问,但如果函数内部定义了同名的局部变量,那么就会把他给覆盖掉.
上面我们提到了同名的局部变量会把全局变量给覆盖掉,如果我们真的想要修改全局变量,那么可以使用global关键字,但是在使用global关键字之前一定要保证这个变量出现并存在过.
1 | num = 1 |
同样如果想要修改嵌套作用域(enclosing作用域)的变量,那么就需要使用nonlocal关键字,如下所示
1 | def outer(): |
当一个内部函数引用了外部函数的局部变量,并且该内部函数在外部函数返回后仍然存在,这个内部函数就称为闭包.所以闭包可以认为就是一个函数被封装在一个局部变量环境中.这样可以延长局部变量的生存时间,如上面的add函数,当add函数执行结束,理论上局部变量应该被释放掉,但是由于do_add还需要使用这些变量,因此Python仍然会保留这些变量在闭包环境中.我们可以用如下方式来查看局部变量在闭包中的生存方式,
1 | a = add(3, 4) |
这里的局部变量并不是直接复制到函数参数里面,而是通过存放在闭包单元中等待访问,do_add通过cell来间接访问它们.闭包的关键特性为闭包保留了函数正常运行所需的所有变量的值.可以将闭包视为一个函数加上一个额外的环境,该环境保存它所依赖的变量的值.
闭包的经典用途一:延迟求值,其代码如下:
1 | def after(seconds, func): |
他的执行逻辑是先执行add函数,将参数绑定到闭包单元并且返回闭包do_add,30s后after再执行do_add函数.所以其实是先绑定函数参数到闭包单元,然后再执行闭包函数逻辑.
闭包的经典用途二:避免代码重复.闭包本质上是生成函数的函数.
1 | def make_adder(n): |
他的优势在于不需要写多个基本上一样的函数,参数n成为函数的内置配置.
对于闭包有一个十分细节的问题是晚绑定(Late Binding),代码如下
1 | funcs = [] |
因为闭包捕获的是变量i的值,而不是i当时的值.因为其实这里的i并没有立刻求值,而是后续调用的时候才会去找i,这个时候循环已经结束了,所以i已经是2了.如果我们要调用i的当前值可以做如下改动
1 | funcs = [] |
我们给几个闭包比较经典的示例:计数器,缓存计算和模拟私有变量.
1 | #计数器 |
这里的count定义在make_counter函数中,counter闭包会调用count这个变量,并利用nonlocal关键字修改enclosing环境变量.每调用一次c(),count都会自增一次.
1 | def memorized_power(): |
这里利用闭包来维护一个私有的cache字典,只有通过闭包函数来访问cache字典.
1 | def make_account(initial_balance): |
这里的balance作为enclosing环境变量的方式存储,他被完全封装在闭包函数里面,因此通过闭包函数返回的函数来操作其值.