Python 的閉包簡介
閉包是較難理解的概念,Python 初學者可以暫時跳過此節(jié)。學習此節(jié)時需要理解 “函數(shù)是第一類對象” 的概念,在詞條 “Python 的 lambda 表達式” 中詳細介紹了這一概念。
本節(jié)首先講解理解閉包所需要的鋪墊知識,最后再引入閉包的定義。
1. 嵌套定義函數(shù)
1.1 在函數(shù)內部定義函數(shù)
Python 允許嵌套定義函數(shù),可以在函數(shù)中定義函數(shù),例如:
def outter():
def inner():
print('Inside inner')
print('Inside outter')
inner()
outter()
- 在第 1 行,定義函數(shù) outter
- 在第 2 行,在函數(shù) outter 內部,定義函數(shù) inner
- 在第 6 行,在函數(shù) outter 內部,調用函數(shù) inner
函數(shù) inner 定義在函數(shù) outter 中,被稱為函數(shù)嵌套定義。運行程序,輸出結果如下:
Inside outter
Inside inner
1.2 實現(xiàn)信息隱藏
定義在函數(shù)內部的函數(shù),對外是不可見的,例如:
def outter():
def inner():
print('inside inner')
print('inside outter')
inner()
inner()
- 在第 1 行,定義了外部函數(shù) outter
- 在第 2 行,定義了內部函數(shù) inner
- 在第 6 行,在函數(shù) outter 中,調用函數(shù) inner
- 在第 8 行,調用函數(shù) inner
程序運行,輸出如下:
Traceback (most recent call last):
File "visible.py", line 8, in <module>
inner()
NameError: name 'inner' is not defined
在第 4 行,試圖調用定義在函數(shù) outter 內部定義的函數(shù) inner,程序運行時報錯:name ‘inner’ is not defined,即找不到函數(shù) inner。
因為函數(shù) inner 是定義在函數(shù) outter 內部的,函數(shù) inner 對外部是不可見的,因此函數(shù) outter 向外界隱藏了實現(xiàn)細節(jié) inner,被稱為信息隱藏。
1.3 實現(xiàn)信息隱藏的例子
實現(xiàn)一個復雜功能的函數(shù)時,在函數(shù)內部定義大量的輔助函數(shù),這些輔助函數(shù)對外不可見。例如,假設要實現(xiàn)一個函數(shù) complex,函數(shù)的功能非常復雜,將函數(shù) complex 的功能分解為 3 個子功能,使用三個輔助函數(shù) 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 行,定義了輔助函數(shù) f1
- 在第 4 行,定義了輔助函數(shù) f2
- 在第 7 行,定義了輔助函數(shù) f3
- 在第 10 行,定義了主函數(shù) complex,它通過調用 f1、f2、f3 實現(xiàn)自己的功能
在以上的實現(xiàn)中,函數(shù) f1、f2、f3 是用于實現(xiàn) complex 的輔助函數(shù),我們希望它們僅僅能夠被 complex 調用,而不會被其它函數(shù)調用。如果可以將函數(shù) f1、f2、f3 定義在函數(shù) 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 行,在函數(shù) complex 內部定義函數(shù) f1
- 在第 4 行,在函數(shù) complex 內部定義函數(shù) f2
- 在第 6 行,在函數(shù) complex 內部定義函數(shù) f3
- 在第 10 行到第 12 行,調用 f1、f2、f3 實現(xiàn)函數(shù) complex 的功能
2. 內部函數(shù)訪問外部函數(shù)的局部變量
嵌套定義函數(shù)時,內部函數(shù)可能需要訪問外部函數(shù)的變量,例子代碼如下:
def outter():
local = 123
def inner(local):
print('Inside inner, local = %d'% local)
inner(local)
outter()
- 在第 1 行,定義了外部函數(shù) outter
- 在第 2 行,定義了函數(shù) outter 的局部變量 local
- 在第 4 行,定義了內部函數(shù) inner
- 函數(shù) inner 需要訪問函數(shù) outter 的局部變量 local
- 在第 7 行,將函數(shù) outter 的局部變量 local 作為參數(shù)傳遞給函數(shù) inner
- 在第 5 行,函數(shù) inner 就可以訪問函數(shù) outter 的局部變量 local
程序運行結果如下:
Inside inner, local = 123
在上面的例子中,將外部函數(shù) outter 的局部變量 local 作為參數(shù)傳遞給內部函數(shù) inner。Python 允許內部函數(shù) inner 不通過參數(shù)傳遞直接訪問外部函數(shù) outter 的局部變量,簡化了參數(shù)傳遞,代碼如下:
def outter():
local = 123
def inner():
print('Inside inner, local = %d'% local)
inner()
- 在第 1 行,定義了外部函數(shù) outter
- 在第 2 行,定義了函數(shù) outter 的局部變量 local
- 在第 4 行,定義了內部函數(shù) inner
- 函數(shù) inner 需要訪問函數(shù) outter 的局部變量 local
- 在第 5 行,函數(shù) inner 可以直接訪問函數(shù) outter 的局部變量 local
- 在第 7 行,不用傳遞參數(shù),直接調用函數(shù) inner()
3. 局部變量的生命周期
通常情況下,函數(shù)執(zhí)行完后,函數(shù)內部的局部變量就不存在了。在嵌套定義函數(shù)的情況下,如果內部函數(shù)訪問了外部函數(shù)的局部變量,外部函數(shù)執(zhí)行完畢后,內部函數(shù)仍然可以訪問外部函數(shù)的局部變量。示例代碼如下:
def outter():
local = 123
def inner():
print('Inside inner, local = %d' % local)
return inner
closure = outter()
closure()
- 在第 1 行,定義了外部函數(shù) outter
- 在第 2 行,定義了函數(shù) outter 的局部變量 local
- 在第 4 行,定義了內部函數(shù) inner
- 函數(shù) inner 需要訪問函數(shù) outter 的局部變量 local
- 在第 7 行,將函數(shù) inner 作為值返回
- 在第 9 行,調用函數(shù) outter(),將返回值保存到變量 closure 中
- 在第 10 行,調用函數(shù) closure()
運行程序,輸出結果如下:
Inside inner, local = 123
注意:在第 10 行,調用函數(shù) closure() 時,外部函數(shù) outter 已經(jīng)執(zhí)行完,外部函數(shù) outter 將內部函數(shù) inner 返回并保存到變量 closure。調用函數(shù) closure() 相當于調用內部函數(shù) inner(),因此,在外部函數(shù) outter 已經(jīng)執(zhí)行完的情況下,內部函數(shù) inner 仍然可以訪問外部函數(shù)的局部變量 local。
4. 閉包的概念
閉包的英文是 closure,維基百科中閉包的嚴謹定義如下:
在計算機科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures),是引用了自由變量的函數(shù)。這個被引用的自由變量將和這個函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外?!?維基百科
在本節(jié),以上一節(jié)具體的例子說明和理解閉包的概念,上一節(jié)的例子程序如下:
def outter():
local = 123
def inner():
print('Inside inner, local = %d' % local)
return inner
closure = outter()
closure()
- 在第 2 行,局部變量 local 就是自由變量
- 在第 5 行,內部函數(shù) inner 引用了局部變量 local (即自由變量)
因此,對照閉包的定義,外部函數(shù)定義了局部變量 local,引用了局部變量 local 的內部函數(shù) inner 就是閉包。閉包的獨特之處在于:外部函數(shù) outter 創(chuàng)造了局部變量 local, 即使外部函數(shù) outter 已經(jīng)執(zhí)行完,內部函數(shù) inner 仍然可以繼續(xù)訪問它引用的局部變量 local。
5. 閉包的應用
5.1 概述
閉包經(jīng)常用于 GUI 編程的事件響應處理函數(shù)。編程語言 Javascript 被用于瀏覽器的用戶界面交互,使用 Javascript 編寫事件響應處理函數(shù)時,閉包也是經(jīng)常提及的知識點。
本小節(jié)通過編寫一個簡單的 Python GUI 程序,了解為什么需要使用閉包的語法特性,才方便實現(xiàn)功能需求。
5.2 Tk 簡介
Tkinter 是 Python 的標準 GUI 庫,Python 使用 Tkinter 可以快速的創(chuàng)建 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 方法會創(chuàng)建一個窗口 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 方法創(chuàng)建一個新的 Button,它有兩個參數(shù):第一個參數(shù) root,指定在 root 窗口中創(chuàng)建 Button;第二個參數(shù) text,指定新創(chuàng)建 Button 的標簽
- 在第 5 行,button.pack 方法將 button 放置在 root 窗口中
運行程序,顯示輸出如下:
5.5 例子 3:為 button 增加一個事件處理函數(shù)
當 button 被點擊時,希望程序得到通知,需要為 button 增加一個事件處理函數(shù),代碼如下:
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 行,定義了函數(shù) on_button_click,當用戶點擊 button 時,程序得到通知,執(zhí)行 on_btton_click
- 在第 4 行,函數(shù) on_button_click 在控制臺打印輸出 ‘Button is clicked’
- 在第 7 行,tkinter.Button 創(chuàng)建一個 Button,設置 3 個參數(shù)
- 參數(shù) root,表示在 root 窗口中創(chuàng)建 button
- 參數(shù) text,表示 button 的標簽
- 參數(shù) command,表示當 button 被點擊時,對應的事件處理函數(shù)
- 在第 9 行,root.mainloop 等待用戶的操作,當用戶點擊 button 時,程序會執(zhí)行 button 對應的事件處理函數(shù),即執(zhí)行 on_button_click
運行程序,顯示輸出如下:
當用戶點擊 button 時,執(zhí)行 on_button_click,在控制臺中打印 ‘Button is clicked’,顯示輸出如下:
5.6 如何實現(xiàn)計算器
由于篇幅,本節(jié)沒有實現(xiàn)一個完整的計算器,在這里僅僅討論實現(xiàn)計算器程序的關鍵要點。windows 自帶的計算器的界面如下所示:

