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. 小结

从概念上来看这一个小节还是比较晦涩的,我在文章的开头也说过了初学者可以先跳过这一小节,等后面在转过头来学习。闭包这个概念非常的重要,面试中有很多面试官喜欢问闭包相关的问题,大家一定要多看几遍,彻底掌握闭包。