1. 引言
此前,我们在介绍 java8 新增的 lambda 表达式时,曾经介绍过“闭包”的概念。
所谓的“闭包”,指的就是可以包含自由变量的代码块,代码块中包含的自由变量并没有在定义时绑定任何对象,他们也不是在这个代码块内或任何全局上下文中定义的,而是在代码块环境中定义的局部变量。 简单的来说,闭包是一个独立的代码块,但是他可以访问其定义体之外的非全局变量。 很多语言通过匿名函数来实现闭包特性,著名的 lambda 表达式就是一个典型的闭包的例子。 python 对闭包有着很好的支持。
2. 闭包实例 — 求解平均数
假设我们有一个方法,每次调用都输出历史所有调用传入参数的总平均数:
代码语言:javascript复制>>> avg(10)
10
>>> avg(11)
10.5
>>> avg(39)
20
我们如何来实现呢?下面就是一个闭包的例子:
代码语言:javascript复制>>> def make_average():
... series = []
... def avg(value):
... series.append(value)
... return sum(series)/len(series)
... return avg
...
>>> avg = make_average()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(39)
20.0
在 avg = make_average() 语句执行完成后,make_average 方法的栈空间就会被销毁,而在其栈空间内定义的 series 变量应该会随着 make_average 方法执行的结束而被销毁。 但令人意外的是,此后 avg 方法的执行并没有出错,其内部对 series 列表的添加并没有报错,那么 series 变量究竟定义在哪里呢? 此前我们介绍过 python 的作用域,其中提到了 Enclosing 作用域(嵌套函数的外层函数内部) — 嵌套作用域(闭包) python 的名称空间与作用域
当 python 解释器看到嵌套函数内部使用了外部该局部变量时,解释器会将其标记为自由变量,从而不会随着局部作用域一起被销毁。
3. python 闭包可能存在的问题 — nonlocal 关键字
上面的例子我们进一步修改:
代码语言:javascript复制>>> def make_average():
... count = total = 0
... def avg(value):
... count = 1
... total = value
... return total / count
... return avg
...
>>> avg = make_average()
>>> avg(10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in avg
UnboundLocalError: local variable 'count' referenced before assignment
看上去这个例子与上面闭包的例子没什么区别,他将 series 变量改为了保存总和与调用次数的两个变量,但是却在调用时报错,因为外部 count 与 total 随着 make_average 方法的调用结束而被销毁了,这又是为什么呢? 当解释器看到在嵌套内部的 avg 函数中,对 count 与 total 两个变量均有赋值行为,于是他们被当做了 avg 方法局部作用域中的变量,而不是自由变量,于是外部的两个局部变量就被正常销毁了。 python3 引入了 nonlocal 关键字,用于解决这样的问题:
代码语言:javascript复制>>> def make_average():
... count = total = 0
... def avg(value):
... nonlocal count, total
... count = 1
... total = value
... return total / count
... return avg
...
>>> avg = make_average()
>>> avg(10)
10
>>> avg(11)
10.5
>>> avg(39)
20
4. 通过可调用对象实现上述问题
代码语言:javascript复制>>> class Average:
... def __init__(self):
... self.series = []
... def __call__(self, value):
... self.series.append(value)
... return sum(self.series)/len(self.series)
...
>>> avg = Average()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(39)
20.0
这个例子通过类实现了对历史调用信息的封装,并通过 __call__ 方法实现了计算逻辑。 通常来说,闭包能够实现的功能都可以通过类的方式来实现,类也是通常最容易想到的解决方案,那么,闭包的优势又体现在哪里呢? 在 python 中,闭包最重要的使用方式是在装饰器中,那么,装饰器究竟是什么?闭包与装饰器结合又能碰撞出什么样的火花呢? 我们即将会有一篇文章详尽介绍装饰器的用法与原理,敬请期待。