Python 闭包

作者: 一去、二三里
个人微信号: iwaleon
微信公众号: 高效程序员

闭包是支持函数式编程范式的一个重要特性,在很多编程语言中都可以找到,包括: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' is not defined

可以看到,即使 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' referenced before assignment

这是因为 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)。

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一去丶二三里

有收获,再打赏!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值