在函数式编程中,有一个很重要的特性 - 闭包,很多编程语言(例如: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
完美运行,而且这在使用上也更加简单、合理!