Python 闭包

在函数式编程中,有一个很重要的特性 - 闭包,很多编程语言(例如:Golang 和 Python)都支持它。闭包的功能十分强大,但也相对比较棘手,因为难以理解和使用。

话虽如此,我会尽可能的为闭包提供一个清晰的解释,并详细介绍 Python 中的闭包支持。在熟悉闭包之后,你会发现它其实很有意思。

1
何为闭包

关于闭包,维基百科描述如下:
在这里插入图片描述
上面涉及一个关键点 - 自由变量
就是说:如果在一个代码块中使用了一个变量,而这个变量并没有被定义在该代码块中,那么该变量就被称为自由变量。

明确了以上概念之后,再来看看创建闭包的条件:

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

下面,我们来一步一步地认识闭包!

2
嵌套函数

在另一个函数中定义的函数称为嵌套函数,需要注意的是,嵌套函数能够访问封闭范围中的变量。

举个栗子:

进群:851211580 可获取海量Python学习资料+大牛指导学习

>>> def outer(msg):
...     def inner():  # 嵌套函数
...         print(msg)
...     inner()
...
>>>
>>> outer('Hello')
Hello

在这里,inner() 就是嵌套函数,它可以很容易地访问变量 msg。

3
定义闭包

对上述示例略作修改,让它直接返回函数对象,而不是调用嵌套函数:

>>> def outer(msg):
...     def inner():
...         print(msg)
...     return inner  # 返回函数对象,而不是调用函数。
...
>>>
>>> func = outer('Hello')
>>> func()
Hello

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

>>> del outer
>>> func()
Hello

可以看到,即使 outer 从当前的命名空间中删除,msg 的值“Hello”也会被记住。

4
闭包的好处

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

  • 闭包可以避免使用全局变量,并提供某种形式的数据隐藏;
  • 当只有几个方法(通常只有一个)时,使用闭包比实现一个类更加简单;
  • 闭包允许我们在其范围之外调用 Python 函数;
  • Python 中的装饰器广泛使用了闭包。

如果要创建一个由不同参数构成的一系列函数(例如:平方和立方),使用传统方式,需要分别实现:

>>> def square(x):  # 求平方
...     return x ** 2
... 
>>> 
>>> def cube(x):    # 求立方
...     return x ** 3
... 
>>> 
>>> square(6)
36
>>> cube(6)
216

换用闭包的话,仅需一个 pow() 就可以搞定了:

>>> def pow(y):
...     def inner(x):
...         return x ** y
...     return inner
...
>>>
>>> square = pow(2)  # 求平方
>>> cube = pow(3)    # 求立方
>>>
>>> square(6)
36
>>> cube(6)
216

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

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

>>> cube.__closure__
(<cell at 0x0000022A903A5DC8: int object at 0x00007FF98A83EF40>,)
>>>
>>> cube.__closure__[0].cell_contents
3

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

5
更改 nonlocal 变量

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

从 2.x 开始,Python 通过词法作用域支持闭包。然而,在特性的最初实现中“有一点小问题”。之所以这么说,是因为在 2.x 中,闭包无法更改 nonlocal 变量(只读的),但从 3.x 起,该问题已经被解决了

问题场景

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

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

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

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

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

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

>>> def outer():
...     count = [0]  # 借助 list
...     def inner():
...         count[0] += 1
...         print('count:', count[0])
...     return inner
...
>>>

尝试一下,看看效果如何:

>>> func = outer()
>>> func()
count: 1
>>> func()
count: 2
>>> func()
count: 3

虽然这种方式可行,但并不算完美,因为不得不改变数据类型(int -> list)。

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

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

再来尝试一下:

>>> func = outer()
>>> func()
count: 1
>>> func()
count: 2
>>> func()
count: 3

完美运行,而且这在使用上也更加简单、合理!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值