Python 的闭包简介
闭包是较难理解的概念,Python 初学者可以暂时跳过此节。学习此节时需要理解 “函数是第一类对象” 的概念,在词条 “Python 的 lambda 表达式” 中详细介绍了这一概念。
本节首先讲解理解闭包所需要的铺垫知识,最后再引入闭包的定义。
1. 嵌套定义函数
1.1 在函数内部定义函数
Python 允许嵌套定义函数,可以在函数中定义函数,例如:
def outter():
def inner():
print('Inside inner')
print('Inside outter')
inner()
outter()
- 在第 1 行,定义函数 outter
- 在第 2 行,在函数 outter 内部,定义函数 inner
- 在第 6 行,在函数 outter 内部,调用函数 inner
函数 inner 定义在函数 outter 中,被称为函数嵌套定义。运行程序,输出结果如下:
Inside outter
Inside inner
1.2 实现信息隐藏
定义在函数内部的函数,对外是不可见的,例如:
def outter():
def inner():
print('inside inner')
print('inside outter')
inner()
inner()
- 在第 1 行,定义了外部函数 outter
- 在第 2 行,定义了内部函数 inner
- 在第 6 行,在函数 outter 中,调用函数 inner
- 在第 8 行,调用函数 inner
程序运行,输出如下:
Traceback (most recent call last):
File "visible.py", line 8, in <module>
inner()
NameError: name 'inner' is not defined
在第 4 行,试图调用定义在函数 outter 内部定义的函数 inner,程序运行时报错:name ‘inner’ is not defined,即找不到函数 inner。
因为函数 inner 是定义在函数 outter 内部的,函数 inner 对外部是不可见的,因此函数 outter 向外界隐藏了实现细节 inner,被称为信息隐藏。
1.3 实现信息隐藏的例子
实现一个复杂功能的函数时,在函数内部定义大量的辅助函数,这些辅助函数对外不可见。例如,假设要实现一个函数 complex,函数的功能非常复杂,将函数 complex 的功能分解为 3 个子功能,使用三个辅助函数 f1、f2、f3 完成对应的子功能,代码如下:
def f1():
print('Inside f1')
def f2():
print('Inside f2')
def f3():
print('Inside f3')
def complex():
print('Inside complex')
f1()
f2()
f3()
- 在第 1 行,定义了辅助函数 f1
- 在第 4 行,定义了辅助函数 f2
- 在第 7 行,定义了辅助函数 f3
- 在第 10 行,定义了主函数 complex,它通过调用 f1、f2、f3 实现自己的功能
在以上的实现中,函数 f1、f2、f3 是用于实现 complex 的辅助函数,我们希望它们仅仅能够被 complex 调用,而不会被其它函数调用。如果可以将函数 f1、f2、f3 定义在函数 complex 的内部,如下所示:
def complex():
def f1():
print('Inside f1')
def f2():
print('Inside f2')
def f3():
print('Inside f3')
print('Inside complex')
f1()
f2()
f3()
- 在第 2 行,在函数 complex 内部定义函数 f1
- 在第 4 行,在函数 complex 内部定义函数 f2
- 在第 6 行,在函数 complex 内部定义函数 f3
- 在第 10 行到第 12 行,调用 f1、f2、f3 实现函数 complex 的功能
2. 内部函数访问外部函数的局部变量
嵌套定义函数时,内部函数可能需要访问外部函数的变量,例子代码如下:
def outter():
local = 123
def inner(local):
print('Inside inner, local = %d', local)
inner(local)
outter()
- 在第 1 行,定义了外部函数 outter
- 在第 2 行,定义了函数 outter 的局部变量 local
- 在第 4 行,定义了内部函数 inner
- 函数 inner 需要访问函数 outter 的局部变量 local
- 在第 7 行,将函数 outter 的局部变量 local 作为参数传递给函数 inner
- 在第 5 行,函数 inner 就可以访问函数 outter 的局部变量 local
程序运行结果如下:
Inside inner, local = 123
在上面的例子中,将外部函数 outter 的局部变量 local 作为参数传递给内部函数 inner。Python 允许内部函数 inner 不通过参数传递直接访问外部函数 outter 的局部变量,简化了参数传递,代码如下:
def outter():
local = 123
def inner():
print('Inside inner, local = %d', local)
inner()
- 在第 1 行,定义了外部函数 outter
- 在第 2 行,定义了函数 outter 的局部变量 local
- 在第 4 行,定义了内部函数 inner
- 函数 inner 需要访问函数 outter 的局部变量 local
- 在第 5 行,函数 inner 可以直接访问函数 outter 的局部变量 local
- 在第 7 行,不用传递参数,直接调用函数 inner()
3. 局部变量的生命周期
通常情况下,函数执行完后,函数内部的局部变量就不存在了。在嵌套定义函数的情况下,如果内部函数访问了外部函数的局部变量,外部函数执行完毕后,内部函数仍然可以访问外部函数的局部变量。示例代码如下:
def outter():
local = 123
def inner():
print('Inside inner, local = ' % local)
return inner
closure = outter()
closure()
- 在第 1 行,定义了外部函数 outter
- 在第 2 行,定义了函数 outter 的局部变量 local
- 在第 4 行,定义了内部函数 inner
- 函数 inner 需要访问函数 outter 的局部变量 local
- 在第 7 行,将函数 inner 作为值返回
- 在第 9 行,调用函数 outter(),将返回值保存到变量 closure 中
- 在第 10 行,调用函数 closure()
运行程序,输出结果如下:
Inside inner, local = 123
注意:在第 10 行,调用函数 closure() 时,外部函数 outter 已经执行完,外部函数 outter 将内部函数 inner 返回并保存到变量 closure。调用函数 closure() 相当于调用内部函数 inner(),因此,在外部函数 outter 已经执行完的情况下,内部函数 inner 仍然可以访问外部函数的局部变量 local。
4. 闭包的概念
闭包的英文是 closure,维基百科中闭包的严谨定义如下:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。—— 维基百科
在本节,以上一节具体的例子说明和理解闭包的概念,上一节的例子程序如下:
def outter():
local = 123
def inner():
print('Inside inner, local = ' % local)
return inner
closure = outter()
closure()
- 在第 2 行,局部变量 local 就是自由变量
- 在第 5 行,内部函数 inner 引用了局部变量 local (即自由变量)
因此,对照闭包的定义,外部函数定义了局部变量 local,引用了局部变量 local 的内部函数 inner 就是闭包。闭包的独特之处在于:外部函数 outter 创造了局部变量 local, 即使外部函数 outter 已经执行完,内部函数 inner 仍然可以继续访问它引用的局部变量 local。
5. 闭包的应用
5.1 概述
闭包经常用于 GUI 编程的事件响应处理函数。编程语言 Javascript 被用于浏览器的用户界面交互,使用 Javascript 编写事件响应处理函数时,闭包也是经常提及的知识点。
本小节通过编写一个简单的 Python GUI 程序,了解为什么需要使用闭包的语法特性,才方便实现功能需求。
5.2 Tk 简介
Tkinter 是 Python 的标准 GUI 库,Python 使用 Tkinter 可以快速的创建 GUI 应用程序。由于 Tkinter 是内置到 python 的安装包中,只要安装好 Python 之后就能使用 Tkinter 库。
由于 Tkinter 简单易学并且不需要安装,因此选择使用 Tk 编写应用闭包的例子程序。
5.3 例子 1:显示一个窗口
下面使用 Tk 编写一个显示窗口的程序,代码如下:
import tkinter
root = tkinter.Tk()
root.mainloop()
- 在第 1 行,引入 Tk 库,Tk 库的名称是 tkinter
- 在第 3 行,tkinter.Tk 方法会创建一个窗口 root
- 在第 4 行,root.mainloop 方法等待用户的操作
运行程序,显示输出如下:
5.4 例子 2:显示一个 button
下面使用 Tk 编写一个显示 button 的程序,代码如下:
import tkinter
root = tkinter.Tk()
button = tkinter.Button(root, text = 'Button')
button.pack()
root.mainloop()
- 在第 4 行,tkinter.Button 方法创建一个新的 Button,它有两个参数:第一个参数 root,指定在 root 窗口中创建 Button;第二个参数 text,指定新创建 Button 的标签
- 在第 5 行,button.pack 方法将 button 放置在 root 窗口中
运行程序,显示输出如下:
5.5 例子 3:为 button 增加一个事件处理函数
当 button 被点击时,希望程序得到通知,需要为 button 增加一个事件处理函数,代码如下:
import tkinter
def on\_button\_click():
print('Button is clicked')
root = tkinter.Tk()
button = tkinter.Button(root, text = 'Button', command = on_button_click)
button.pack()
root.mainloop()
- 在第 3 行,定义了函数 on_button_click,当用户点击 button 时,程序得到通知,执行 on_btton_click
- 在第 4 行,函数 on_button_click 在控制台打印输出 ‘Button is clicked’
- 在第 7 行,tkinter.Button 创建一个 Button,设置 3 个参数
- 参数 root,表示在 root 窗口中创建 button
- 参数 text,表示 button 的标签
- 参数 command,表示当 button 被点击时,对应的事件处理函数
- 在第 8 行,root.mainloop 等待用户的操作,当用户点击 button 时,程序会执行 button 对应的事件处理函数,即执行 on_button_click
运行程序,显示输出如下:
当用户点击 button 时,执行 on_button_click,在控制台中打印 ‘Button is clicked’,显示输出如下:
5.6 如何实现计算器
由于篇幅,本节没有实现一个完整的计算器,在这里仅仅讨论实现计算器程序的关键要点。windows 自带的计算器的界面如下所示:
计算器向用户展示各种按钮,包括:
- 数字按键,0、1、2、3、4、5、6、7、9
- 运算符按键,+、-、*、\、=
用户在点击某个按键时,程序得到通知:按键被点击了,但是这样的信息还不够,为了实现运算逻辑,还需要知道具体是哪一个按键被点击了。
为了区分是哪一个按键被点击了,可以为不同的按键设定不同的按键处理函数,如下所示:
import tkinter
def on\_button0\_click():
print('Button 0 is clicked')
def on\_button1\_click():
print('Button 1 is clicked')
def on\_button2\_click():
print('Button 2 is clicked')
root = tkinter.Tk()
button0 = tkinter.Button(root, text = 'Button 0', command = on_button0_click)
button0.pack()
button1 = tkinter.Button(root, text = 'Button 1', command = on_button0_click)
button1.pack()
button2 = tkinter.Button(root, text = 'Button 2', command = on_button0_click)
button2.pack()
root.mainloop()
为了节省篇幅,这里仅仅处理了 3 个按键。显然,这样的方式是很不合理的,在一个完整的计算器程序中,存在 20 多个按键,如果对每个按键都编写一个事件处理函数,就需要编写 20 多个事件处理函数。在下面的小节中,通过使用闭包解决这个问题。
5.7 例子 4:使用闭包为多个 button 增加事件处理函数
在上面的小节中,面临的问题是:需要为每个 button 编写一个事件处理函数。本小节编写一个事件处理函数响应所有的按键点击事件,代码如下:
import tkinter
def build\_button(root, i):
def on\_button\_click():
print('Button %d is clicked' % i)
title = 'Button ' + str(i)
button = tkinter.Button(root, text = title, command = on_button_click)
button.pack()
root = tkinter.Tk()
for i in range(3):
build_button(root, i)
root.mainloop()
- 在第 11 行,tkinter.Tk 创建窗口 root
- 在第 12 行,使用 for 循环调用 build_button 创建 3 个 button
- 在第 14 行,root.mainloop 等待用户操作
- 在第 3 行,定义函数 build_button 创建 1 个 button
- 参数 root,表示在 root 窗口中创建 button
- 参数 i,表示 button 的序号
- 在第 4 行,定义事件处理函数 on_button_click
- build_button 是外部函数
- on_button_click 是内部函数
- 在第 5 行,打印外部函数 build_button 的参数 i,因此 on_button_click 是一个闭包函数
- 在第 7 行,根据 button 的序号 i 设置 button 的标签
- 在第 7 行,创建一个 button,设置标签和事件处理函数
运行程序,显示输出如下:
当用户点击不同的 button 时,都是执行 on_button_click,但在控制台中打印的字符串是不一样的,显示输出如下:
在这个例子中,外部函数 build_button 提供了参数 i 用于区分 button,内部函数 on_button_click 可以访问外部函数的参数。因此,当 button 被点击时,通过参数 i 知道是哪一个 button 被点击了,编写 1 个事件处理函数就可以处理多个 button 的点击事件,即使用闭包就很自然的解决了实现计算器程序需要面临的问题。
6. 小结
从概念上来看这一个小节还是比较晦涩的,我在文章的开头也说过了初学者可以先跳过这一小节,等后面在转过头来学习。闭包这个概念非常的重要,面试中有很多面试官喜欢问闭包相关的问题,大家一定要多看几遍,彻底掌握闭包。