python 的闭包特性

2022-06-27 13:26:38 浏览数 (1)

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 中,闭包最重要的使用方式是在装饰器中,那么,装饰器究竟是什么?闭包与装饰器结合又能碰撞出什么样的火花呢? 我们即将会有一篇文章详尽介绍装饰器的用法与原理,敬请期待。

0 人点赞