简述

闭包是支持函数式编程范式的一个重要特性,在很多编程语言中都可以找到,包括:JavaScript、Python 和 Ruby。闭包十分强大,也非常有用,但是也很棘手,因为难以理解和使用。

下面,我会尽可能的为闭包提供一个清晰的解释,并详细介绍 Python 中的闭包支持。熟悉闭包之后,你会发现其实它非常有趣。

| 版权声明:一去、二三里,未经博主允许不得转载。

闭包

关于闭包,来看维基百科上的词条:

在计算机科学中,闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

上面涉及一个关键点 - 自由变量,根据 ​​Python 文档​​ 描述:

If a variable is used in a code block but not defined there, it is a free variable.

也就是说:如果在一个代码块中使用了一个变量,而这个变量并没有被定义在该代码块中,那么该变量就被称为自由变量。

产生闭包的条件

要创建闭包,必须满足以下条件:

  • 必须包含一个嵌套函数
  • 嵌套函数必须引用封闭函数中定义的值(自由变量)
  • 封闭函数必须返回嵌套函数

无码不欢,来用一个简单的程序来说明:

>>> def outer(x):
... def inner():
... return x
... return inner # 返回的函数没有圆括号
...
>>> f = outer('Hello')
>>> f()
'Hello'

当外部函数 ​​outer(x)​​​ 被调用时,一个闭包 ​​inner()​​​ 就形成了,并且该闭包持有自由变量 - ​​x​​​。这也意味着,当函数 ​​outer(x)​​​ 的生命周期结束之后,变量 ​​x​​ 的值依然会被记住。

>>> del outer
>>> f()
'Hello'
>>>
>>> outer('Hello')
Traceback (most recent call last):
...
NameError: name 'outer'

可以看到,即使 ​​outer​​​ 从当前的命名空间中删除,​​x​​​ 的值(​​'Hello'​​)也会被记住。

闭包的好处

那么,闭包的好处是什么呢?

  • 取代硬编码中的常量
  • 避免使用全局值,并提供某种形式的数据隐藏。
  • 提供一致的函数签名
  • 实现面向对象

注意: 当类中几乎没有(大多数情况只有一种)方法时,闭包可以提供一种更优雅的替代方案。但是,当属性和方法的数量较多时,最好通过类来实现。

如果要创建一个由不同参数构成的一系列函数。例如,关于正方形和立方体的函数,仅指数不同(分别为:2 和 3)。

使用传统方式:

>>> def square(x):  # 正方形
... return x ** 2
...
>>>
>>> def cube(x): # 立方形
... return x ** 3
...
>>>
>>> square(2)
4
>>>
>>> cube(2)
8

换用闭包来实现,仅需一个 ​​fpower()​​ 就可以构造这些函数:

>>> def fpower(exp):
... def inner(x):
... return x ** exp
... return inner
...
>>> square = fpower(2)
>>> cube = fpower(3)
>>>
>>> square(2)
4
>>>
>>> cube(2)
8

这样做的好处是:​​fpower​​ 可以用来构建任何一个指数(2、3、4、…)。

所有函数对象都有一个 ​​__closure__​​​ 属性,如果这个函数是一个闭包函数,那么会返回的是一个由 ​​cell​​​ 对象组成的元组。​​cell​​​ 对象具有 ​​cell_contents​​ 属性,存储了闭包中的自由变量。

>>> fpower.__closure__
>>> cube.__closure__
(<cell at 0x7fbfaccc4fa8: int object at 0x8beac0>,)
>>>
>>> cube.__closure__[0].cell_contents
3

这也解释了为什么局部变量在脱离函数之后,还可以在函数之外被访问,因为它存储在了闭包的 ​​cell_contents​​ 中。

词法作用域

词法作用域(Lexical Scoping):变量的作用域在定义时决定,而不是在执行时决定。也就是说,词法作用域取决于源码,通过静态分析就能够确定,因此,词法作用域也叫做静态作用域。

从 2.x 开始,Python 通过词法作用域支持闭包。然而,Python 在特性的最初实现中犯了一些小错误。请原谅我的冒犯,之所以这么说,是因为在 2.x 中,闭包无法更改非本地(​​non-local​​)变量,这是 Python 的词法作用域规则中的一个固有问题,其名称只能绑定到本地范围或全局范围。

自 Python 3.x 起,这个问题已经被解决掉了,见 ​​PEP-3104​​。

考虑下面的例子,每当调用函数时,为计数器加 1。

>>> def outer():
... count = 0
... def inner():
... count += 1
... return count
... return inner
...

看起来好像没任何问题,但是很遗憾,执行时会引发错误:

>>> f = outer()
>>> f()
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count'

这是因为 count 是一个不可变类型,当在内部范围对其重新分配时,它会被看作是一个新变量,由于它还没有被定义,所以会发生错误。

幸运的是,Python 3.x 引入了 nonlocal 关键字,用于标识外部作用域的变量。

>>> def outer():
... count = 0
... def inner():
... nonlocal count # 使用 nonlocal
... count += 1
... return count
... return inner
...

再次运行,结果和预期一样 - perfect:

>>> f = outer()
>>> f()
1
>>> f()
2
>>> f()
3

在 Python 2.x 中,要解决此问题,需要借助一个可变数据类型(例如:list 或 dict)。

>>> def outer():
... count = [0] # 使用 list
... def inner():
... count[0] += 1
... return count[0]
... return inner
...
>>>
>>> f = outer()
>>> f()
1
>>> f()
2
>>> f()
3

显然,这不算完美,因为不得不改变数据类型(int -> list)。