- 數(shù)字按鍵,0、1、2、3、4、5、6、7、9
- 運算符按鍵,+、-、*、\、=
用戶在點擊某個按鍵時,程序得到通知:按鍵被點擊了,但是這樣的信息還不夠,為了實現(xiàn)運算邏輯,還需要知道具體是哪一個按鍵被點擊了。
為了區(qū)分是哪一個按鍵被點擊了,可以為不同的按鍵設定不同的按鍵處理函數(shù),如下所示:
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()
為了節(jié)省篇幅,這里僅僅處理了 3 個按鍵。顯然,這樣的方式是很不合理的,在一個完整的計算器程序中,存在 20 多個按鍵,如果對每個按鍵都編寫一個事件處理函數(shù),就需要編寫 20 多個事件處理函數(shù)。在下面的小節(jié)中,通過使用閉包解決這個問題。
5.7 例子 4:使用閉包為多個 button 增加事件處理函數(shù)
在上面的小節(jié)中,面臨的問題是:需要為每個 button 編寫一個事件處理函數(shù)。本小節(jié)編寫一個事件處理函數(shù)響應所有的按鍵點擊事件,代碼如下:
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 創(chuàng)建窗口 root
- 在第 12 行,使用 for 循環(huán)調用 build_button 創(chuàng)建 3 個 button
- 在第 14 行,root.mainloop 等待用戶操作
- 在第 3 行,定義函數(shù) build_button 創(chuàng)建 1 個 button
- 參數(shù) root,表示在 root 窗口中創(chuàng)建 button
- 參數(shù) i,表示 button 的序號
- 在第 4 行,定義事件處理函數(shù) on_button_click
- build_button 是外部函數(shù)
- on_button_click 是內部函數(shù)
- 在第 5 行,打印外部函數(shù) build_button 的參數(shù) i,因此 on_button_click 是一個閉包函數(shù)
- 在第 7 行,根據(jù) button 的序號 i 設置 button 的標簽
- 在第 8 行,創(chuàng)建一個 button,設置標簽和事件處理函數(shù)
運行程序,顯示輸出如下:
當用戶點擊不同的 button 時,都是執(zhí)行 on_button_click,但在控制臺中打印的字符串是不一樣的,顯示輸出如下:
在這個例子中,外部函數(shù) build_button 提供了參數(shù) i 用于區(qū)分 button,內部函數(shù) on_button_click 可以訪問外部函數(shù)的參數(shù)。因此,當 button 被點擊時,通過參數(shù) i 知道是哪一個 button 被點擊了,編寫 1 個事件處理函數(shù)就可以處理多個 button 的點擊事件,即使用閉包就很自然的解決了實現(xiàn)計算器程序需要面臨的問題。
6. 小結
從概念上來看這一個小節(jié)還是比較晦澀的,我在文章的開頭也說過了初學者可以先跳過這一小節(jié),等后面在轉過頭來學習。閉包這個概念非常的重要,面試中有很多面試官喜歡問閉包相關的問題,大家一定要多看幾遍,徹底掌握閉包。